fix: green full-suite after main product drift (#2275)

## Summary
Follow-up after #2266: full suite on latest `main` still failed on i18n
parity and engine product-drift tests (duplicate flagging, mission
validator signature, worktree path reservation, pi ModelRuntime,
assigned-agent PG layer).

## Changes
- **i18n**: add `listView/tasks.statusReplan` and mobile-nav settings
keys to es/fr/ko/zh-CN/zh-TW
- **triage**: opt-in `triageDuplicateResolution: "delete"` coverage +
default prompt/flag path; stub `recordActivity`
- **MCP PR response**: stub `getTask` for merger model resolution
- **pi-layers**: mock `ModelRuntime.create` (FN-8142)
- **assigned-agent**: stub `getAsyncLayer` for authoritative AgentStore
fallback
- **mission validation**: `startValidatorRun(..., taskId)` +
`runValidation` `{ result, inspection }` shape
- **worktree acquisition**: real temp roots for path reservation;
probe-aware worktrunk failure fixture
- **useBlockerFanout**: read `MAX_AUTO_MERGE_RETRIES` from
`self-healing-constants.ts` (wave-8 peel)

## Test plan
- [x] Targeted engine suites above (48 tests)
- [x] i18n parity + gate coverage
- [x] useBlockerFanout
- [x] `pnpm test:gate`
- [ ] Full Suite (non-blocking) on PR

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Localization**
* Added “Replan”/replanning status labels across Spanish, French,
Korean, Simplified Chinese, and Traditional Chinese.
* Completed settings mobile navigation translations, including primary
items controls and reordering options.

* **Tests**
* Expanded coverage for validation recovery and task forwarding
behavior.
* Improved duplicate-resolution scenarios (including activity recording
and lineage deletion behavior).
* Increased test reliability for worktree acquisition by using temporary
filesystem roots.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-17 17:27:04 -07:00
committed by GitHub
parent f5538e6253
commit 3fcab9fc1d
14 changed files with 281 additions and 106 deletions

View File

@@ -130,9 +130,16 @@ describe("computeBlockerFanoutMap", () => {
it("keeps the dashboard fallback aligned with the documented self-healing default seed", () => {
const testDir = dirname(fileURLToPath(import.meta.url));
const source = readFileSync(resolve(testDir, "../../../../engine/src/self-healing.ts"), "utf8");
const match = source.match(/export const MAX_AUTO_MERGE_RETRIES = (\d+);/);
/*
FNXC:DashboardTests 2026-07-17-11:45:
Wave-8 peeled MAX_AUTO_MERGE_RETRIES into self-healing-constants.ts (re-exported
from self-healing.ts). Read the constant definition file so this alignment
guard still pins the dashboard seed to the engine default of 3.
*/
const constantsSource = readFileSync(resolve(testDir, "../../../../engine/src/self-healing-constants.ts"), "utf8");
const match = constantsSource.match(/export const MAX_AUTO_MERGE_RETRIES = (\d+);/);
expect(match?.[1]).toBe(String(MAX_AUTO_MERGE_RETRIES));
const source = readFileSync(resolve(testDir, "../../../../engine/src/self-healing.ts"), "utf8");
expect(source).toContain("SelfHealingManager must call resolveMaxAutoMergeRetries(settings)");
});
});

View File

@@ -10,6 +10,13 @@ function createStore() {
return {
on: vi.fn(),
getFusionDir: vi.fn(() => "/worktree/.fusion"),
/*
FNXC:EngineTests 2026-07-17-11:45:
getAuthoritativeAssignedAgent requires store.getAsyncLayer() so the fallback
AgentStore runs in PostgreSQL backend mode. A missing method throws, is
swallowed, and the lookup returns null — defeating the runtimeConfig spy.
*/
getAsyncLayer: vi.fn(() => ({ kind: "test-async-layer" })),
} as any;
}

View File

@@ -38,6 +38,14 @@ const settings = {
function createStore(enabled: boolean): TaskStore {
return {
/*
FNXC:EngineTests 2026-07-17-11:45:
pr-response-run-ops now loads the task via store.getTask so merger model resolution
can honor per-task overrides. Stub a minimal task for the MCP-forwarding path.
*/
async getTask(taskId: string) {
return { id: taskId, column: "in-review" } as any;
},
async getSettingsByScope() {
return {
global: { mcpServers: { enabled: true, servers: [] } },

View File

@@ -117,6 +117,14 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
},
discoverAndLoadExtensions: discoverAndLoadExtensionsMock,
getAgentDir: () => "/mock-agent-dir",
/*
FNXC:EngineTests 2026-07-17-11:45:
createFusionModelRegistry awaits ModelRuntime.create before ModelRegistry (FN-8142 / pi 0.80.8+).
Without this export the layers wiring suite fails every createFnAgent path.
*/
ModelRuntime: {
create: async () => ({ getAuth: async () => ({ auth: { headers: {} as Record<string, string> } }) }),
},
ModelRegistry: class {
static create(..._args: unknown[]) {
return new (this as unknown as new () => unknown)();

View File

@@ -250,7 +250,11 @@ describe("FN-5715 reliability: mission validation trigger gap", () => {
taskStore: taskStore as any,
rootDir: process.cwd(),
});
vi.spyOn(loop as any, "runValidation").mockResolvedValue({ status: "pass", summary: "ok" });
// FNXC:EngineTests 2026-07-17-11:50: runFeatureValidation destructures { result, inspection }.
vi.spyOn(loop as any, "runValidation").mockResolvedValue({
result: { status: "pass", summary: "ok" },
inspection: { rootDir: process.cwd() },
});
loop.start();
const periodicMaintenancePass = async () => loop.recoverActiveMissions();
@@ -258,7 +262,8 @@ describe("FN-5715 reliability: mission validation trigger gap", () => {
await periodicMaintenancePass();
expect(missionStore.ensureFeatureAssertionLinked).toHaveBeenCalledWith("F-001");
expect(missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion");
// FNXC:EngineTests 2026-07-17-11:45: startValidatorRun now threads the completing task id.
expect(missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion", "FN-001");
const noAssertionEvents = missionStore.logMissionEvent.mock.calls.filter(
([, type, , payload]) => type === "warning" && payload?.code === "validation_auto_passed_no_assertions",
);
@@ -316,13 +321,18 @@ describe("FN-5715 reliability: mission validation trigger gap", () => {
};
const loop = new MissionExecutionLoop({ missionStore: missionStore as any, taskStore: taskStore as any, rootDir: process.cwd() });
vi.spyOn(loop as any, "runValidation").mockResolvedValue({ status: "pass", summary: "ok" });
// FNXC:EngineTests 2026-07-17-11:50: runFeatureValidation destructures { result, inspection }.
vi.spyOn(loop as any, "runValidation").mockResolvedValue({
result: { status: "pass", summary: "ok" },
inspection: { rootDir: process.cwd() },
});
loop.start();
await loop.recoverActiveMissions();
expect(missionStore.ensureFeatureAssertionLinked).toHaveBeenCalledWith("F-001");
expect(missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion");
// FNXC:EngineTests 2026-07-17-11:45: startValidatorRun now threads the completing task id.
expect(missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion", "FN-001");
const noAssertionEvents = missionStore.logMissionEvent.mock.calls.filter(
([, type, , payload]) => type === "warning" && payload?.code === "validation_auto_passed_no_assertions",
);

View File

@@ -16,6 +16,8 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
updateTask: vi.fn(),
moveTask: vi.fn(),
logEntry: vi.fn(),
// FNXC:EngineTests 2026-07-17-11:45: flagTriageDuplicate records task:auto-archived-duplicate activity.
recordActivity: vi.fn().mockResolvedValue(undefined),
deleteTask: vi.fn(),
on: vi.fn(),
off: vi.fn(),
@@ -102,8 +104,21 @@ describe("triage finalize duplicate lineage", () => {
expect(vi.mocked(store.updateTask).mock.calls[0]?.[1]).not.toHaveProperty("sourceMetadataPatch");
});
it("preserves duplicate stub delete path", async () => {
const store = createMockStore();
it("preserves opt-in duplicate stub delete path", async () => {
/*
FNXC:EngineTests 2026-07-17-11:50:
Issue #2225 default is prompt (flag + pause). Deletion remains opt-in via
settings.triageDuplicateResolution === "delete" and still requires a live
canonical task for the marker short-circuit.
*/
const store = createMockStore({
getSettings: vi.fn().mockResolvedValue({ requirePlanApproval: false, triageDuplicateResolution: "delete" } as Settings),
getTask: vi.fn().mockImplementation(async (id: string) => (
id === "FN-4894"
? createTask({ id: "FN-4894", title: "Canonical", column: "todo", status: null })
: undefined
)),
});
await runRecovery(createTask(), "DUPLICATE: FN-4894\n", store);
expect(store.deleteTask).toHaveBeenCalledWith("FN-001", expect.objectContaining({
@@ -113,6 +128,32 @@ describe("triage finalize duplicate lineage", () => {
runId: expect.stringMatching(/^triage-delete-FN-001-/),
}),
}));
expect(store.updateTask).not.toHaveBeenCalled();
});
it("flags and parks DUPLICATE markers under default prompt resolution", async () => {
const store = createMockStore({
getTask: vi.fn().mockImplementation(async (id: string) => (
id === "FN-4894"
? createTask({ id: "FN-4894", title: "Canonical", column: "todo", status: null })
: undefined
)),
});
await runRecovery(createTask(), "DUPLICATE: FN-4894\n", store);
expect(store.recordActivity).toHaveBeenCalledWith(expect.objectContaining({
type: "task:auto-archived-duplicate",
taskId: "FN-001",
}));
expect(store.updateTask).toHaveBeenCalledWith(
"FN-001",
expect.objectContaining({
sourceMetadataPatch: expect.objectContaining({ nearDuplicateOf: "FN-4894", duplicateSource: "triage-marker" }),
}),
);
expect(store.updateTask).toHaveBeenCalledWith(
"FN-001",
expect.objectContaining({ paused: true, pausedReason: "duplicate-decision-required" }),
);
expect(store.deleteTask).not.toHaveBeenCalled();
});
});

View File

@@ -66,6 +66,8 @@ function createStore(overrides: Partial<TaskStore> = {}): TaskStore {
planningFallbackModelId: "fallback-model",
} as Settings),
logEntry: vi.fn().mockResolvedValue(undefined),
// FNXC:EngineTests 2026-07-17-11:45: flagTriageDuplicate records task:auto-archived-duplicate activity.
recordActivity: vi.fn().mockResolvedValue(undefined),
appendAgentLog: vi.fn().mockResolvedValue(undefined),
parseDependenciesFromPrompt: vi.fn().mockResolvedValue([]),
parseStepsFromPrompt: vi.fn().mockResolvedValue([]),
@@ -148,9 +150,26 @@ describe("triage split/delete lineage forwarding", () => {
}));
});
it("passes removeLineageReferences on DUPLICATE close", async () => {
it("passes removeLineageReferences on opt-in DUPLICATE delete resolution", async () => {
/*
FNXC:EngineTests 2026-07-17-11:50:
Default triageDuplicateResolution is prompt (flag, not delete). This suite
still covers the delete path when operators opt in.
*/
const store = createStore({
getTask: vi.fn().mockResolvedValue(undefined),
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 10000,
groupOverlappingFiles: false,
autoMerge: true,
triageDuplicateResolution: "delete",
} as Settings),
getTask: vi.fn().mockImplementation(async (id: string) => (
id === "FN-4894"
? createTask({ id: "FN-4894", title: "Canonical", column: "todo", status: null })
: undefined
)),
deleteTask: vi.fn().mockResolvedValue(undefined),
});

View File

@@ -1,4 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../worktree-hooks.js", () => ({
installTaskWorktreeIdentityGuard: vi.fn().mockResolvedValue(undefined),
@@ -16,15 +19,25 @@ vi.mock("../worktree-db-hydrate.js", () => ({
hydrateWorktreeDb: vi.fn().mockResolvedValue({ degraded: false, tasksCopied: 1, documentsCopied: 1, artifactsCopied: 0 }),
}));
const { execMock, existsSyncMock, accessMock } = vi.hoisted(() => {
const { execMock, existsSyncMock } = vi.hoisted(() => {
const mock = vi.fn();
(mock as any)[Symbol.for("nodejs.util.promisify.custom")] = mock;
return { execMock: mock, existsSyncMock: vi.fn(), accessMock: vi.fn().mockResolvedValue(undefined) };
return {
execMock: mock,
existsSyncMock: vi.fn(),
};
});
vi.mock("node:child_process", () => ({ exec: execMock, execFile: vi.fn() }));
vi.mock("node:fs", () => ({ existsSync: existsSyncMock }));
vi.mock("node:fs/promises", () => ({ access: accessMock }));
/*
FNXC:EngineTests 2026-07-17-11:55:
Path reservation writes lock state under rootDir/.worktrees. Use real fs/promises
against a temp root; only stub existsSync for worktrunk path probes.
*/
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return { ...actual, existsSync: existsSyncMock };
});
describe("acquireTaskWorktree backend wiring", () => {
const task = { id: "FN-1", title: "Task", description: "Desc", branch: null, worktree: null } as any;
@@ -33,19 +46,29 @@ describe("acquireTaskWorktree backend wiring", () => {
pauseTask: vi.fn().mockResolvedValue(undefined),
logEntry: vi.fn().mockResolvedValue(undefined),
} as any;
const roots: string[] = [];
async function makeRootDir(): Promise<string> {
const rootDir = await mkdtemp(join(tmpdir(), "fusion-wt-backend-"));
roots.push(rootDir);
return rootDir;
}
beforeEach(() => {
execMock.mockReset();
existsSyncMock.mockReset();
existsSyncMock.mockReturnValue(true);
accessMock.mockReset();
accessMock.mockResolvedValue(undefined);
store.updateTask.mockClear();
store.logEntry.mockClear();
store.pauseTask.mockClear();
});
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
it("uses native backend by default and emits no worktrunk audit", async () => {
const rootDir = await makeRootDir();
execMock.mockResolvedValue({ stdout: "", stderr: "" });
const audit = {
git: vi.fn().mockResolvedValue(undefined),
@@ -56,25 +79,25 @@ describe("acquireTaskWorktree backend wiring", () => {
const result = await acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store,
settings: { worktreeNaming: "task-id" } as any,
audit,
});
expect(result.branch).toBe("fusion/fn-1");
expect(result.worktreePath).toBe("/repo/.worktrees/fn-1");
expect(result.worktreePath).toBe(`${rootDir}/.worktrees/fn-1`);
/*
* FNXC:WorktreeIsolation 2026-07-02-07:40:
* acquireTaskWorktree now resolves the integration branch via `git symbolic-ref` and pins fresh worktree creation to that start point so new task branches never inherit the root checkout's ambient HEAD. With an empty mock stdout the resolver falls back to "main", so the native create command appends "main" as the start point and there are two exec calls (symbolic-ref + worktree add).
*/
expect(execMock).toHaveBeenCalledWith(
"git symbolic-ref --short refs/remotes/origin/HEAD",
expect.objectContaining({ cwd: "/repo" }),
expect.objectContaining({ cwd: rootDir }),
);
expect(execMock).toHaveBeenCalledWith(
'git worktree add -b "fusion/fn-1" "/repo/.worktrees/fn-1" "main"',
expect.objectContaining({ cwd: "/repo" }),
`git worktree add -b "fusion/fn-1" "${rootDir}/.worktrees/fn-1" "main"`,
expect.objectContaining({ cwd: rootDir }),
);
expect(audit.git).not.toHaveBeenCalledWith(
expect.objectContaining({ type: "worktree:worktrunk-create" }),
@@ -82,12 +105,13 @@ describe("acquireTaskWorktree backend wiring", () => {
});
it("routes through worktrunk backend when enabled and emits audit once", async () => {
const rootDir = await makeRootDir();
execMock.mockImplementation((command: string) => {
if (command.includes('"config" "show"')) return Promise.resolve({ stdout: "", stderr: "" });
if (command.includes('"switch" "--create"')) return Promise.resolve({ stdout: "", stderr: "" });
if (command === "git worktree list --porcelain") {
return Promise.resolve({
stdout: "worktree /repo/.worktrees/fusion/fn-1\nbranch refs/heads/fusion/fn-1\n",
stdout: `worktree ${rootDir}/.worktrees/fusion/fn-1\nbranch refs/heads/fusion/fn-1\n`,
stderr: "",
});
}
@@ -102,7 +126,7 @@ describe("acquireTaskWorktree backend wiring", () => {
await acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store,
settings: { worktreeNaming: "task-id", worktrunk: { enabled: true, binaryPath: "wt" } } as any,
audit,
@@ -124,6 +148,7 @@ describe("acquireTaskWorktree backend wiring", () => {
});
it("passes audit into worktrunk-to-native fallback collision recovery", async () => {
const rootDir = await makeRootDir();
let nativeAddAttempts = 0;
execMock.mockImplementation((command: string) => {
if (command.includes('"wt" "switch" "--create"')) {
@@ -135,14 +160,14 @@ describe("acquireTaskWorktree backend wiring", () => {
? Promise.reject({ message: "branch collision", stderr: "fatal: a branch named 'fusion/fn-1' already exists" })
: Promise.resolve({ stdout: "", stderr: "" });
}
if (command === "git worktree list --porcelain") return Promise.resolve({ stdout: "worktree /repo\nbranch refs/heads/main\n", stderr: "" });
if (command === "git worktree list --porcelain") return Promise.resolve({ stdout: `worktree ${rootDir}\nbranch refs/heads/main\n`, stderr: "" });
if (command.startsWith("git cherry")) return Promise.resolve({ stdout: "", stderr: "" });
return Promise.resolve({ stdout: "deadbeef\n", stderr: "" });
});
const audit = { git: vi.fn().mockResolvedValue(undefined) };
await acquireTaskWorktree({
task: { ...task, executionStartBranch: "release" }, rootDir: "/repo", store,
task: { ...task, executionStartBranch: "release" }, rootDir, store,
settings: { worktreeNaming: "task-id", worktrunk: { enabled: true, binaryPath: "wt", onFailure: "fallback-native" } } as any, audit: audit as any,
});
@@ -154,10 +179,11 @@ describe("acquireTaskWorktree backend wiring", () => {
});
it("throws worktrunk_binary_missing with no binaryPath", async () => {
const rootDir = await makeRootDir();
await expect(
acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store,
settings: { worktreeNaming: "task-id", worktrunk: { enabled: true } } as any,
}),
@@ -170,23 +196,36 @@ describe("acquireTaskWorktree backend wiring", () => {
expect(execMock).toHaveBeenCalledTimes(2);
expect(execMock).toHaveBeenCalledWith(
"git symbolic-ref --short refs/remotes/origin/HEAD",
expect.objectContaining({ cwd: "/repo" }),
expect.objectContaining({ cwd: rootDir }),
);
expect(execMock).toHaveBeenCalledWith(
"git remote",
expect.objectContaining({ cwd: "/repo" }),
expect.objectContaining({ cwd: rootDir }),
);
expect(execMock.mock.calls.some((call) => String(call[0]).includes('"switch"'))).toBe(false);
});
it("throws worktrunk_operation_failed and preserves stderr", async () => {
execMock.mockRejectedValue({ stderr: "worktrunk exploded", status: 17 });
const explicitBinaryPath = "/opt/wt";
const rootDir = await makeRootDir();
/*
FNXC:EngineTests 2026-07-17-11:55:
WorktrunkWorktreeBackend access()-checks the override path before exec. Use a real
temp file so access succeeds; probe via --version must also succeed; only switch
--create should surface operation_failed.
*/
const explicitBinaryPath = join(rootDir, "fake-wt");
await writeFile(explicitBinaryPath, "#!/bin/sh\n");
execMock.mockImplementation((command: string) => {
if (String(command).includes("--version") || String(command).includes("version")) {
return Promise.resolve({ stdout: "wt 0.4.2\n", stderr: "" });
}
return Promise.reject({ stderr: "worktrunk exploded", status: 17 });
});
await expect(
acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store,
settings: { worktreeNaming: "task-id", worktrunk: { enabled: true, binaryPath: explicitBinaryPath } } as any,
}),
@@ -200,6 +239,7 @@ describe("acquireTaskWorktree backend wiring", () => {
});
it("forwards canonical branch and pinned execution start point to an injected backend", async () => {
const rootDir = await makeRootDir();
const create = vi.fn().mockResolvedValue({ path: "/tmp/backend", branch: "fusion/fn-1" });
const backend: WorktreeBackend = {
kind: "native", create, remove: vi.fn(), sync: vi.fn().mockResolvedValue({ skipped: true as const }), prune: vi.fn(),
@@ -207,7 +247,7 @@ describe("acquireTaskWorktree backend wiring", () => {
};
await acquireTaskWorktree({
task: { ...task, executionStartBranch: "release" }, rootDir: "/repo", store,
task: { ...task, executionStartBranch: "release" }, rootDir, store,
settings: { worktreeNaming: "task-id" } as any, backend,
});
@@ -217,6 +257,7 @@ describe("acquireTaskWorktree backend wiring", () => {
});
it("forwards canonical branch and pinned start point on pool fresh fallback", async () => {
const rootDir = await makeRootDir();
const create = vi.fn().mockResolvedValue({ path: "/tmp/fresh", branch: "fusion/fn-1" });
const backend: WorktreeBackend = {
kind: "native", create, remove: vi.fn(), sync: vi.fn().mockResolvedValue({ skipped: true as const }), prune: vi.fn(),
@@ -229,7 +270,7 @@ describe("acquireTaskWorktree backend wiring", () => {
};
await acquireTaskWorktree({
task: { ...task, executionStartBranch: "release" }, rootDir: "/repo", store,
task: { ...task, executionStartBranch: "release" }, rootDir, store,
settings: { worktreeNaming: "task-id", recycleWorktrees: true } as any, backend, pool: pool as any,
});
@@ -239,6 +280,7 @@ describe("acquireTaskWorktree backend wiring", () => {
});
it("uses explicit backend override", async () => {
const rootDir = await makeRootDir();
const create = vi.fn().mockResolvedValue({ path: "/tmp/backend", branch: "fusion/fn-backend" });
const backend: WorktreeBackend = {
kind: "native",
@@ -251,7 +293,7 @@ describe("acquireTaskWorktree backend wiring", () => {
const result = await acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store,
settings: { worktreeNaming: "task-id", worktrunk: { enabled: true } } as any,
backend,
@@ -267,11 +309,11 @@ describe("acquireTaskWorktree backend wiring", () => {
expect(execMock).toHaveBeenCalledTimes(2);
expect(execMock).toHaveBeenCalledWith(
"git symbolic-ref --short refs/remotes/origin/HEAD",
expect.objectContaining({ cwd: "/repo" }),
expect.objectContaining({ cwd: rootDir }),
);
expect(execMock).toHaveBeenCalledWith(
"git remote",
expect.objectContaining({ cwd: "/repo" }),
expect.objectContaining({ cwd: rootDir }),
);
});
});

View File

@@ -1,4 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { execMock, existsSyncMock } = vi.hoisted(() => {
const mock = vi.fn();
@@ -7,7 +10,16 @@ const { execMock, existsSyncMock } = vi.hoisted(() => {
});
vi.mock("node:child_process", () => ({ exec: execMock, execFile: vi.fn() }));
vi.mock("node:fs", () => ({ existsSync: existsSyncMock }));
/*
FNXC:EngineTests 2026-07-17-11:55:
Path reservation now mkdirs lock state under rootDir/.worktrees before create.
Keep real node:fs/promises so reservation works against a temp root; only stub
existsSync for worktrunk path existence checks.
*/
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return { ...actual, existsSync: existsSyncMock };
});
vi.mock("../worktree-hooks.js", () => ({
installTaskWorktreeIdentityGuard: vi.fn().mockResolvedValue(undefined),
IDENTITY_GUARD_BYPASS_ENV: "FUSION_MERGER_BYPASS_IDENTITY_GUARD",
@@ -46,20 +58,33 @@ const makeAudit = () => {
};
};
beforeEach(() => {
execMock.mockReset();
existsSyncMock.mockReset();
existsSyncMock.mockReturnValue(true);
});
describe("acquireTaskWorktree worktrunk wiring", () => {
const roots: string[] = [];
async function makeRootDir(): Promise<string> {
const rootDir = await mkdtemp(join(tmpdir(), "fusion-wt-"));
roots.push(rootDir);
return rootDir;
}
beforeEach(() => {
execMock.mockReset();
existsSyncMock.mockReset();
existsSyncMock.mockReturnValue(true);
});
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
it("uses native by default when worktrunk settings absent", async () => {
execMock.mockResolvedValue({ stdout: "", stderr: "" });
const { acquireTaskWorktree } = await import("../worktree-acquisition.js");
const rootDir = await makeRootDir();
const result = await acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store: makeStore() as any,
settings: {},
});
@@ -79,10 +104,11 @@ describe("acquireTaskWorktree worktrunk wiring", () => {
it("prefers explicit createWorktree override", async () => {
const createWorktree = vi.fn().mockResolvedValue({ path: "/tmp/new", branch: "fusion/fn-1" });
const { acquireTaskWorktree } = await import("../worktree-acquisition.js");
const rootDir = await makeRootDir();
await acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store: makeStore() as any,
settings: { worktrunk: { enabled: true, binaryPath: "wt" } } as any,
createWorktree,
@@ -93,12 +119,13 @@ describe("acquireTaskWorktree worktrunk wiring", () => {
});
it("emits worktrunk + native create audits when worktrunk succeeds", async () => {
const rootDir = await makeRootDir();
execMock.mockImplementation((command: string) => {
if (command.includes('"config" "show"')) return Promise.resolve({ stdout: "", stderr: "" });
if (command.includes('"switch" "--create"')) return Promise.resolve({ stdout: "", stderr: "" });
if (command === "git worktree list --porcelain") {
return Promise.resolve({
stdout: "worktree /repo/.worktrees/fusion/fn-1\nbranch refs/heads/fusion/fn-1\n",
stdout: `worktree ${rootDir}/.worktrees/fusion/fn-1\nbranch refs/heads/fusion/fn-1\n`,
stderr: "",
});
}
@@ -109,7 +136,7 @@ describe("acquireTaskWorktree worktrunk wiring", () => {
await acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store: makeStore() as any,
settings: { worktrunk: { enabled: true, binaryPath: "wt", onFailure: "fail" } } as any,
audit: audit as any,
@@ -122,30 +149,32 @@ describe("acquireTaskWorktree worktrunk wiring", () => {
it("propagates resolved worktrunk path into result and task store", async () => {
const store = makeStore();
const rootDir = await makeRootDir();
const resolvedPath = join(rootDir, ".worktrees/custom/fusion-fn-1");
execMock.mockImplementation((command: string) => {
if (command.includes('"config" "show"')) return Promise.resolve({ stdout: "", stderr: "" });
if (command.includes('"switch" "--create"')) return Promise.resolve({ stdout: "", stderr: "" });
if (command === "git worktree list --porcelain") {
return Promise.resolve({
stdout: "worktree /repo/.worktrees/custom/fusion-fn-1\nbranch refs/heads/fusion/fn-1\n",
stdout: `worktree ${resolvedPath}\nbranch refs/heads/fusion/fn-1\n`,
stderr: "",
});
}
return Promise.resolve({ stdout: "", stderr: "" });
});
existsSyncMock.mockImplementation((path: string) => path === "/repo/.worktrees/custom/fusion-fn-1");
existsSyncMock.mockImplementation((path: string) => path === resolvedPath);
const { acquireTaskWorktree } = await import("../worktree-acquisition.js");
const result = await acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store: store as any,
settings: { worktrunk: { enabled: true, binaryPath: "wt", onFailure: "fail" } } as any,
});
expect(result.worktreePath).toBe("/repo/.worktrees/custom/fusion-fn-1");
expect(result.worktreePath).toBe(resolvedPath);
expect(store.updateTask).toHaveBeenCalledWith("FN-1", {
worktree: "/repo/.worktrees/custom/fusion-fn-1",
worktree: resolvedPath,
branch: "fusion/fn-1",
});
});
@@ -154,11 +183,12 @@ describe("acquireTaskWorktree worktrunk wiring", () => {
execMock.mockRejectedValue({ stderr: "nope", status: 9 });
const { acquireTaskWorktree } = await import("../worktree-acquisition.js");
const { audit, events } = makeAudit();
const rootDir = await makeRootDir();
await expect(
acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store: makeStore() as any,
settings: { worktrunk: { enabled: true, binaryPath: "wt", onFailure: "fail" } } as any,
audit: audit as any,
@@ -181,10 +211,11 @@ describe("acquireTaskWorktree worktrunk wiring", () => {
});
const { acquireTaskWorktree } = await import("../worktree-acquisition.js");
const { audit, events } = makeAudit();
const rootDir = await makeRootDir();
await acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store: makeStore() as any,
settings: { worktrunk: { enabled: true, binaryPath: "wt", onFailure: "fallback-native" } } as any,
audit: audit as any,
@@ -198,11 +229,12 @@ describe("acquireTaskWorktree worktrunk wiring", () => {
it("fails with binary missing when enabled and binaryPath absent", async () => {
const { acquireTaskWorktree } = await import("../worktree-acquisition.js");
const rootDir = await makeRootDir();
await expect(
acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store: makeStore() as any,
settings: { worktrunk: { enabled: true, onFailure: "fail" } } as any,
}),
@@ -212,10 +244,11 @@ describe("acquireTaskWorktree worktrunk wiring", () => {
it("uses custom backend when provided", async () => {
const create = vi.fn().mockResolvedValue({ path: "/tmp/custom", branch: "fusion/fn-1-custom" });
const { acquireTaskWorktree } = await import("../worktree-acquisition.js");
const rootDir = await makeRootDir();
const result = await acquireTaskWorktree({
task,
rootDir: "/repo",
rootDir,
store: makeStore() as any,
settings: { worktrunk: { enabled: true, binaryPath: "wt" } } as any,
backend: {
@@ -237,11 +270,11 @@ describe("acquireTaskWorktree worktrunk wiring", () => {
expect(execMock).toHaveBeenCalledTimes(2);
expect(execMock).toHaveBeenCalledWith(
"git symbolic-ref --short refs/remotes/origin/HEAD",
expect.objectContaining({ cwd: "/repo" }),
expect.objectContaining({ cwd: rootDir }),
);
expect(execMock).toHaveBeenCalledWith(
"git remote",
expect.objectContaining({ cwd: "/repo" }),
expect.objectContaining({ cwd: rootDir }),
);
});
});

View File

@@ -3300,7 +3300,7 @@
"useProjectDefault": "Usar predeterminado del proyecto",
"viewOptions": "Opciones de vista",
"workflowLabel": "",
"statusReplan": ""
"statusReplan": "Replanificar"
},
"mailbox": {
"agent": "Agente",
@@ -5889,13 +5889,13 @@
"restartNow": "",
"restartUnavailable": "",
"restarting": "",
"mobileNavPrimaryItems": "",
"mobileNavPrimaryItemsHint": "",
"addNavItem": "",
"selectNavItem": "",
"removeNavItem": "",
"moveNavItemEarlier": "",
"moveNavItemLater": ""
"mobileNavPrimaryItems": "Acciones rápidas del pie móvil",
"mobileNavPrimaryItemsHint": "Predeterminado: Panel, Tareas, Agentes, Misiones, Chat, Buzón. Añade destinos elegibles; los no seleccionados permanecen en Más.",
"addNavItem": "Añadir acción rápida",
"selectNavItem": "Elegir un destino",
"removeNavItem": "Quitar {{item}}",
"moveNavItemEarlier": "Mover {{item}} antes",
"moveNavItemLater": "Mover {{item}} después"
},
"globalGeneral": {
"andShowsUpdateNoticesInTheCLIAnd": "",
@@ -8371,7 +8371,7 @@
"awaitingApprovalPlanReviewReplanCapTitle": "",
"reviewBudgetExhausted": "",
"awaitingApprovalTitle": "",
"statusReplan": ""
"statusReplan": "Replanificar"
},
"terminal": {
"arrowKeysLabel": "",

View File

@@ -3300,7 +3300,7 @@
"useProjectDefault": "Utiliser le défaut du projet",
"viewOptions": "Options d'affichage",
"workflowLabel": "",
"statusReplan": ""
"statusReplan": "Replanifier"
},
"mailbox": {
"agent": "Agent",
@@ -5889,13 +5889,13 @@
"restartNow": "",
"restartUnavailable": "",
"restarting": "",
"mobileNavPrimaryItems": "",
"mobileNavPrimaryItemsHint": "",
"addNavItem": "",
"selectNavItem": "",
"removeNavItem": "",
"moveNavItemEarlier": "",
"moveNavItemLater": ""
"mobileNavPrimaryItems": "Actions rapides du bas mobile",
"mobileNavPrimaryItemsHint": "Par défaut : Tableau de bord, Tâches, Agents, Missions, Chat, Boîte de réception. Ajoutez des destinations éligibles ; les non sélectionnées restent dans Plus.",
"addNavItem": "Ajouter une action rapide",
"selectNavItem": "Choisir une destination",
"removeNavItem": "Retirer {{item}}",
"moveNavItemEarlier": "Déplacer {{item}} plus tôt",
"moveNavItemLater": "Déplacer {{item}} plus tard"
},
"globalGeneral": {
"andShowsUpdateNoticesInTheCLIAnd": "",
@@ -8371,7 +8371,7 @@
"awaitingApprovalPlanReviewReplanCapTitle": "",
"reviewBudgetExhausted": "",
"awaitingApprovalTitle": "",
"statusReplan": ""
"statusReplan": "Replanifier"
},
"terminal": {
"arrowKeysLabel": "",

View File

@@ -3300,7 +3300,7 @@
"useProjectDefault": "프로젝트 기본값 사용",
"viewOptions": "보기 옵션",
"workflowLabel": "",
"statusReplan": ""
"statusReplan": "재계획"
},
"mailbox": {
"agent": "에이전트",
@@ -5889,13 +5889,13 @@
"restartNow": "",
"restartUnavailable": "",
"restarting": "",
"mobileNavPrimaryItems": "",
"mobileNavPrimaryItemsHint": "",
"addNavItem": "",
"selectNavItem": "",
"removeNavItem": "",
"moveNavItemEarlier": "",
"moveNavItemLater": ""
"mobileNavPrimaryItems": "모바일 하단 빠른 작업",
"mobileNavPrimaryItemsHint": "기본값: 대시보드, 작업, 에이전트, 미션, 채팅, 메일함. 가능한 대상을 추가하세요. 선택하지 않은 항목은 더보기에 유지됩니다.",
"addNavItem": "빠른 작업 추가",
"selectNavItem": "대상 선택",
"removeNavItem": "{{item}} 제거",
"moveNavItemEarlier": "{{item}} 앞으로 이동",
"moveNavItemLater": "{{item}} 뒤로 이동"
},
"globalGeneral": {
"andShowsUpdateNoticesInTheCLIAnd": "",
@@ -8371,7 +8371,7 @@
"awaitingApprovalPlanReviewReplanCapTitle": "",
"reviewBudgetExhausted": "",
"awaitingApprovalTitle": "",
"statusReplan": ""
"statusReplan": "재계획"
},
"terminal": {
"arrowKeysLabel": "",

View File

@@ -3300,7 +3300,7 @@
"useProjectDefault": "使用项目默认",
"viewOptions": "视图选项",
"workflowLabel": "",
"statusReplan": ""
"statusReplan": "重新规划"
},
"mailbox": {
"agent": "代理",
@@ -5889,13 +5889,13 @@
"restartNow": "",
"restartUnavailable": "",
"restarting": "",
"mobileNavPrimaryItems": "",
"mobileNavPrimaryItemsHint": "",
"addNavItem": "",
"selectNavItem": "",
"removeNavItem": "",
"moveNavItemEarlier": "",
"moveNavItemLater": ""
"mobileNavPrimaryItems": "移动端底部快捷操作",
"mobileNavPrimaryItemsHint": "默认:仪表盘、任务、代理、任务组、聊天、邮箱。可添加符合条件的目标;未选中的目标仍在“更多”中。",
"addNavItem": "添加快捷操作",
"selectNavItem": "选择目标",
"removeNavItem": "移除 {{item}}",
"moveNavItemEarlier": "将 {{item}} 前移",
"moveNavItemLater": "将 {{item}} 后移"
},
"globalGeneral": {
"andShowsUpdateNoticesInTheCLIAnd": "",
@@ -8371,7 +8371,7 @@
"awaitingApprovalPlanReviewReplanCapTitle": "",
"reviewBudgetExhausted": "",
"awaitingApprovalTitle": "",
"statusReplan": ""
"statusReplan": "重新规划"
},
"terminal": {
"arrowKeysLabel": "",

View File

@@ -3300,7 +3300,7 @@
"useProjectDefault": "使用專案預設",
"viewOptions": "檢視選項",
"workflowLabel": "",
"statusReplan": ""
"statusReplan": "重新規劃"
},
"mailbox": {
"agent": "代理",
@@ -5889,13 +5889,13 @@
"restartNow": "",
"restartUnavailable": "",
"restarting": "",
"mobileNavPrimaryItems": "",
"mobileNavPrimaryItemsHint": "",
"addNavItem": "",
"selectNavItem": "",
"removeNavItem": "",
"moveNavItemEarlier": "",
"moveNavItemLater": ""
"mobileNavPrimaryItems": "行動版底部快捷操作",
"mobileNavPrimaryItemsHint": "預設:儀表板、任務、代理、任務組、聊天、信箱。可新增符合條件的目的地;未選取的目的地仍在「更多」中。",
"addNavItem": "新增快捷操作",
"selectNavItem": "選擇目的地",
"removeNavItem": "移除 {{item}}",
"moveNavItemEarlier": "將 {{item}} 前移",
"moveNavItemLater": "將 {{item}} 後移"
},
"globalGeneral": {
"andShowsUpdateNoticesInTheCLIAnd": "",
@@ -8371,7 +8371,7 @@
"awaitingApprovalPlanReviewReplanCapTitle": "",
"reviewBudgetExhausted": "",
"awaitingApprovalTitle": "",
"statusReplan": ""
"statusReplan": "重新規劃"
},
"terminal": {
"arrowKeysLabel": "",