fix(FN-2462): enforce frozen-lockfile bootstrap across workflows

- Update CI, version, and test-release workflows to use pnpm install --frozen-lockfile consistently.
- Align contributor and settings documentation plus worktree init examples toward deterministic frozen-lockfile bootstrap.
- Clarify TaskExecutor worktree init guidance to distinguish dependency bootstrap failures from missing workspace dist export failures.
- Refresh workflow, executor, restart, and SettingsModal tests (including stable Node Sync tab selection) to assert the new bootstrap contract.
This commit is contained in:
Fusion
2026-04-24 11:41:10 -07:00
committed by gsxdsm
parent bafcbd6c39
commit 05bd69da68
12 changed files with 49 additions and 32 deletions

View File

@@ -432,7 +432,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
worktreeInitCommand: "pnpm install",
worktreeInitCommand: "pnpm install --frozen-lockfile",
});
const executor = new TaskExecutor(store, "/tmp/test");
@@ -440,7 +440,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
// execSync is called for worktree creation + init command
const initCall = mockedExecSync.mock.calls.find(
(call) => call[0] === "pnpm install",
(call) => call[0] === "pnpm install --frozen-lockfile",
);
expect(initCall).toBeDefined();
expect(initCall![1]).toMatchObject({
@@ -452,7 +452,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
expect(store.logEntry).toHaveBeenCalledWith(
"FN-010",
expect.stringMatching(/^\[timing\] Worktree init command completed in \d+ms$/),
"pnpm install",
"pnpm install --frozen-lockfile",
expect.objectContaining({ agentId: "executor" }),
);
});
@@ -464,7 +464,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// Only worktree creation calls to execSync, no "pnpm install" etc.
// Only worktree creation calls to execSync, no "pnpm install --frozen-lockfile" etc.
const initCall = mockedExecSync.mock.calls.find(
(call) => typeof call[0] === "string" && !call[0].startsWith("git"),
);
@@ -524,7 +524,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
worktreeInitCommand: "pnpm install",
worktreeInitCommand: "pnpm install --frozen-lockfile",
});
// Worktree already exists (resume)
@@ -2019,15 +2019,15 @@ describe("TaskExecutor worktree pool integration", () => {
groupOverlappingFiles: false,
autoMerge: false,
recycleWorktrees: true,
worktreeInitCommand: "pnpm install",
worktreeInitCommand: "pnpm install --frozen-lockfile",
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask());
// "pnpm install" should NOT have been called (pooled worktree has warm cache)
// "pnpm install --frozen-lockfile" should NOT have been called (pooled worktree has warm cache)
const initCalls = mockedExecSync.mock.calls.filter(
(c) => c[0] === "pnpm install",
(c) => c[0] === "pnpm install --frozen-lockfile",
);
expect(initCalls).toHaveLength(0);
});

View File

@@ -1320,11 +1320,16 @@ export class TaskExecutor {
}
// Run worktree init command for fresh worktrees (skip for pooled — caches are warm).
// The init command must leave the worktree in a state where workspace
// packages resolve at runtime (e.g. `pnpm install && pnpm build`). If
// it doesn't, the first command the executor runs will usually fail
// with "@fusion/core entry not found" because monorepo exports point
// to dist/. 5-minute timeout accommodates install + build together.
// The init command should deterministically install the full dependency
// graph required by test/typecheck/build commands (for pnpm workspaces,
// prefer `pnpm install --frozen-lockfile`) so transitive modules and
// declarations like @vitest/runner, loupe, debug, @types/express, and
// node-pty are present after bootstrap.
//
// NOTE: This is distinct from the separate workspace-export failure
// class where internal packages fail to resolve because exports point
// to missing dist/* outputs (e.g. "@fusion/core entry not found").
// 5-minute timeout accommodates larger dependency installs.
if (settings.worktreeInitCommand) {
const initStartedAt = Date.now();
try {

View File

@@ -366,7 +366,7 @@ describe("In-progress task resume after restart", () => {
const store = createMockStore();
store.getSettings.mockResolvedValue({
...DEFAULT_SETTINGS,
worktreeInitCommand: "pnpm install",
worktreeInitCommand: "pnpm install --frozen-lockfile",
});
const task = makeTask("FN-030", "in-progress");
store.listTasks.mockResolvedValue([task]);
@@ -384,7 +384,7 @@ describe("In-progress task resume after restart", () => {
// No init command calls
const initCalls = mockedExecSync.mock.calls.filter(
(call) => call[0] === "pnpm install",
(call) => call[0] === "pnpm install --frozen-lockfile",
);
expect(initCalls).toHaveLength(0);
});