fix(FN-5353): fallback merge reuse handoff to cwd-main
This commit is contained in:
@@ -349,6 +349,52 @@ describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
|
|||||||
}
|
}
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
|
it.skipIf(!hasGit)("falls back to cwd-main when reuse is requested without a task worktree", async () => {
|
||||||
|
const fixture = await makeReliabilityFixture({
|
||||||
|
taskId: "FN-5353-RI-MISSING-WORKTREE",
|
||||||
|
settings: {
|
||||||
|
baseBranch: "master",
|
||||||
|
mergeIntegrationWorktree: "reuse-task-worktree",
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { rootDir, store, task } = fixture;
|
||||||
|
const actualTask = await store.getTask(task.id);
|
||||||
|
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
|
||||||
|
|
||||||
|
git(rootDir, "git branch -m main master");
|
||||||
|
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
|
||||||
|
await store.updateTask(task.id, {
|
||||||
|
baseBranch: "master",
|
||||||
|
branch,
|
||||||
|
worktree: null,
|
||||||
|
steps: completedSteps,
|
||||||
|
currentStep: completedSteps.length,
|
||||||
|
} as any);
|
||||||
|
await fixture.createBranch(branch);
|
||||||
|
await fixture.writeAndCommit("packages/engine/src/fn-5353-ri-missing-worktree.ts", "export const fallback = true;\n", "feat: add fallback merge content");
|
||||||
|
await fixture.checkout("master");
|
||||||
|
store.enqueueMergeQueue(task.id);
|
||||||
|
|
||||||
|
const result = await aiMergeTask(store, rootDir, task.id);
|
||||||
|
expect(result.merged).toBe(true);
|
||||||
|
expect((await store.getTask(task.id))?.column).toBe("done");
|
||||||
|
const audits = store.getRunAuditEvents({ taskId: task.id });
|
||||||
|
const auditTypes = audits.map((event) => event.mutationType);
|
||||||
|
expect(auditTypes).toContain("merge:reuse-fallback-cwd-main");
|
||||||
|
expect(auditTypes).not.toContain("merge:reuse-handoff-acquired");
|
||||||
|
const fallback = audits.find((event) => event.mutationType === "merge:reuse-fallback-cwd-main");
|
||||||
|
expect(fallback?.metadata).toMatchObject({
|
||||||
|
reason: "missing-task-worktree",
|
||||||
|
worktreePath: null,
|
||||||
|
integrationBranch: "master",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await fixture.cleanup();
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
it.skipIf(!hasGit)("cwd-main mode stays on the legacy path and emits no reuse handoff events", async () => {
|
it.skipIf(!hasGit)("cwd-main mode stays on the legacy path and emits no reuse handoff events", async () => {
|
||||||
const fixture = await makeReliabilityFixture({
|
const fixture = await makeReliabilityFixture({
|
||||||
taskId: "FN-5279-RI-CWD-MAIN",
|
taskId: "FN-5279-RI-CWD-MAIN",
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ import { accumulateSessionTokenUsage } from "./session-token-usage.js";
|
|||||||
import { createResolvedAgentSession, extractRuntimeHint, resolveMergerSessionModel } from "./agent-session-helpers.js";
|
import { createResolvedAgentSession, extractRuntimeHint, resolveMergerSessionModel } from "./agent-session-helpers.js";
|
||||||
import { createFallbackModelObserver } from "./fallback-model-observer.js";
|
import { createFallbackModelObserver } from "./fallback-model-observer.js";
|
||||||
import { buildSessionSkillContext } from "./session-skill-context.js";
|
import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||||
import { RemovalReason, removeWorktree, type WorktreePool } from "./worktree-pool.js";
|
import { classifyTaskWorktree, RemovalReason, removeWorktree, type WorktreePool } from "./worktree-pool.js";
|
||||||
import { activeSessionRegistry } from "./active-session-registry.js";
|
import { activeSessionRegistry } from "./active-session-registry.js";
|
||||||
import { AgentLogger } from "./agent-logger.js";
|
import { AgentLogger } from "./agent-logger.js";
|
||||||
import { mergerLog } from "./logger.js";
|
import { mergerLog } from "./logger.js";
|
||||||
@@ -6386,6 +6386,7 @@ export async function aiMergeTask(
|
|||||||
| "merge:reuse-handoff-refused"
|
| "merge:reuse-handoff-refused"
|
||||||
| "merge:reuse-handoff-released"
|
| "merge:reuse-handoff-released"
|
||||||
| "merge:reuse-handoff-deferred-to-worktrunk"
|
| "merge:reuse-handoff-deferred-to-worktrunk"
|
||||||
|
| "merge:reuse-fallback-cwd-main"
|
||||||
| "branch:auto-canonicalize-case",
|
| "branch:auto-canonicalize-case",
|
||||||
metadata: Record<string, unknown>,
|
metadata: Record<string, unknown>,
|
||||||
target: string,
|
target: string,
|
||||||
@@ -6402,18 +6403,47 @@ export async function aiMergeTask(
|
|||||||
const requestedIntegrationMode = settings.mergeIntegrationWorktree === "cwd-main"
|
const requestedIntegrationMode = settings.mergeIntegrationWorktree === "cwd-main"
|
||||||
? "cwd-main"
|
? "cwd-main"
|
||||||
: "reuse-task-worktree";
|
: "reuse-task-worktree";
|
||||||
const integrationRoot = resolveMergeIntegrationRoot({
|
let integrationRoot = resolveMergeIntegrationRoot({
|
||||||
task,
|
task,
|
||||||
settings,
|
settings,
|
||||||
projectRoot: projectRootDir,
|
projectRoot: projectRootDir,
|
||||||
});
|
});
|
||||||
const reuseTaskWorktreeMerge = integrationRoot.mode === "reuse-task-worktree";
|
let reuseTaskWorktreeMerge = integrationRoot.mode === "reuse-task-worktree";
|
||||||
rootDir = integrationRoot.rootDir;
|
rootDir = integrationRoot.rootDir;
|
||||||
const integrationRemote = await resolveIntegrationRemote({
|
let integrationRemote = await resolveIntegrationRemote({
|
||||||
settings,
|
settings,
|
||||||
rootDir: rootDir,
|
rootDir: rootDir,
|
||||||
integrationBranch: mergeTarget.branch,
|
integrationBranch: mergeTarget.branch,
|
||||||
});
|
});
|
||||||
|
const fallbackReuseIntegrationToCwdMain = async (
|
||||||
|
reason: string,
|
||||||
|
diagnostics: Record<string, unknown>,
|
||||||
|
): Promise<void> => {
|
||||||
|
integrationRoot = {
|
||||||
|
...integrationRoot,
|
||||||
|
mode: "cwd-main",
|
||||||
|
rootDir: projectRootDir,
|
||||||
|
};
|
||||||
|
reuseTaskWorktreeMerge = false;
|
||||||
|
rootDir = projectRootDir;
|
||||||
|
integrationRemote = await resolveIntegrationRemote({
|
||||||
|
settings,
|
||||||
|
rootDir,
|
||||||
|
integrationBranch: mergeTarget.branch,
|
||||||
|
});
|
||||||
|
await emitReuseHandoffAuditEvent(
|
||||||
|
"merge:reuse-fallback-cwd-main",
|
||||||
|
{
|
||||||
|
taskId,
|
||||||
|
reason,
|
||||||
|
worktreePath: task.worktree ?? null,
|
||||||
|
diagnostics,
|
||||||
|
integrationRemote: integrationRemote ?? null,
|
||||||
|
integrationBranch: mergeTarget.branch,
|
||||||
|
},
|
||||||
|
rootDir,
|
||||||
|
);
|
||||||
|
};
|
||||||
if (
|
if (
|
||||||
settings.worktrunk?.enabled === true
|
settings.worktrunk?.enabled === true
|
||||||
&& requestedIntegrationMode === "reuse-task-worktree"
|
&& requestedIntegrationMode === "reuse-task-worktree"
|
||||||
@@ -6432,6 +6462,22 @@ export async function aiMergeTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let reuseHandoff: HandoffResult | undefined;
|
let reuseHandoff: HandoffResult | undefined;
|
||||||
|
if (integrationRoot.mode === "reuse-task-worktree") {
|
||||||
|
const reusableWorktreePath = task.worktree?.trim();
|
||||||
|
if (!reusableWorktreePath) {
|
||||||
|
await fallbackReuseIntegrationToCwdMain("missing-task-worktree", {
|
||||||
|
requestedMode: requestedIntegrationMode,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const classification = await classifyTaskWorktree(projectRootDir, reusableWorktreePath);
|
||||||
|
if (!classification.ok) {
|
||||||
|
await fallbackReuseIntegrationToCwdMain("unusable-task-worktree", {
|
||||||
|
requestedMode: requestedIntegrationMode,
|
||||||
|
classification,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (integrationRoot.mode === "reuse-task-worktree") {
|
if (integrationRoot.mode === "reuse-task-worktree") {
|
||||||
try {
|
try {
|
||||||
reuseHandoff = await acquireReuseHandoff({
|
reuseHandoff = await acquireReuseHandoff({
|
||||||
@@ -6453,8 +6499,10 @@ export async function aiMergeTask(
|
|||||||
},
|
},
|
||||||
reuseHandoff.worktreePath,
|
reuseHandoff.worktreePath,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof MergeHandoffRefusedError) {
|
if (!(error instanceof MergeHandoffRefusedError)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
await emitReuseHandoffAuditEvent(
|
await emitReuseHandoffAuditEvent(
|
||||||
"merge:reuse-handoff-refused",
|
"merge:reuse-handoff-refused",
|
||||||
{
|
{
|
||||||
@@ -6465,10 +6513,28 @@ export async function aiMergeTask(
|
|||||||
},
|
},
|
||||||
integrationRoot.rootDir,
|
integrationRoot.rootDir,
|
||||||
);
|
);
|
||||||
}
|
const reusableWorktreePath = task.worktree?.trim();
|
||||||
|
if (!reusableWorktreePath) {
|
||||||
|
await fallbackReuseIntegrationToCwdMain("missing-task-worktree-after-refusal", {
|
||||||
|
requestedMode: requestedIntegrationMode,
|
||||||
|
gate: error.gate,
|
||||||
|
reason: error.reason,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const classification = await classifyTaskWorktree(projectRootDir, reusableWorktreePath);
|
||||||
|
if (!classification.ok) {
|
||||||
|
await fallbackReuseIntegrationToCwdMain("unusable-task-worktree-after-refusal", {
|
||||||
|
requestedMode: requestedIntegrationMode,
|
||||||
|
gate: error.gate,
|
||||||
|
reason: error.reason,
|
||||||
|
classification,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const requestedBaseRef = task.mergeDetails?.mergeTargetBranch?.trim() || mergeTarget.branch;
|
const requestedBaseRef = task.mergeDetails?.mergeTargetBranch?.trim() || mergeTarget.branch;
|
||||||
const releaseReuseHandoffEarly = async (outcome: string): Promise<void> => {
|
const releaseReuseHandoffEarly = async (outcome: string): Promise<void> => {
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export type GitMutationType =
|
|||||||
| "merge:reuse-handoff-refused"
|
| "merge:reuse-handoff-refused"
|
||||||
| "merge:reuse-handoff-released"
|
| "merge:reuse-handoff-released"
|
||||||
| "merge:reuse-handoff-deferred-to-worktrunk"
|
| "merge:reuse-handoff-deferred-to-worktrunk"
|
||||||
|
| "merge:reuse-fallback-cwd-main"
|
||||||
| "merge:audit-failure"
|
| "merge:audit-failure"
|
||||||
| "branch:auto-reclaim"
|
| "branch:auto-reclaim"
|
||||||
| "branch:auto-canonicalize-case"
|
| "branch:auto-canonicalize-case"
|
||||||
|
|||||||
Reference in New Issue
Block a user