feat(FN-5419): add stash conflict modal gating and smart pull routing for m

Implements a pull-based merge workflow by wiring the merger pull helpers from the engine, extending the git pull and stash routes, and aligning the `MergeAdvanceNotice` and `StashConflictModal` components to gate dismissal on stash drop. The `run-audit` module is updated with pull mutation documenta

Fusion-Task-Id: FN-5419

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5419
This commit is contained in:
gsxdsm
2026-05-23 12:59:49 -07:00
parent 7a20b95502
commit 14bc63e813
15 changed files with 603 additions and 742 deletions

View File

@@ -88,16 +88,51 @@ describe("run-audit provisioning mutation types", () => {
]);
});
it("accepts smart-pull git mutation types", async () => {
it("accepts pull:fast-forward metadata shape", async () => {
const store = new AuditStoreStub();
const auditor = createRunAuditor(store as unknown as TaskStore, { runId: "r1", agentId: "a1", taskId: "FN-5358" });
const auditor = createRunAuditor(store as unknown as TaskStore, { runId: "r1", agentId: "a1", taskId: "FN-5419" });
const types: GitMutationType[] = ["pull:fast-forward", "stash:pop-conflict", "push:origin"];
for (const type of types) {
await auditor.git({ type, target: "feature/worktree", metadata: { taskId: "FN-5358" } });
}
const type: GitMutationType = "pull:fast-forward";
await auditor.git({
type,
target: "/repo/.worktrees/integration",
metadata: {
taskId: "FN-5419",
worktreePath: "/repo/.worktrees/integration",
integrationBranch: "main",
remote: "origin",
fromSha: "1111111",
toSha: "2222222",
durationMs: 12,
succeeded: true,
behind: 0,
ahead: 0,
},
});
expect(store.events.map((event) => event.mutationType)).toEqual(types);
expect(store.events[0]?.mutationType).toBe(type);
});
it("accepts stash:pop-conflict metadata shape", async () => {
const store = new AuditStoreStub();
const auditor = createRunAuditor(store as unknown as TaskStore, { runId: "r1", agentId: "a1", taskId: "FN-5419" });
const type: GitMutationType = "stash:pop-conflict";
await auditor.git({
type,
target: "/repo/.worktrees/integration",
metadata: {
taskId: "FN-5419",
worktreePath: "/repo/.worktrees/integration",
stashSha: "abc123",
stashLabel: "fusion-autostash-FN-5419",
conflictedFiles: ["README.md"],
autostashOutcome: "conflict-needs-manual",
advice: "Resolve conflicts and drop stash when complete",
},
});
expect(store.events[0]?.mutationType).toBe(type);
});
it("records merge:scope:auto-widen git events", async () => {

View File

@@ -33,6 +33,12 @@ export {
SquashAuditError,
type MergerOptions,
type AutostashOrphanRecord,
stashUnrelatedRootDirChanges,
dropAutostashHandle,
restoreUnrelatedRootDirChanges,
tryFastForwardFromOrigin,
getConflictedFiles,
type AutostashHandle,
} from "./merger.js";
export {
resolveIntegrationBranch,
@@ -48,6 +54,9 @@ export {
type HandoffResult,
type MergeIntegrationRootResolution,
} from "./merger-integration-worktree.js";
export {
generateSyntheticRunId,
} from "./run-audit.js";
export {
auditSquashMerge,
formatSquashAuditReport,

View File

@@ -1896,7 +1896,7 @@ function resetMergeWithWarn(rootDir: string, taskId: string, label: string): voi
* `rescueShas` lists any race-rescue stashes the autostash captured for
* late-dirty paths (concurrent dev edits during the merger run). They are
* surfaced separately so the caller can log them to the task feed. */
interface AutostashHandle {
export interface AutostashHandle {
sha: string;
label: string;
rescueShas?: { sha: string; label: string }[];
@@ -2461,7 +2461,7 @@ export const __test__ = {
notifyAutostashOrphans,
};
async function stashUnrelatedRootDirChanges(
export async function stashUnrelatedRootDirChanges(
rootDir: string,
taskId: string,
): Promise<AutostashHandle | null> {
@@ -2688,7 +2688,7 @@ async function isAutostashLive(rootDir: string, sha: string): Promise<boolean> {
}
}
async function dropAutostashHandle(
export async function dropAutostashHandle(
rootDir: string,
taskId: string,
handle: AutostashHandle,
@@ -3384,7 +3384,7 @@ async function restoreRescueAutostashes(
return { unresolvedCount };
}
async function restoreUnrelatedRootDirChanges(
export async function restoreUnrelatedRootDirChanges(
rootDir: string,
taskId: string,
handle: AutostashHandle,
@@ -9926,7 +9926,7 @@ export async function aiMergeTask(
* NOTE: This is NOT FN-5350's integration-branch ref advance path. FN-5350
* advances refs/heads/<integration-branch> via compare-and-swap `git update-ref`.
*/
async function tryFastForwardFromOrigin(
export async function tryFastForwardFromOrigin(
rootDir: string,
taskId: string,
integrationBranch: string,

View File

@@ -288,11 +288,14 @@ export type GitMutationType =
* taskId?: string;
* worktreePath: string;
* integrationBranch: string;
* remote?: string;
* fromSha: string;
* toSha: string;
* durationMs: number;
* succeeded: boolean;
* error?: string;
* behind?: number;
* ahead?: number;
* }
* ```
*/
@@ -324,7 +327,8 @@ export type GitMutationType =
* stashSha: string;
* stashLabel: string;
* conflictedFiles: string[];
* advice: string;
* autostashOutcome: "conflict-needs-manual" | "failed";
* advice?: string;
* }
* ```
*/