feat(FN-2922): merge fusion/fn-2922

This commit is contained in:
gsxdsm
2026-04-29 12:01:13 -07:00
parent 8796a32401
commit cecf14a4f1
11 changed files with 253 additions and 27 deletions

View File

@@ -131,7 +131,7 @@ describe("Database", () => {
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
it("seeds lastModified", () => {
@@ -154,7 +154,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
it("does not overwrite existing config on re-init", () => {
@@ -761,7 +761,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -786,11 +786,11 @@ describe("schema migrations", () => {
const db = new Database(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
db.close();
});
@@ -825,7 +825,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("priority");
@@ -866,7 +866,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -935,7 +935,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -994,7 +994,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("attachments");
@@ -1005,6 +1005,58 @@ describe("schema migrations", () => {
db.close();
});
it("migration v53 adds task provenance columns", () => {
tmpDir = makeTmpDir();
const fusionDir = join(tmpDir, ".fusion");
const localDb = new Database(fusionDir);
localDb.init();
const columns = localDb.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const columnNames = columns.map((c) => c.name);
expect(columnNames).toContain("sourceType");
expect(columnNames).toContain("sourceAgentId");
expect(columnNames).toContain("sourceRunId");
expect(columnNames).toContain("sourceSessionId");
expect(columnNames).toContain("sourceMessageId");
expect(columnNames).toContain("sourceParentTaskId");
expect(columnNames).toContain("sourceMetadata");
localDb.close();
});
it("migration v53 backfills sourceType to unknown", () => {
tmpDir = makeTmpDir();
const fusionDir = join(tmpDir, ".fusion");
const legacyDb = new Database(fusionDir);
legacyDb.exec(`
CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
"column" TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS config (
id INTEGER PRIMARY KEY CHECK (id = 1),
nextId INTEGER DEFAULT 1,
nextWorkflowStepId INTEGER DEFAULT 1,
settings TEXT DEFAULT '{}',
workflowSteps TEXT DEFAULT '[]',
updatedAt TEXT
);
`);
legacyDb.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '52')");
legacyDb.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
legacyDb.exec(`INSERT INTO tasks (id, description, "column", createdAt, updatedAt) VALUES ('FN-53', 'legacy', 'triage', '2026-01-01', '2026-01-01')`);
legacyDb.init();
const row = legacyDb.prepare("SELECT sourceType FROM tasks WHERE id = 'FN-53'").get() as { sourceType: string | null };
expect(row.sourceType).toBe("unknown");
legacyDb.close();
});
it("applies migration 14+15 by creating agentRatings and ai_sessions indexes", () => {
tmpDir = makeTmpDir();
const fusionDir = join(tmpDir, ".fusion");
@@ -1016,7 +1068,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -1040,7 +1092,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]);
@@ -1144,7 +1196,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1595,7 +1647,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();

View File

@@ -779,7 +779,7 @@ describe("Migration: pre-33 DB upgrade", () => {
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
const db1 = createDatabase(legacyDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(52);
expect(db1.getSchemaVersion()).toBe(53);
db1.close();
// Step 2: Manually downgrade to version 32 and drop insight tables
@@ -814,7 +814,7 @@ describe("Migration: pre-33 DB upgrade", () => {
expect(tableNamesBefore).not.toContain("project_insight_runs");
// Now run init — this triggers the v32→v33 migration
db3.init();
expect(db3.getSchemaVersion()).toBe(52);
expect(db3.getSchemaVersion()).toBe(53);
// Step 4: Verify insight tables exist after migration
const tablesAfter = db3.prepare(
@@ -845,12 +845,12 @@ describe("Migration: pre-33 DB upgrade", () => {
try {
const db1 = createDatabase(testDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(52);
expect(db1.getSchemaVersion()).toBe(53);
db1.close();
const db2 = createDatabase(testDir);
expect(() => db2.init()).not.toThrow();
expect(db2.getSchemaVersion()).toBe(52);
expect(db2.getSchemaVersion()).toBe(53);
db2.close();
} finally {
rmSync(testDir, { recursive: true, force: true });

View File

@@ -2629,7 +2629,7 @@ describe("MissionStore", () => {
describe("Loop State & Validator Run Schema (v31)", () => {
it("schema version is 40 after migration", () => {
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
it("mission_features table has loop state columns", () => {

View File

@@ -742,7 +742,7 @@ describe("RoadmapStore", () => {
describe("schema version", () => {
it("schema version is 40 after init", () => {
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
});

View File

@@ -465,7 +465,7 @@ describe("Run Audit", () => {
});
it("schema version is bumped to 40", () => {
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
});
});

View File

@@ -7022,6 +7022,77 @@ Task with acceptance criteria
});
});
describe("task provenance", () => {
it("defaults sourceType to unknown when source is omitted", async () => {
const task = await store.createTask({ description: "Provenance default" });
const fetched = await store.getTask(task.id);
expect(fetched.sourceType).toBe("unknown");
});
it("persists simple source type from createTask", async () => {
const task = await store.createTask({
description: "Created from dashboard",
source: { sourceType: "dashboard_ui" },
});
const fetched = await store.getTask(task.id);
expect(fetched.sourceType).toBe("dashboard_ui");
});
it("roundtrips full provenance metadata", async () => {
const task = await store.createTask({
description: "Heartbeat-generated task",
source: {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-123",
sourceRunId: "run-456",
sourceSessionId: "session-789",
sourceMessageId: "msg-001",
sourceMetadata: { reason: "scheduled" },
},
});
const fetched = await store.getTask(task.id);
expect(fetched.sourceType).toBe("agent_heartbeat");
expect(fetched.sourceAgentId).toBe("agent-123");
expect(fetched.sourceRunId).toBe("run-456");
expect(fetched.sourceSessionId).toBe("session-789");
expect(fetched.sourceMessageId).toBe("msg-001");
expect(fetched.sourceMetadata).toEqual({ reason: "scheduled" });
});
it("sets duplicate and refine provenance parent links", async () => {
const source = await store.createTask({ description: "Original" });
const duplicated = await store.duplicateTask(source.id);
expect(duplicated.sourceType).toBe("task_duplicate");
expect(duplicated.sourceParentTaskId).toBe(source.id);
await store.moveTask(source.id, "todo");
await store.moveTask(source.id, "in-progress");
await store.moveTask(source.id, "in-review");
await store.moveTask(source.id, "done");
const refined = await store.refineTask(source.id, "Needs polish");
expect(refined.sourceType).toBe("task_refine");
expect(refined.sourceParentTaskId).toBe(source.id);
});
it("preserves provenance on updateTask", async () => {
const task = await store.createTask({
description: "Will be updated",
source: {
sourceType: "automation",
sourceAgentId: "agent-auto",
sourceMetadata: { trigger: "nightly" },
},
});
await store.updateTask(task.id, { title: "Updated" });
const fetched = await store.getTask(task.id);
expect(fetched.sourceType).toBe("automation");
expect(fetched.sourceAgentId).toBe("agent-auto");
expect(fetched.sourceMetadata).toEqual({ trigger: "nightly" });
});
});
// ── Title Handling Tests ────────────────────────────────────────
describe("title handling", () => {

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const index = db
.prepare(