test(FN-5083): add case-insensitive task-done branch canonicalization
This commit is contained in:
committed by
gsxdsm
parent
c56e0cf814
commit
a2f4b99ecc
@@ -0,0 +1,92 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import "./executor-test-helpers.js";
|
||||||
|
import { TaskExecutor } from "../executor.js";
|
||||||
|
import * as worktreePool from "../worktree-pool.js";
|
||||||
|
import * as branchAutocorrect from "../branch-autocorrect.js";
|
||||||
|
import { createMockStore, mockedCreateFnAgent, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
|
||||||
|
|
||||||
|
function baseTask() {
|
||||||
|
return {
|
||||||
|
id: "FN-5083",
|
||||||
|
title: "Branch case check",
|
||||||
|
description: "",
|
||||||
|
column: "in-progress",
|
||||||
|
worktree: "/repo/.worktrees/amber-hawk",
|
||||||
|
branch: "fusion/fn-5083",
|
||||||
|
baseCommitSha: "abc123",
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
steps: [{ name: "Step 1", status: "in-progress" as const }],
|
||||||
|
currentStep: 0,
|
||||||
|
dependencies: [],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setup() {
|
||||||
|
const store = createMockStore();
|
||||||
|
let task: any = baseTask();
|
||||||
|
let tool: any;
|
||||||
|
|
||||||
|
store.getTask.mockImplementation(async () => ({ ...task, steps: task.steps.map((s: any) => ({ ...s })) }));
|
||||||
|
store.moveTask.mockImplementation(async (id: string, column: string) => {
|
||||||
|
task = { ...task, id, column, paused: false, pausedByAgentId: null, status: null, error: null };
|
||||||
|
});
|
||||||
|
|
||||||
|
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
|
||||||
|
tool = customTools.find((t: any) => t.name === "fn_task_done");
|
||||||
|
return { session: { prompt: vi.fn().mockResolvedValue(undefined), dispose: vi.fn() } } as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
|
await executor.execute(baseTask() as any);
|
||||||
|
return { tool, store };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("executor fn_task_done case-insensitive branch handling", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetExecutorMocks();
|
||||||
|
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
|
||||||
|
vi.spyOn(branchAutocorrect, "attemptBranchAutocorrect").mockResolvedValue({ status: "renamed" });
|
||||||
|
mockedExecSync.mockImplementation((cmd: string) => {
|
||||||
|
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/amber-hawk\n");
|
||||||
|
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-5083\n");
|
||||||
|
if (cmd.includes("rev-list --count")) return Buffer.from("1\n");
|
||||||
|
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
|
||||||
|
return Buffer.from("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows exact branch matches", async () => {
|
||||||
|
const { tool } = await setup();
|
||||||
|
const result = await tool.execute("id", {});
|
||||||
|
expect(result.content[0].text).toContain("Task marked complete");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("auto-canonicalizes case-only mismatches", async () => {
|
||||||
|
const { tool } = await setup();
|
||||||
|
mockedExecSync.mockImplementation((cmd: string) => {
|
||||||
|
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/amber-hawk\n");
|
||||||
|
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/FN-5083\n");
|
||||||
|
if (cmd.includes("rev-list --count")) return Buffer.from("1\n");
|
||||||
|
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
|
||||||
|
return Buffer.from("");
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await tool.execute("id", {});
|
||||||
|
expect(result.content[0].text).toContain("Task marked complete");
|
||||||
|
expect(branchAutocorrect.attemptBranchAutocorrect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still refuses genuine wrong branches", async () => {
|
||||||
|
const { tool } = await setup();
|
||||||
|
mockedExecSync.mockImplementation((cmd: string) => {
|
||||||
|
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/amber-hawk\n");
|
||||||
|
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("main\n");
|
||||||
|
return Buffer.from("");
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await tool.execute("id", {});
|
||||||
|
expect(result.content[0].text).toContain("fn_task_done refused: wrong_branch");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -44,6 +44,7 @@ import type { SandboxBackend } from "./sandbox/types.js";
|
|||||||
import { ModelRegistry, SessionManager, type ToolDefinition, type AgentSession } from "@mariozechner/pi-coding-agent";
|
import { ModelRegistry, SessionManager, type ToolDefinition, type AgentSession } from "@mariozechner/pi-coding-agent";
|
||||||
import { PRIORITY_EXECUTE, type AgentSemaphore } from "./concurrency.js";
|
import { PRIORITY_EXECUTE, type AgentSemaphore } from "./concurrency.js";
|
||||||
import { RemovalReason, classifyTaskWorktree, describeRegisteredWorktrees, getRegisteredWorktreePaths, isGitRepository, isInsideWorktreesDir, isRegisteredGitWorktree, removeWorktree, type WorktreePool } from "./worktree-pool.js";
|
import { RemovalReason, classifyTaskWorktree, describeRegisteredWorktrees, getRegisteredWorktreePaths, isGitRepository, isInsideWorktreesDir, isRegisteredGitWorktree, removeWorktree, type WorktreePool } from "./worktree-pool.js";
|
||||||
|
import { attemptBranchAutocorrect } from "./branch-autocorrect.js";
|
||||||
import { ActiveSessionWorktreeRemovalError } from "./worktree-backend.js";
|
import { ActiveSessionWorktreeRemovalError } from "./worktree-backend.js";
|
||||||
import { activeSessionRegistry, executingTaskLock } from "./active-session-registry.js";
|
import { activeSessionRegistry, executingTaskLock } from "./active-session-registry.js";
|
||||||
import {
|
import {
|
||||||
@@ -5358,6 +5359,31 @@ export class TaskExecutor {
|
|||||||
});
|
});
|
||||||
const observedBranch = stdout.trim();
|
const observedBranch = stdout.trim();
|
||||||
if (observedBranch && observedBranch !== branchName) {
|
if (observedBranch && observedBranch !== branchName) {
|
||||||
|
if (observedBranch.toLowerCase() === branchName.toLowerCase()) {
|
||||||
|
executorLog.log(`${task.id}: branch case-mismatch detected; canonicalizing observed=${observedBranch} expected=${branchName}`);
|
||||||
|
const autocorrectResult = await attemptBranchAutocorrect({
|
||||||
|
worktreePath,
|
||||||
|
observedBranch,
|
||||||
|
expectedBranch: branchName,
|
||||||
|
rootDir: this.rootDir,
|
||||||
|
});
|
||||||
|
if (autocorrectResult.status !== "failed") {
|
||||||
|
const auditor = createRunAuditor(this.store, this.getRunContextFor(task.id));
|
||||||
|
await auditor.git({
|
||||||
|
type: "branch:auto-canonicalize-case",
|
||||||
|
target: worktreePath,
|
||||||
|
metadata: {
|
||||||
|
taskId: task.id,
|
||||||
|
observed: observedBranch,
|
||||||
|
expected: branchName,
|
||||||
|
worktreePath,
|
||||||
|
mode: autocorrectResult.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
executorLog.warn(`${task.id}: failed to canonicalize branch case mismatch: ${autocorrectResult.reason ?? "unknown"}`);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
reason: "wrong_branch",
|
reason: "wrong_branch",
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export type GitMutationType =
|
|||||||
| "merge:layer3:scope-override-bypass"
|
| "merge:layer3:scope-override-bypass"
|
||||||
| "merge:audit-failure"
|
| "merge:audit-failure"
|
||||||
| "branch:auto-reclaim"
|
| "branch:auto-reclaim"
|
||||||
|
| "branch:auto-canonicalize-case"
|
||||||
| "branch:stale-active-reclaim"
|
| "branch:stale-active-reclaim"
|
||||||
| "branch:stale-active-reclaim-deferred"
|
| "branch:stale-active-reclaim-deferred"
|
||||||
| "branch:orphan-prune"
|
| "branch:orphan-prune"
|
||||||
|
|||||||
Reference in New Issue
Block a user