## 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 -->
87 lines
4.0 KiB
TypeScript
87 lines
4.0 KiB
TypeScript
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { AgentStore } from "@fusion/core";
|
|
import { TaskExecutor } from "../executor.js";
|
|
import { resolveExecutorSessionModel } from "../agent-session-helpers.js";
|
|
|
|
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;
|
|
}
|
|
|
|
describe("TaskExecutor assigned-agent runtimeConfig lookup", () => {
|
|
const roots: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
});
|
|
|
|
async function createHarness(runtimeConfig: Record<string, unknown> | undefined) {
|
|
const rootDir = await mkdtemp(join(tmpdir(), "fn-7787-"));
|
|
roots.push(rootDir);
|
|
/*
|
|
* FNXC:PostgresCutover 2026-07-10:
|
|
* Upstream seeded a real sqlite AgentStore; that runtime is removed on
|
|
* this branch (VAL-REMOVAL-005). The invariant under test is the
|
|
* EXECUTOR's fallback from an agents-less execution store to the
|
|
* authoritative project agent store, so spy the AgentStore prototype the
|
|
* executor constructs internally instead of persisting a real agent.
|
|
*/
|
|
const agent = {
|
|
id: `agent-${Math.random().toString(16).slice(2)}`,
|
|
name: `executor-${Math.random().toString(16).slice(2)}`,
|
|
role: "executor",
|
|
state: "idle",
|
|
...(runtimeConfig ? { runtimeConfig } : {}),
|
|
};
|
|
vi.spyOn(AgentStore.prototype, "init").mockResolvedValue(undefined);
|
|
vi.spyOn(AgentStore.prototype, "getAgent").mockImplementation(async (id: string) => (id === agent.id ? (agent as never) : null) as never);
|
|
const worktreeAgentStore = { getAgent: vi.fn().mockResolvedValue(null) };
|
|
const executor = new TaskExecutor(createStore(), rootDir, { agentStore: worktreeAgentStore } as any);
|
|
return { executor: executor as any, agent, worktreeAgentStore };
|
|
}
|
|
|
|
it.each([
|
|
[{ model: "anthropic/claude-fable-5", modelProvider: "ignored", modelId: "ignored" }, { provider: "anthropic", modelId: "claude-fable-5" }],
|
|
[{ modelProvider: "anthropic", modelId: "claude-fable-5" }, { provider: "anthropic", modelId: "claude-fable-5" }],
|
|
])("falls back from an agents-less execution store to the authoritative project agent runtimeConfig %#", async (runtimeConfig, expected) => {
|
|
const { executor, agent, worktreeAgentStore } = await createHarness(runtimeConfig);
|
|
|
|
const foundRuntimeConfig = await executor.getAssignedAgentRuntimeConfig(agent.id);
|
|
|
|
expect(worktreeAgentStore.getAgent).toHaveBeenCalledWith(agent.id);
|
|
expect(resolveExecutorSessionModel(undefined, undefined, {}, foundRuntimeConfig)).toEqual(expected);
|
|
});
|
|
|
|
it("returns undefined when the assigned agent is missing or has no complete runtime model", async () => {
|
|
const { executor, agent } = await createHarness(undefined);
|
|
|
|
expect(await executor.getAssignedAgentRuntimeConfig("missing-agent")).toBeUndefined();
|
|
const runtimeConfig = await executor.getAssignedAgentRuntimeConfig(agent.id);
|
|
expect(resolveExecutorSessionModel(undefined, undefined, {}, runtimeConfig)).toEqual({ provider: undefined, modelId: undefined });
|
|
});
|
|
|
|
it("keeps configured settings ahead of the assigned agent runtimeConfig", async () => {
|
|
const { executor, agent } = await createHarness({ model: "anthropic/claude-fable-5" });
|
|
|
|
const runtimeConfig = await executor.getAssignedAgentRuntimeConfig(agent.id);
|
|
|
|
expect(resolveExecutorSessionModel(undefined, undefined, {
|
|
executionProvider: "openai",
|
|
executionModelId: "gpt-4.1",
|
|
}, runtimeConfig)).toEqual({ provider: "openai", modelId: "gpt-4.1" });
|
|
});
|
|
});
|