diff --git a/packages/engine/src/__tests__/triage-planning-prompt-single-source.test.ts b/packages/engine/src/__tests__/triage-planning-prompt-single-source.test.ts index 1be76c1fd1..ee4a8a8920 100644 --- a/packages/engine/src/__tests__/triage-planning-prompt-single-source.test.ts +++ b/packages/engine/src/__tests__/triage-planning-prompt-single-source.test.ts @@ -18,11 +18,20 @@ vi.mock("../reviewer.js", () => ({ reviewStep: mockReviewStep, })); -vi.mock("../pi.js", () => ({ - createFnAgent: mockCreateFnAgent, - describeModel: vi.fn().mockReturnValue("mock-model"), - promptWithFallback: vi.fn().mockResolvedValue(undefined), -})); +vi.mock("../pi.js", () => { + /* + FNXC:EngineTests 2026-07-07-09:10: + triage.ts specifyTask catch checks `err instanceof ModelFallbackExhaustedError` (FN-7559 planner fallback exhaustion handling) and agentWork calls `formatModelMarkerDetails`. The pi mock must expose both so the instanceof guard is callable and model-marker formatting resolves, instead of crashing happy-path specifyTask runs. + */ + class ModelFallbackExhaustedError extends Error {} + return { + ModelFallbackExhaustedError, + createFnAgent: mockCreateFnAgent, + describeModel: vi.fn().mockReturnValue("mock-model"), + formatModelMarkerDetails: vi.fn((model: string) => model), + promptWithFallback: vi.fn().mockResolvedValue(undefined), + }; +}); vi.mock("@fusion/core", async (importOriginal) => { const { createEngineCoreMock } = await import("../test/mockCore.js"); diff --git a/packages/engine/src/__tests__/worktree-acquisition-backend.test.ts b/packages/engine/src/__tests__/worktree-acquisition-backend.test.ts index 8f326e270d..573ac6df15 100644 --- a/packages/engine/src/__tests__/worktree-acquisition-backend.test.ts +++ b/packages/engine/src/__tests__/worktree-acquisition-backend.test.ts @@ -134,14 +134,18 @@ describe("acquireTaskWorktree backend wiring", () => { ).rejects.toMatchObject({ name: "WorktrunkOperationError", code: "worktrunk_binary_missing" }); /* - * FNXC:WorktreeIsolation 2026-07-02-07:40: - * The integration-branch resolution (`git symbolic-ref`) runs before the worktrunk binary check, so one exec call is expected. No worktrunk `switch` command should be attempted when the binary is missing. + * FNXC:WorktreeIsolation 2026-07-02-07:40 (updated 2026-07-07-09:15 for FN-7438): + * The integration-branch resolution runs before the worktrunk binary check. With an empty symbolic-ref result, FN-7438 (aa8f1f32e) adds a `git remote` discovery call before the "main" fallback, so two exec calls happen. No worktrunk `switch` command should be attempted when the binary is missing. */ - expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledTimes(2); expect(execMock).toHaveBeenCalledWith( "git symbolic-ref --short refs/remotes/origin/HEAD", expect.objectContaining({ cwd: "/repo" }), ); + expect(execMock).toHaveBeenCalledWith( + "git remote", + expect.objectContaining({ cwd: "/repo" }), + ); expect(execMock.mock.calls.some((call) => String(call[0]).includes('"switch"'))).toBe(false); }); @@ -188,13 +192,17 @@ describe("acquireTaskWorktree backend wiring", () => { expect(result.branch).toBe("fusion/fn-backend"); expect(create).toHaveBeenCalledTimes(1); /* - * FNXC:WorktreeIsolation 2026-07-02-07:40: - * The integration-branch resolution runs before the explicit backend's create is invoked, so the only exec call is the `git symbolic-ref` lookup. The custom backend's create mock performs no exec. + * FNXC:WorktreeIsolation 2026-07-02-07:40 (updated 2026-07-07-09:15 for FN-7438): + * The integration-branch resolution runs before the explicit backend's create. With an empty symbolic-ref result, FN-7438 (aa8f1f32e) adds a `git remote` discovery call before the "main" fallback, so two exec calls happen: symbolic-ref + git remote. The custom backend's create mock performs no exec. */ - expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledTimes(2); expect(execMock).toHaveBeenCalledWith( "git symbolic-ref --short refs/remotes/origin/HEAD", expect.objectContaining({ cwd: "/repo" }), ); + expect(execMock).toHaveBeenCalledWith( + "git remote", + expect.objectContaining({ cwd: "/repo" }), + ); }); }); diff --git a/packages/engine/src/__tests__/worktree-acquisition-worktrunk.test.ts b/packages/engine/src/__tests__/worktree-acquisition-worktrunk.test.ts index f194aa7692..f174bc7e44 100644 --- a/packages/engine/src/__tests__/worktree-acquisition-worktrunk.test.ts +++ b/packages/engine/src/__tests__/worktree-acquisition-worktrunk.test.ts @@ -66,13 +66,14 @@ describe("acquireTaskWorktree worktrunk wiring", () => { expect(result).toMatchObject({ source: "fresh", branch: "fusion/fn-1" }); /* - * FNXC:WorktreeIsolation 2026-07-02-07:40: - * acquireTaskWorktree now resolves the integration branch via `git symbolic-ref` (returning empty here, so it falls back to "main") and pins the fresh worktree to that start point. Two exec calls happen: the symbolic-ref lookup, then the native `git worktree add -b ... "main"` create. + * FNXC:WorktreeIsolation 2026-07-02-07:40 (updated 2026-07-07-09:15 for FN-7438): + * acquireTaskWorktree resolves the integration branch via `git symbolic-ref`. When that returns empty (as the mock does here), FN-7438 (aa8f1f32e) added a `git remote` discovery call before falling back to "main". So three exec calls happen: symbolic-ref, git remote, then the native `git worktree add -b ... "main"` create. */ - expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenCalledTimes(3); expect(execMock.mock.calls[0]?.[0]).toBe("git symbolic-ref --short refs/remotes/origin/HEAD"); - expect(execMock.mock.calls[1]?.[0]).toContain('git worktree add -b "fusion/fn-1"'); - expect(execMock.mock.calls[1]?.[0]).toContain('"main"'); + expect(execMock.mock.calls[1]?.[0]).toBe("git remote"); + expect(execMock.mock.calls[2]?.[0]).toContain('git worktree add -b "fusion/fn-1"'); + expect(execMock.mock.calls[2]?.[0]).toContain('"main"'); }); it("prefers explicit createWorktree override", async () => { @@ -230,13 +231,17 @@ describe("acquireTaskWorktree worktrunk wiring", () => { expect(result.branch).toBe("fusion/fn-1-custom"); expect(create).toHaveBeenCalledTimes(1); /* - * FNXC:WorktreeIsolation 2026-07-02-07:40: - * The integration-branch resolution runs before the custom backend's create, so the only exec call is the `git symbolic-ref` lookup. The backend's create mock performs no exec. + * FNXC:WorktreeIsolation 2026-07-02-07:40 (updated 2026-07-07-09:15 for FN-7438): + * The integration-branch resolution runs before the custom backend's create. With an empty symbolic-ref result, FN-7438 (aa8f1f32e) adds a `git remote` discovery call before the "main" fallback, so two exec calls happen: symbolic-ref + git remote. The backend's create mock performs no exec. */ - expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledTimes(2); expect(execMock).toHaveBeenCalledWith( "git symbolic-ref --short refs/remotes/origin/HEAD", expect.objectContaining({ cwd: "/repo" }), ); + expect(execMock).toHaveBeenCalledWith( + "git remote", + expect.objectContaining({ cwd: "/repo" }), + ); }); }); diff --git a/packages/engine/src/__tests__/worktree-backend.test.ts b/packages/engine/src/__tests__/worktree-backend.test.ts index b1cc8406a3..894e080bca 100644 --- a/packages/engine/src/__tests__/worktree-backend.test.ts +++ b/packages/engine/src/__tests__/worktree-backend.test.ts @@ -821,11 +821,14 @@ describe("WorktrunkWorktreeBackend", () => { }); it("maps rebase conflicts to worktrunk_sync_conflict", async () => { + // FN-7438 (aa8f1f32e): resolveIntegrationBranch now does symbolic-ref + `git remote` + // before fetch+rebase when no trunk is given, which would consume this mock queue. + // Pass an explicit trunk to isolate the rebase-conflict mapping path under test. execMock.mockResolvedValueOnce({ stdout: "", stderr: "" }).mockRejectedValueOnce({ stderr: "CONFLICT" }); const backend = new WorktrunkWorktreeBackend({ binaryPath: "worktrunk" }); await expect( - backend.sync({ rootDir: "/repo", worktreePath: "/repo/.worktrees/fn-1", branch: "main" }), + backend.sync({ rootDir: "/repo", worktreePath: "/repo/.worktrees/fn-1", branch: "main", trunk: "main" }), ).rejects.toMatchObject({ code: "worktrunk_sync_conflict", operation: "sync" }); });