Files
fusion/packages/cli/src/__tests__/task-retry.test.ts
gsxdsm c484964b82 FN-8102: fix CLI package-lane test scaffolding
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>
2026-07-16 08:58:13 -07:00

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);
});
});