feat(FN-2922): merge fusion/fn-2922
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ describe("Run Audit", () => {
|
||||
});
|
||||
|
||||
it("schema version is bumped to 40", () => {
|
||||
expect(db.getSchemaVersion()).toBe(52);
|
||||
expect(db.getSchemaVersion()).toBe(53);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user