test(FN-4892): add reliability interaction coverage for intake auto-archive
Fusion-Task-Id: FN-4892 Fusion-Task-Lineage: 6f56b468-a827-416d-ba1f-444e1326d613
This commit is contained in:
committed by
gsxdsm
parent
e0f8e4c7e7
commit
619070388b
@@ -0,0 +1,89 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { TaskStore } from "@fusion/core";
|
||||
|
||||
async function createStore() {
|
||||
const rootDir = await mkdtemp(join(tmpdir(), "fusion-duplicate-intake-"));
|
||||
const store = new TaskStore(rootDir, undefined, { inMemoryDb: true });
|
||||
await store.init();
|
||||
return {
|
||||
store,
|
||||
cleanup: async () => {
|
||||
store.close();
|
||||
await rm(rootDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("reliability interactions: same-agent duplicate intake", () => {
|
||||
const fixtures: Array<Awaited<ReturnType<typeof createStore>>> = [];
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
while (fixtures.length) await fixtures.pop()!.cleanup();
|
||||
});
|
||||
|
||||
it("archives later near-duplicate from same agent", async () => {
|
||||
const fx = await createStore();
|
||||
fixtures.push(fx);
|
||||
|
||||
const a = await fx.store.createTask({
|
||||
title: "fix: secrets sync typecheck",
|
||||
description: "typecheck error in secrets-sync",
|
||||
source: { sourceType: "agent", sourceAgentId: "agent-x" },
|
||||
});
|
||||
const b = await fx.store.createTask({
|
||||
title: "fix: secrets sync typecheck regression",
|
||||
description: "typecheck error in secrets-sync",
|
||||
source: { sourceType: "agent", sourceAgentId: "agent-x" },
|
||||
});
|
||||
|
||||
expect((await fx.store.getTask(a.id)).column).toBe("triage");
|
||||
expect((await fx.store.getTask(b.id)).column).toBe("archived");
|
||||
const activity = await fx.store.getActivityLog({ type: "task:auto-archived-duplicate", limit: 10 });
|
||||
const entry = activity.find((item) => item.taskId === b.id);
|
||||
expect(entry).toBeTruthy();
|
||||
expect((entry?.metadata as { siblingTaskIds?: string[] } | null)?.siblingTaskIds).toEqual([a.id]);
|
||||
});
|
||||
|
||||
it("does not archive unrelated tasks", async () => {
|
||||
const fx = await createStore();
|
||||
fixtures.push(fx);
|
||||
|
||||
const a = await fx.store.createTask({
|
||||
title: "fix: api timeout",
|
||||
description: "network timeout issue",
|
||||
source: { sourceType: "agent", sourceAgentId: "agent-x" },
|
||||
});
|
||||
const b = await fx.store.createTask({
|
||||
title: "feat: add mission detail panel",
|
||||
description: "new dashboard ui",
|
||||
source: { sourceType: "agent", sourceAgentId: "agent-x" },
|
||||
});
|
||||
|
||||
expect((await fx.store.getTask(a.id)).column).toBe("triage");
|
||||
expect((await fx.store.getTask(b.id)).column).toBe("triage");
|
||||
});
|
||||
|
||||
it("fails open when duplicate detection throws", async () => {
|
||||
const fx = await createStore();
|
||||
fixtures.push(fx);
|
||||
|
||||
await fx.store.createTask({
|
||||
title: "fix: baseline",
|
||||
description: "desc",
|
||||
source: { sourceType: "agent", sourceAgentId: "agent-x" },
|
||||
});
|
||||
|
||||
vi.spyOn(fx.store, "listTasks").mockRejectedValueOnce(new Error("boom"));
|
||||
|
||||
const b = await fx.store.createTask({
|
||||
title: "fix: baseline clone",
|
||||
description: "desc",
|
||||
source: { sourceType: "agent", sourceAgentId: "agent-x" },
|
||||
});
|
||||
|
||||
expect((await fx.store.getTask(b.id)).column).toBe("triage");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { execSync } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { DEFAULT_SETTINGS, TaskStore } from "@fusion/core";
|
||||
import { TriageProcessor } from "../../triage.js";
|
||||
import * as triagePreflight from "../../triage-preflight.js";
|
||||
|
||||
function git(cwd: string, command: string): string {
|
||||
return execSync(command, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
||||
}
|
||||
|
||||
async function createFixture() {
|
||||
const rootDir = await mkdtemp(join(tmpdir(), "fusion-ghost-preflight-"));
|
||||
git(rootDir, "git init -b main");
|
||||
git(rootDir, 'git config user.email "test@example.com"');
|
||||
git(rootDir, 'git config user.name "Test User"');
|
||||
await mkdir(join(rootDir, "packages/core/src"), { recursive: true });
|
||||
await writeFile(join(rootDir, "packages/core/src/secrets-sync.ts"), "export const value = 1;\n", "utf-8");
|
||||
git(rootDir, "git add .");
|
||||
git(rootDir, 'git commit -m "chore: init fixture"');
|
||||
|
||||
const store = new TaskStore(rootDir, undefined, { inMemoryDb: true });
|
||||
await store.init();
|
||||
await store.updateSettings({ ...DEFAULT_SETTINGS, requirePlanApproval: false });
|
||||
const triage = new TriageProcessor(store, rootDir);
|
||||
|
||||
return {
|
||||
rootDir,
|
||||
store,
|
||||
triage,
|
||||
cleanup: async () => {
|
||||
store.close();
|
||||
await rm(rootDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("reliability interactions: ghost-bug preflight", () => {
|
||||
const fixtures: Array<Awaited<ReturnType<typeof createFixture>>> = [];
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
while (fixtures.length) await fixtures.pop()!.cleanup();
|
||||
});
|
||||
|
||||
const basePrompt = `# Task: FN-1 - Fix issue\n\n**Size:** S\n\n## Review Level: 1\n`;
|
||||
|
||||
it("auto-archives when cited construct is missing", async () => {
|
||||
const fx = await createFixture();
|
||||
fixtures.push(fx);
|
||||
const task = await fx.store.createTask({ title: "fix: missing construct", description: "typecheck error", prompt: "draft" });
|
||||
|
||||
await (fx.triage as any).finalizeApprovedTask(
|
||||
task,
|
||||
`${basePrompt}\nCited identifier: \`DefinitelyMissingSymbol_DoNotExist\`\n`,
|
||||
await fx.store.getSettings(),
|
||||
{},
|
||||
);
|
||||
|
||||
const updated = await fx.store.getTask(task.id);
|
||||
expect(updated.column).toBe("archived");
|
||||
const activity = await fx.store.getActivityLog({ type: "task:auto-archived-ghost-bug", limit: 10 });
|
||||
expect(activity.some((entry) => entry.taskId === task.id)).toBe(true);
|
||||
const audit = fx.store.getRunAuditEvents({ taskId: task.id, limit: 20 });
|
||||
expect(audit.some((entry) => entry.mutationType === "task:auto-archived-ghost-bug")).toBe(true);
|
||||
});
|
||||
|
||||
it("passes to todo when construct exists", async () => {
|
||||
const fx = await createFixture();
|
||||
fixtures.push(fx);
|
||||
const task = await fx.store.createTask({ title: "fix: existing construct", description: "typecheck error", prompt: "draft" });
|
||||
|
||||
await (fx.triage as any).finalizeApprovedTask(
|
||||
task,
|
||||
`${basePrompt}\n\`value = 1;\`\n`,
|
||||
await fx.store.getSettings(),
|
||||
{},
|
||||
);
|
||||
|
||||
const updated = await fx.store.getTask(task.id);
|
||||
expect(updated.column).toBe("todo");
|
||||
});
|
||||
|
||||
it("fails open when probe throws", async () => {
|
||||
const fx = await createFixture();
|
||||
fixtures.push(fx);
|
||||
const task = await fx.store.createTask({ title: "fix: throwing probe", description: "typecheck error", prompt: "draft" });
|
||||
vi.spyOn(triagePreflight, "runGhostBugPreflight").mockRejectedValueOnce(new Error("boom"));
|
||||
|
||||
await (fx.triage as any).finalizeApprovedTask(
|
||||
task,
|
||||
`${basePrompt}\nCited identifier: \`DefinitelyMissingSymbol_DoNotExist\`\n`,
|
||||
await fx.store.getSettings(),
|
||||
{},
|
||||
);
|
||||
|
||||
const updated = await fx.store.getTask(task.id);
|
||||
expect(updated.column).toBe("todo");
|
||||
const activity = await fx.store.getActivityLog({ type: "task:auto-archived-ghost-bug", limit: 10 });
|
||||
expect(activity.some((entry) => entry.taskId === task.id)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user