feat(FN-4499): complete Step 5 — add acquisition misbinding warning
Fusion-Task-Id: FN-4499 Fusion-Task-Lineage: 3e9fee75-5c3d-4ce5-aa8b-3f2bb94e48eb
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { promisify } from "node:util";
|
||||
import { acquireTaskWorktree } from "../worktree-acquisition.js";
|
||||
|
||||
vi.mock("../worktree-pool.js", async () => {
|
||||
@@ -117,3 +118,52 @@ describe("acquireTaskWorktree", () => {
|
||||
expect(runConfiguredCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("acquireTaskWorktree foreign start-point warning", () => {
|
||||
it("emits warning/log for fusion/fn-* start point with foreign-attributed tip and stays silent for main", async () => {
|
||||
vi.resetModules();
|
||||
const warn = vi.fn();
|
||||
const logEntry = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const execMock: any = (command: string, _opts: any, cb: any) => cb(null, "", "");
|
||||
execMock[promisify.custom] = (command: string) => {
|
||||
if (command.startsWith("git rev-parse --verify \"fusion/fn-4367^")) {
|
||||
return Promise.resolve({ stdout: "deadbeefdeadbeef\n", stderr: "" });
|
||||
}
|
||||
if (command.startsWith("git log -1 --format=%s%x1f%b")) {
|
||||
return Promise.resolve({ stdout: "feat(FN-4367): dep\u001fFusion-Task-Id: FN-4367\n", stderr: "" });
|
||||
}
|
||||
return Promise.resolve({ stdout: "", stderr: "" });
|
||||
};
|
||||
|
||||
vi.doMock("node:child_process", () => ({ exec: execMock }));
|
||||
const mod = await import("../worktree-acquisition.js");
|
||||
|
||||
await mod.acquireTaskWorktree({
|
||||
task: { id: "FN-4488", title: "Task", description: "Desc", branch: null, worktree: null, executionStartBranch: "fusion/fn-4367" } as any,
|
||||
rootDir: "/tmp/repo",
|
||||
store: { updateTask: vi.fn().mockResolvedValue(undefined), logEntry } as any,
|
||||
settings: {},
|
||||
logger: { log: vi.fn(), warn, error: vi.fn() },
|
||||
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/repo/.worktrees/x", branch: "fusion/fn-4488" }),
|
||||
});
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining("worktree acquired with foreign-task start point: fusion/fn-4367"));
|
||||
expect(logEntry).toHaveBeenCalledWith("FN-4488", expect.stringContaining("worktree acquired with foreign-task start point: fusion/fn-4367"), undefined, undefined);
|
||||
|
||||
warn.mockClear();
|
||||
logEntry.mockClear();
|
||||
|
||||
await mod.acquireTaskWorktree({
|
||||
task: { id: "FN-4488", title: "Task", description: "Desc", branch: null, worktree: null, executionStartBranch: "main" } as any,
|
||||
rootDir: "/tmp/repo",
|
||||
store: { updateTask: vi.fn().mockResolvedValue(undefined), logEntry } as any,
|
||||
settings: {},
|
||||
logger: { log: vi.fn(), warn, error: vi.fn() },
|
||||
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/repo/.worktrees/x", branch: "fusion/fn-4488" }),
|
||||
});
|
||||
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
expect(logEntry).not.toHaveBeenCalledWith("FN-4488", expect.stringContaining("foreign-task start point"), undefined, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,6 +57,38 @@ function configuredCommandErrorMessage(result: { spawnError?: string | Error; ti
|
||||
return `Command exited with code ${result.exitCode ?? "unknown"}`;
|
||||
}
|
||||
|
||||
async function maybeWarnForeignTaskStartPoint(
|
||||
input: {
|
||||
baseBranch: string | null;
|
||||
rootDir: string;
|
||||
worktreePath: string;
|
||||
branch: string;
|
||||
taskId: string;
|
||||
logger?: { warn: (m: string) => void };
|
||||
store: TaskStore;
|
||||
runContext?: RunMutationContext;
|
||||
},
|
||||
): Promise<void> {
|
||||
const { baseBranch, rootDir, worktreePath, branch, taskId, logger, store, runContext } = input;
|
||||
if (!baseBranch || !/^fusion\/fn-\d+$/i.test(baseBranch)) return;
|
||||
|
||||
try {
|
||||
const tipSha = (await execAsync(`git rev-parse --verify ${JSON.stringify(`${baseBranch}^{commit}`)}`, { cwd: rootDir, encoding: "utf-8" })).stdout.trim();
|
||||
const details = (await execAsync(`git log -1 --format=%s%x1f%b ${JSON.stringify(tipSha)}`, { cwd: worktreePath, encoding: "utf-8" })).stdout.trim();
|
||||
const [subject = "", body = ""] = details.split("\u001f");
|
||||
const subjectMatch = subject.match(/^(?:feat|fix|test|chore|docs|refactor|perf|build)\((FN-\d+)\):/i);
|
||||
const trailerMatch = body.match(/(?:^|\n)Fusion-Task-Id:\s*(FN-\d+)\s*(?:\n|$)/i);
|
||||
const attributedTaskId = (trailerMatch?.[1] ?? subjectMatch?.[1] ?? "").toUpperCase();
|
||||
if (!attributedTaskId || attributedTaskId === taskId.toUpperCase()) return;
|
||||
|
||||
const warning = `worktree acquired with foreign-task start point: ${baseBranch} (resolved tip ${tipSha.slice(0, 12)}) — bootstrap-misbinding recovery may engage on contamination check`;
|
||||
logger?.warn(`${taskId}: ${warning}`);
|
||||
await store.logEntry(taskId, warning, undefined, runContext);
|
||||
} catch {
|
||||
// best-effort observability only
|
||||
}
|
||||
}
|
||||
|
||||
async function createWorktreeFallback(
|
||||
rootDir: string,
|
||||
branch: string,
|
||||
@@ -171,6 +203,16 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro
|
||||
} else {
|
||||
await store.logEntry(task.id, `Acquired worktree from pool: ${worktreePath}`, undefined, runContext);
|
||||
}
|
||||
await maybeWarnForeignTaskStartPoint({
|
||||
baseBranch,
|
||||
rootDir,
|
||||
worktreePath,
|
||||
branch,
|
||||
taskId: task.id,
|
||||
logger,
|
||||
store,
|
||||
runContext,
|
||||
});
|
||||
const hydrated = await hydrate(worktreePath);
|
||||
return {
|
||||
worktreePath,
|
||||
@@ -230,6 +272,16 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro
|
||||
}
|
||||
}
|
||||
|
||||
await maybeWarnForeignTaskStartPoint({
|
||||
baseBranch,
|
||||
rootDir,
|
||||
worktreePath,
|
||||
branch,
|
||||
taskId: task.id,
|
||||
logger,
|
||||
store,
|
||||
runContext,
|
||||
});
|
||||
const hydrated = await hydrate(worktreePath);
|
||||
return { worktreePath, branch, source: acquiredFromPool ? "pool" : "fresh", hydrated, isResume: false };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user