fix(FN-4811): unblock @fusion/engine typecheck so verification bootstrap can run
Symptom found while investigating 'tasks are still struggling': every
in-review task hitting pre-merge deterministic verification failed with
[verification:bootstrap] bootstrap preamble failed (exit 2):
[test-bootstrap] FAILED: workspace dist artifact rebuild did not complete.
[test-bootstrap] command: pnpm --filter @fusion/engine build
Because pnpm --filter @fusion/engine build hit 17 TS errors from a prior
autonomous-agent refactor introducing a RemovalReason enum-like object
and two new audit event types. The bootstrap preamble is run by the
merger before every direct-merge verification, so a broken engine
typecheck blocked EVERY task from merging.
Fixes:
1. Duplicate RemovalReason re-export in worktree-pool.ts
Both and
were present for the same identifier,
producing TS2300 'Duplicate identifier'. RemovalReason is a const
object with derived type (typeof-keyof pattern), so a single value
export covers both kinds; the type-only re-export was redundant.
2. GitMutationType union missing the FN-4811 audit event types
merger.ts and worktree-backend.ts were emitting
'worktree:removal-refused-active-session' and
'worktree:removal-forced-over-active-session' audit events, but the
union in run-audit.ts didn't include them. Added both.
3. self-healing.test.ts vi.mock had wrong RemovalReason keys
The mock only exposed 5 keys (SelfHealing*) but production code
references HardCancel, Executor*, Merger*, PoolPrune, etc. Calls
like removeWorktree({ reason: RemovalReason.MergerPostMerge }) were
getting reason=undefined, producing confusing 'cannot remove
worktree: [vitest] No RemovalReason export is defined on mock'
error messages. Updated the mock to mirror the production const
exactly.
4. worktree-backend.test.ts removeWorktree calls missing required reason
The new contract makes reason: RemovalReason a required field on
removeWorktree's input. Five existing test cases were missing it;
added reason: RemovalReason.MergerCleanup to each.
5. integrity-warning-persisted-dedup.test.ts Settings cast
The test's makeStore helper cast a partial settings object to
Settings; TS rejected the narrowed type. Cast through unknown.
Verification:
- pnpm --filter @fusion/engine build: clean
- pnpm lint: clean
- pnpm build (full workspace): clean
- pnpm --filter @fusion/engine test: 5045 pass, 1 pre-existing
aiMergeTask real-git timeout flake, 1 skipped
With this fix, the verification bootstrap can complete and the merger
can finalize tasks again.
Fusion-Task-Id: FN-4811
This commit is contained in:
13
.changeset/FN-4811-engine-build-unblock.md
Normal file
13
.changeset/FN-4811-engine-build-unblock.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(FN-4811): unblock `@fusion/engine` typecheck so verification bootstrap can run
|
||||||
|
|
||||||
|
Restores `pnpm --filter @fusion/engine build` after a stack of TypeScript regressions blocked every merge. Symptoms: every task hitting pre-merge verification failed with "Verification bootstrap preamble failed — workspace dist artifact rebuild did not complete" because the bootstrap shells `pnpm --filter @fusion/engine build` and that compile was erroring on 17 type issues.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Remove duplicate `RemovalReason` re-export in `worktree-pool.ts` (`export type` + `export` for the same identifier produced TS2300 "Duplicate identifier").
|
||||||
|
- Add `worktree:removal-refused-active-session` and `worktree:removal-forced-over-active-session` to the `GitMutationType` union in `run-audit.ts` so the new FN-4811 audit events are accepted.
|
||||||
|
- Update `self-healing.test.ts` `vi.mock("../worktree-pool.js")` to mirror the production `RemovalReason` const exactly (was missing keys, causing `reason: undefined` to flow into mock calls and confusing error messages).
|
||||||
|
- Add `reason: RemovalReason.MergerCleanup` to existing `worktree-backend.test.ts` `removeWorktree` calls now that `reason` is a required parameter.
|
||||||
@@ -38,7 +38,7 @@ function makeStore(task: Task, events: unknown[] = []): TaskStore & EventEmitter
|
|||||||
commitAuthorEnabled: false,
|
commitAuthorEnabled: false,
|
||||||
useAiMergeCommitSummary: false,
|
useAiMergeCommitSummary: false,
|
||||||
baseBranch: "main",
|
baseBranch: "main",
|
||||||
} as Settings;
|
} as unknown as Settings;
|
||||||
return Object.assign(emitter, {
|
return Object.assign(emitter, {
|
||||||
getSettings: vi.fn(async () => mergedSettings),
|
getSettings: vi.fn(async () => mergedSettings),
|
||||||
getTask: vi.fn(async () => task),
|
getTask: vi.fn(async () => task),
|
||||||
|
|||||||
@@ -51,6 +51,25 @@ vi.mock("node:fs", async (importOriginal) => {
|
|||||||
|
|
||||||
vi.mock("../worktree-pool.js", () => ({
|
vi.mock("../worktree-pool.js", () => ({
|
||||||
WorktreePool: vi.fn(),
|
WorktreePool: vi.fn(),
|
||||||
|
// FN-4811: Must mirror the production `RemovalReason` const in worktree-backend.ts
|
||||||
|
// exactly — every key referenced as `RemovalReason.X` in production code (self-healing,
|
||||||
|
// executor, merger) needs to resolve here, otherwise removeWorktree({ reason: undefined })
|
||||||
|
// gets passed through and the gate logic fails with confusing 'reason is undefined' errors.
|
||||||
|
RemovalReason: {
|
||||||
|
HardCancel: "hard-cancel",
|
||||||
|
ExecutorTransientRetry: "executor-transient-retry",
|
||||||
|
ExecutorStuckKilled: "executor-stuck-killed",
|
||||||
|
ExecutorDispose: "executor-dispose",
|
||||||
|
StepSessionCleanup: "step-session-cleanup",
|
||||||
|
MergerPostMerge: "merger-post-merge",
|
||||||
|
MergerCleanup: "merger-cleanup",
|
||||||
|
SelfHealingReclaim: "self-healing-reclaim",
|
||||||
|
SelfHealingStaleActiveBranch: "self-healing-stale-active-branch",
|
||||||
|
SelfHealingBranchConflict: "self-healing-branch-conflict",
|
||||||
|
SelfHealingOrphanRescue: "self-healing-orphan-rescue",
|
||||||
|
SelfHealingIdleSweep: "self-healing-idle-sweep",
|
||||||
|
PoolPrune: "pool-prune",
|
||||||
|
},
|
||||||
scanIdleWorktrees: vi.fn().mockResolvedValue([]),
|
scanIdleWorktrees: vi.fn().mockResolvedValue([]),
|
||||||
cleanupOrphanedWorktrees: vi.fn().mockResolvedValue(0),
|
cleanupOrphanedWorktrees: vi.fn().mockResolvedValue(0),
|
||||||
scanOrphanedBranches: vi.fn().mockResolvedValue([]),
|
scanOrphanedBranches: vi.fn().mockResolvedValue([]),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
WorktrunkWorktreeBackend,
|
WorktrunkWorktreeBackend,
|
||||||
removeWorktree,
|
removeWorktree,
|
||||||
resolveWorktreeBackend,
|
resolveWorktreeBackend,
|
||||||
|
RemovalReason,
|
||||||
} from "../worktree-backend.js";
|
} from "../worktree-backend.js";
|
||||||
|
|
||||||
const { execMock, accessMock, existsSyncMock } = vi.hoisted(() => {
|
const { execMock, accessMock, existsSyncMock } = vi.hoisted(() => {
|
||||||
@@ -527,6 +528,7 @@ describe("removeWorktree", () => {
|
|||||||
worktreePath: "/repo/.worktrees/fn-1",
|
worktreePath: "/repo/.worktrees/fn-1",
|
||||||
settings: {},
|
settings: {},
|
||||||
audit,
|
audit,
|
||||||
|
reason: RemovalReason.SelfHealingReclaim,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(execMock).toHaveBeenCalledWith(
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
@@ -546,6 +548,7 @@ describe("removeWorktree", () => {
|
|||||||
settings: { worktrunk: { enabled: true, binaryPath: "worktrunk", onFailure: "fail" } as any },
|
settings: { worktrunk: { enabled: true, binaryPath: "worktrunk", onFailure: "fail" } as any },
|
||||||
audit,
|
audit,
|
||||||
taskId: "FN-1",
|
taskId: "FN-1",
|
||||||
|
reason: RemovalReason.SelfHealingReclaim,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(audit.git).toHaveBeenCalledWith({ type: "worktree:worktrunk-remove", target: "/repo/.worktrees/fn-1" });
|
expect(audit.git).toHaveBeenCalledWith({ type: "worktree:worktrunk-remove", target: "/repo/.worktrees/fn-1" });
|
||||||
@@ -562,6 +565,7 @@ describe("removeWorktree", () => {
|
|||||||
worktreePath: "/repo/.worktrees/fn-1",
|
worktreePath: "/repo/.worktrees/fn-1",
|
||||||
settings: { worktrunk: { enabled: true, binaryPath: "worktrunk", onFailure: "fallback-native" } as any },
|
settings: { worktrunk: { enabled: true, binaryPath: "worktrunk", onFailure: "fallback-native" } as any },
|
||||||
audit,
|
audit,
|
||||||
|
reason: RemovalReason.SelfHealingReclaim,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(audit.git).toHaveBeenCalledWith(
|
expect(audit.git).toHaveBeenCalledWith(
|
||||||
@@ -580,6 +584,7 @@ describe("removeWorktree", () => {
|
|||||||
rootDir: "/repo",
|
rootDir: "/repo",
|
||||||
worktreePath: "/repo/.worktrees/fn-1",
|
worktreePath: "/repo/.worktrees/fn-1",
|
||||||
settings: { worktrunk: { enabled: true, binaryPath: "worktrunk", onFailure: "fail" } as any },
|
settings: { worktrunk: { enabled: true, binaryPath: "worktrunk", onFailure: "fail" } as any },
|
||||||
|
reason: RemovalReason.SelfHealingReclaim,
|
||||||
}),
|
}),
|
||||||
).rejects.toMatchObject({ code: "worktrunk_operation_failed", operation: "remove" });
|
).rejects.toMatchObject({ code: "worktrunk_operation_failed", operation: "remove" });
|
||||||
});
|
});
|
||||||
@@ -590,6 +595,7 @@ describe("removeWorktree", () => {
|
|||||||
rootDir: "/repo",
|
rootDir: "/repo",
|
||||||
worktreePath: "/repo/.worktrees/fn-1",
|
worktreePath: "/repo/.worktrees/fn-1",
|
||||||
settings: { worktrunk: { enabled: true, onFailure: "fail" } as any },
|
settings: { worktrunk: { enabled: true, onFailure: "fail" } as any },
|
||||||
|
reason: RemovalReason.SelfHealingReclaim,
|
||||||
}),
|
}),
|
||||||
).rejects.toMatchObject({ code: "worktrunk_binary_missing", operation: "remove" });
|
).rejects.toMatchObject({ code: "worktrunk_binary_missing", operation: "remove" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export type GitMutationType =
|
|||||||
| "worktree:worktrunk-fallback"
|
| "worktree:worktrunk-fallback"
|
||||||
| "worktree:worktrunk-failure"
|
| "worktree:worktrunk-failure"
|
||||||
| "worktree:worktrunk-fallback-native"
|
| "worktree:worktrunk-fallback-native"
|
||||||
|
| "worktree:removal-refused-active-session"
|
||||||
|
| "worktree:removal-forced-over-active-session"
|
||||||
| "branch:create"
|
| "branch:create"
|
||||||
| "branch:delete"
|
| "branch:delete"
|
||||||
| "branch:checkout"
|
| "branch:checkout"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export {
|
|||||||
removeWorktree,
|
removeWorktree,
|
||||||
resolveWorktreeBackend,
|
resolveWorktreeBackend,
|
||||||
} from "./worktree-backend.js";
|
} from "./worktree-backend.js";
|
||||||
export type { WorktreeBackend, WorktreeBackendKind, RemovalReason } from "./worktree-backend.js";
|
export type { WorktreeBackend, WorktreeBackendKind } from "./worktree-backend.js";
|
||||||
export { RemovalReason } from "./worktree-backend.js";
|
export { RemovalReason } from "./worktree-backend.js";
|
||||||
|
|
||||||
// Re-export worktrunk installer types for convenience.
|
// Re-export worktrunk installer types for convenience.
|
||||||
|
|||||||
Reference in New Issue
Block a user