Restore CLI tests to the current PostgreSQL harness and structured tool-error contracts. - Inject cached stores and project-context mocks for isolated extension and command tests - Assert structured MCP error responses for delete and lineage rejection cases - Update workflow, retry, and project mocks for current harness and core APIs Files changed: .../extension-experiment-finalize.test.ts | 21 ++++++++++-- .../src/__tests__/extension-workflow-tools.test.ts | 15 ++++++-- .../__tests__/task-command-gitlab-import.test.ts | 20 +++++++++++ .../task-delete-allow-resurrection.test.ts | 20 ++++++----- .../cli/src/__tests__/task-lineage-unlink.test.ts | 40 +++++++++++++++------- packages/cli/src/__tests__/task-retry.test.ts | 16 +++++++-- .../cli/src/commands/__tests__/project.test.ts | 4 +++ .../cli/src/commands/__tests__/task.test.ts | 6 ++++ 8 files changed, 114 insertions(+), 28 deletions(-) Fusion-Task-Id: FN-8102 Fusion-Task-Lineage: 20723ce0-77bb-4d37-9395-4191769ee752 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
139 lines
5.3 KiB
TypeScript
139 lines
5.3 KiB
TypeScript
/**
|
|
* FNXC:PostgresCutover 2026-07-04-00:00:
|
|
* Migrated from the legacy SQLite `new TaskStore(tmpDir)` harness to the
|
|
* PostgreSQL extension harness. `runTaskRetry` resolves its store through the
|
|
* CLI command path (`project-context.resolveProject`), which is independent of
|
|
* the extension store cache the harness injects — so `resolveProject` is
|
|
* redirected to the harness's PG-backed store, and the full retry lifecycle
|
|
* (moveTask / updateTask / getTask / logEntry) runs against real PostgreSQL
|
|
* state instead of the removed SQLite runtime.
|
|
*
|
|
* FNXC:CliTests 2026-07-16-08:45:
|
|
* FN-8102 repairs stale retry scaffolding left after the PG migration: every
|
|
* lifecycle seed and verification read must use the initialized harness store,
|
|
* rather than the removed `createStore()` helper.
|
|
*/
|
|
|
|
import { afterAll, afterEach, beforeAll, beforeEach, expect, it, vi } from "vitest";
|
|
import { pgDescribe } from "../../../core/src/__test-utils__/pg-test-harness.js";
|
|
import { createPgExtensionHarness } from "./pg-extension-harness.js";
|
|
|
|
// `runTaskRetry` resolves its store via resolveProject() (commands/task.ts →
|
|
// project-context.ts), a separate cache from the extension store the harness
|
|
// injects. Redirect resolveProject to the harness PG store so the command path
|
|
// and the seeded task share one isolated PostgreSQL database.
|
|
const resolveProjectMock = vi.hoisted(() => vi.fn());
|
|
const closeProjectStoreMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
|
vi.mock("../project-context.js", () => ({
|
|
resolveProject: resolveProjectMock,
|
|
// FNXC:CliTests 2026-07-16-08:47: FN-8102 keeps command-finally cleanup
|
|
// awaitable while the PG harness retains ownership of the test store lifecycle.
|
|
closeProjectStore: closeProjectStoreMock,
|
|
}));
|
|
|
|
import { runTaskRetry } from "../commands/task.js";
|
|
|
|
const pgTest = pgDescribe;
|
|
|
|
pgTest("runTaskRetry", () => {
|
|
const h = createPgExtensionHarness("fn-task-retry");
|
|
|
|
beforeAll(h.beforeAll);
|
|
beforeEach(async () => {
|
|
await h.beforeEach();
|
|
resolveProjectMock.mockResolvedValue({
|
|
store: h.store(),
|
|
projectId: h.rootDir(),
|
|
projectPath: h.rootDir(),
|
|
projectName: "test",
|
|
isRegistered: false,
|
|
});
|
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
});
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
resolveProjectMock.mockReset();
|
|
closeProjectStoreMock.mockClear();
|
|
await h.afterEach();
|
|
});
|
|
afterAll(h.afterAll);
|
|
|
|
it("retries merge-active missing-worktree session failures by clearing phantom metadata", async () => {
|
|
const store = h.store();
|
|
const task = await store.createTask({
|
|
title: "missing worktree merge-active task",
|
|
description: "test",
|
|
column: "todo",
|
|
});
|
|
await store.moveTask(task.id, "in-progress");
|
|
await store.moveTask(task.id, "in-review");
|
|
await store.updateTask(task.id, {
|
|
status: "merging",
|
|
error: "Refusing to start coding agent in missing worktree: /tmp/fusion-missing-worktree",
|
|
worktree: "/tmp/fusion-missing-worktree",
|
|
branch: `fusion/${task.id}`,
|
|
sessionFile: "/tmp/fusion-session.json",
|
|
steps: [{ name: "implemented", status: "done" }, { name: "fix", status: "pending" }],
|
|
worktreeSessionRetryCount: 3,
|
|
mergeRetries: 3,
|
|
});
|
|
|
|
await runTaskRetry(task.id);
|
|
|
|
const verificationStore = h.store();
|
|
const updated = await verificationStore.getTask(task.id);
|
|
expect(updated.column).toBe("todo");
|
|
expect(updated.status).toBeUndefined();
|
|
expect(updated.error).toBeUndefined();
|
|
expect(updated.worktree).toBeUndefined();
|
|
expect(updated.branch).toBeUndefined();
|
|
expect(updated.sessionFile).toBeUndefined();
|
|
expect(updated.worktreeSessionRetryCount).toBe(0);
|
|
expect(updated.mergeRetries).toBe(0);
|
|
expect(updated.steps?.[0]?.status).toBe("done");
|
|
});
|
|
|
|
it("rejects unrelated merge-active tasks without the missing-worktree signature", async () => {
|
|
const store = h.store();
|
|
const task = await store.createTask({ title: "ordinary merge", description: "test", column: "todo" });
|
|
await store.moveTask(task.id, "in-progress");
|
|
await store.moveTask(task.id, "in-review");
|
|
await store.updateTask(task.id, {
|
|
status: "merging",
|
|
error: "ordinary merge still running",
|
|
steps: [{ name: "implemented", status: "done" }],
|
|
});
|
|
|
|
await expect(runTaskRetry(task.id)).rejects.toThrow(/not in a retryable state/);
|
|
});
|
|
|
|
it("clears the deadlock auto-pause when retrying a failed task", async () => {
|
|
const store = h.store();
|
|
const task = await store.createTask({
|
|
title: "deadlock-paused task",
|
|
description: "test",
|
|
column: "todo",
|
|
});
|
|
await store.moveTask(task.id, "in-progress");
|
|
await store.moveTask(task.id, "in-review");
|
|
await store.updateTask(task.id, {
|
|
status: "failed",
|
|
error: "merge deadlock",
|
|
paused: true,
|
|
pausedReason: "in-review-stall-deadlock",
|
|
steps: [{ name: "implemented", status: "done" }],
|
|
mergeRetries: 4,
|
|
});
|
|
|
|
await runTaskRetry(task.id);
|
|
|
|
const updated = await store.getTask(task.id);
|
|
expect(updated.column).toBe("todo");
|
|
expect(updated.status).toBeFalsy();
|
|
expect(updated.error).toBeFalsy();
|
|
expect(updated.paused).toBeFalsy();
|
|
expect(updated.pausedReason).toBeFalsy();
|
|
expect(updated.mergeRetries).toBe(0);
|
|
});
|
|
});
|