test(FN-4383): complete Step 2 — add base commit capture unit tests

Fusion-Task-Id: FN-4383
Fusion-Task-Lineage: 69516bdf-34d1-405c-8bc9-b558c4780863
This commit is contained in:
Fusion
2026-05-13 14:47:00 -07:00
committed by gsxdsm
parent 731aa7b14a
commit f9f8ee6f62
2 changed files with 100 additions and 1 deletions

View File

@@ -0,0 +1,96 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import { executorLog } from "../logger.js";
import type { Task } from "@fusion/core";
import { createMockStore, mockedExec, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
function makeTask(overrides: Partial<Task> = {}): Task {
return {
id: "FN-4383",
title: "Test",
description: "Test",
column: "in-progress",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
} as Task;
}
describe("captureBaseCommitSha", () => {
beforeEach(() => {
resetExecutorMocks();
});
it("captures merge-base for fresh worktree", async () => {
mockedExec.mockImplementation((cmd: any, _opts: any, cb: any) => cb(null, cmd.includes("merge-base") ? "abc1234\n" : ""));
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const audit = { git: vi.fn().mockResolvedValue(undefined) };
await (executor as any).captureBaseCommitSha(makeTask(), "/tmp/test/.worktrees/fn-4383", audit);
expect(store.updateTask).toHaveBeenCalledWith("FN-4383", { baseCommitSha: "abc1234" });
expect(audit.git).toHaveBeenCalledWith(expect.objectContaining({ metadata: { purpose: "base", preserved: false } }));
});
it("preserves existing valid baseCommitSha across sessions", async () => {
mockedExecSync.mockReturnValue("");
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const audit = { git: vi.fn().mockResolvedValue(undefined) };
await (executor as any).captureBaseCommitSha(makeTask({ baseCommitSha: "old123" }), "/tmp/test/.worktrees/fn-4383", audit);
expect(store.updateTask).not.toHaveBeenCalled();
expect(audit.git).toHaveBeenCalledWith(expect.objectContaining({ metadata: { purpose: "base", preserved: true } }));
});
it("recaptures when existing baseCommitSha is not ancestor", async () => {
mockedExecSync.mockImplementation(() => {
throw new Error("not ancestor");
});
mockedExec.mockImplementation((cmd: any, _opts: any, cb: any) => cb(null, cmd.includes("merge-base") ? "new456\n" : ""));
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const audit = { git: vi.fn().mockResolvedValue(undefined) };
await (executor as any).captureBaseCommitSha(makeTask({ baseCommitSha: "stale999" }), "/tmp/test/.worktrees/fn-4383", audit);
expect(store.updateTask).toHaveBeenCalledWith("FN-4383", { baseCommitSha: "new456" });
});
it("preserves prior merge base for FN-4309/FN-4383 multi-session regression", async () => {
mockedExecSync.mockReturnValue("");
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const audit = { git: vi.fn().mockResolvedValue(undefined) };
await (executor as any).captureBaseCommitSha(makeTask({ baseCommitSha: "merge_base_sha" }), "/tmp/test/.worktrees/fn-4383", audit);
expect(store.updateTask).not.toHaveBeenCalled();
expect(audit.git).toHaveBeenCalledWith(expect.objectContaining({ metadata: { purpose: "base", preserved: true } }));
});
it("falls back to HEAD when merge-base fails", async () => {
mockedExec.mockImplementation((cmd: any, _opts: any, cb: any) => {
if (String(cmd).includes("merge-base")) {
cb(new Error("merge-base failed"), "", "merge-base failed");
return;
}
cb(null, "head777\n", "");
});
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const audit = { git: vi.fn().mockResolvedValue(undefined) };
await (executor as any).captureBaseCommitSha(makeTask(), "/tmp/test/.worktrees/fn-4383", audit);
expect(store.updateTask).toHaveBeenCalledWith("FN-4383", { baseCommitSha: "head777" });
expect(vi.mocked(executorLog.warn)).toHaveBeenCalledWith(expect.stringContaining("falling back to HEAD"));
});
});

View File

@@ -234,7 +234,7 @@ import { generateWorktreeName } from "../worktree-names.js";
import { findWorktreeUser } from "../merger.js";
import { StepSessionExecutor } from "../step-session-executor.js";
import { withRateLimitRetry } from "../rate-limit-retry.js";
import { execSync } from "node:child_process";
import { exec, execSync } from "node:child_process";
import { existsSync, realpathSync } from "node:fs";
import { hydrateWorktreeDb } from "../worktree-db-hydrate.js";
import { isUsableTaskWorktree } from "../worktree-pool.js";
@@ -245,6 +245,7 @@ export const mockedGenerateWorktreeName = vi.mocked(generateWorktreeName);
export const mockedFindWorktreeUser = vi.mocked(findWorktreeUser);
export const mockedStepSessionExecutor = vi.mocked(StepSessionExecutor);
export const mockedWithRateLimitRetry = vi.mocked(withRateLimitRetry);
export const mockedExec = vi.mocked(exec);
export const mockedExecSync = vi.mocked(execSync);
export const mockedExistsSync = vi.mocked(existsSync);
export const mockedRealpathSync = vi.mocked(realpathSync);
@@ -320,6 +321,8 @@ export function createMockStore() {
export function resetExecutorMocks() {
vi.clearAllMocks();
mockedExec.mockReset();
mockedExecSync.mockReset();
mockedIsUsableTaskWorktree.mockResolvedValue(true);
mockExecuteAll.mockResolvedValue([]);
mockTerminateAllSessions.mockResolvedValue(undefined);