feat(FN-2991): merge fusion/fn-2991
Commits merged: - fix(FN-2991): align failed research status badge semantics - feat(FN-2991): add research dashboard view and navigation entry - fix(FN-2991): complete Step 8 — resolve lint and typecheck gates - test(FN-2991): address review feedback for Step 7 - test(FN-2991): complete Step 7 — add research store and route tests - feat(FN-2991): complete Step 6 — add research client API helpers - feat(FN-2991): complete Step 5 — add research REST routes - feat(FN-2991): complete Step 4 — wire research store into core exports - feat(FN-2991): complete Step 3 — implement research store - feat(FN-2991): complete Step 2 — add research schema and migration - feat(FN-2991): complete Step 1 — add research domain types Files changed: packages/core/src/__tests__/db.test.ts | 26 +- packages/core/src/__tests__/insight-store.test.ts | 8 +- packages/core/src/__tests__/mission-store.test.ts | 2 +- packages/core/src/__tests__/research-store.test.ts | 118 +++++++ packages/core/src/__tests__/roadmap-store.test.ts | 2 +- packages/core/src/__tests__/run-audit.test.ts | 2 +- packages/core/src/__tests__/task-documents.test.ts | 2 +- packages/core/src/db.ts | 82 ++++- packages/core/src/index.ts | 30 ++ packages/core/src/research-store.ts | 377 +++++++++++++++++++++ packages/core/src/research-types.ts | 162 +++++++++ packages/core/src/store.ts | 14 + packages/dashboard/app/App.tsx | 12 + .../app/api/__tests__/research-api.test.ts | 120 +++++++ packages/dashboard/app/api/legacy.ts | 132 +++++++- packages/dashboard/app/components/Header.tsx | 23 +- packages/dashboard/app/components/MobileNavBar.tsx | 16 +- packages/dashboard/app/components/ResearchView.css | 110 ++++++ packages/dashboard/app/components/ResearchView.tsx | 145 ++++++++ .../app/components/__tests__/Header.test.tsx | 4 +- .../app/components/__tests__/ResearchView.test.tsx | 170 ++++++++++ packages/dashboard/app/hooks/useViewState.ts | 3 +- packages/dashboard/src/research-routes.ts | 223 ++++++++++++ .../src/routes/register-integrated-routers.ts | 2 + 24 files changed, 1751 insertions(+), 34 deletions(-) Fusion-Task-Id: FN-2991
This commit is contained in:
@@ -131,7 +131,7 @@ describe("Database", () => {
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
});
|
||||
|
||||
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(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
});
|
||||
|
||||
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(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
// 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(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -825,7 +825,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
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(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
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(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
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(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
|
||||
expect(cols.map((col) => col.name)).toContain("attachments");
|
||||
@@ -1068,7 +1068,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
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" }]);
|
||||
@@ -1092,7 +1092,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
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" }]);
|
||||
@@ -1196,7 +1196,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29
|
||||
expect(db.getSchemaVersion()).toBe(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -1647,7 +1647,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
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(54);
|
||||
expect(db1.getSchemaVersion()).toBe(55);
|
||||
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(54);
|
||||
expect(db3.getSchemaVersion()).toBe(55);
|
||||
|
||||
// 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(54);
|
||||
expect(db1.getSchemaVersion()).toBe(55);
|
||||
db1.close();
|
||||
|
||||
const db2 = createDatabase(testDir);
|
||||
expect(() => db2.init()).not.toThrow();
|
||||
expect(db2.getSchemaVersion()).toBe(54);
|
||||
expect(db2.getSchemaVersion()).toBe(55);
|
||||
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(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
});
|
||||
|
||||
it("mission_features table has loop state columns", () => {
|
||||
|
||||
118
packages/core/src/__tests__/research-store.test.ts
Normal file
118
packages/core/src/__tests__/research-store.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createDatabase, type Database } from "../db.js";
|
||||
import { ResearchStore } from "../research-store.js";
|
||||
|
||||
describe("ResearchStore", () => {
|
||||
let db: Database;
|
||||
let store: ResearchStore;
|
||||
|
||||
beforeEach(() => {
|
||||
const fusionDir = mkdtempSync(join(tmpdir(), "fn-research-test-"));
|
||||
db = createDatabase(fusionDir, { inMemory: true });
|
||||
db.init();
|
||||
store = new ResearchStore(db);
|
||||
});
|
||||
|
||||
it("creates, gets, updates, lists and deletes runs", () => {
|
||||
const run = store.createRun({ query: "test topic", tags: ["a"] });
|
||||
expect(run.id).toMatch(/^RR-/);
|
||||
expect(store.getRun(run.id)?.query).toBe("test topic");
|
||||
|
||||
const updated = store.updateRun(run.id, { topic: "new topic", error: "oops" });
|
||||
expect(updated?.topic).toBe("new topic");
|
||||
|
||||
const listed = store.listRuns({ status: "pending" });
|
||||
expect(listed.map((r) => r.id)).toContain(run.id);
|
||||
|
||||
expect(store.deleteRun(run.id)).toBe(true);
|
||||
expect(store.getRun(run.id)).toBeUndefined();
|
||||
expect(store.deleteRun("RR-missing")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles status transitions with lifecycle timestamps", () => {
|
||||
const run = store.createRun({ query: "status test" });
|
||||
store.updateStatus(run.id, "running");
|
||||
const running = store.getRun(run.id)!;
|
||||
expect(running.startedAt).toBeTruthy();
|
||||
|
||||
store.updateStatus(run.id, "completed");
|
||||
const completed = store.getRun(run.id)!;
|
||||
expect(completed.completedAt).toBeTruthy();
|
||||
|
||||
const failed = store.createRun({ query: "failure" });
|
||||
store.updateStatus(failed.id, "failed");
|
||||
expect(store.getRun(failed.id)?.completedAt).toBeTruthy();
|
||||
|
||||
const cancelled = store.createRun({ query: "cancel" });
|
||||
store.updateStatus(cancelled.id, "cancelled");
|
||||
expect(store.getRun(cancelled.id)?.cancelledAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("appends events, manages sources, and sets results", () => {
|
||||
const run = store.createRun({ query: "events" });
|
||||
const event = store.appendEvent(run.id, { type: "info", message: "started" });
|
||||
expect(event.id).toMatch(/^REVT-/);
|
||||
|
||||
const source = store.addSource(run.id, {
|
||||
type: "web",
|
||||
reference: "https://example.com",
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
store.updateSource(run.id, source.id, { status: "completed", title: "Example" });
|
||||
store.setResults(run.id, {
|
||||
summary: "Done",
|
||||
findings: [{ heading: "H1", content: "C1", sources: [source.id], confidence: 0.8 }],
|
||||
});
|
||||
|
||||
const next = store.getRun(run.id)!;
|
||||
expect(next.events).toHaveLength(1);
|
||||
expect(next.sources[0].status).toBe("completed");
|
||||
expect(next.results?.summary).toBe("Done");
|
||||
});
|
||||
|
||||
it("supports filtering, search, ordering, exports and stats", () => {
|
||||
const r1 = store.createRun({ query: "alpha", topic: "first", tags: ["core"] });
|
||||
const r2 = store.createRun({ query: "beta", topic: "second", tags: ["edge"] });
|
||||
store.setResults(r2.id, { summary: "beta summary", findings: [] });
|
||||
|
||||
expect(store.listRuns({ tag: "core" }).map((r) => r.id)).toEqual([r1.id]);
|
||||
expect(store.searchRuns("beta").map((r) => r.id)).toContain(r2.id);
|
||||
|
||||
const all = store.listRuns();
|
||||
expect(all[0].createdAt <= all[1].createdAt).toBe(true);
|
||||
|
||||
const ex = store.createExport(r1.id, "json", "{}");
|
||||
expect(store.getExports(r1.id)).toHaveLength(1);
|
||||
expect(store.getExport(ex.id)?.runId).toBe(r1.id);
|
||||
expect(store.getExport("REXP-missing")).toBeUndefined();
|
||||
|
||||
store.updateStatus(r1.id, "running");
|
||||
store.updateStatus(r2.id, "completed");
|
||||
const stats = store.getStats();
|
||||
expect(stats.total).toBeGreaterThanOrEqual(2);
|
||||
expect(stats.byStatus.completed).toBeGreaterThanOrEqual(1);
|
||||
|
||||
store.deleteRun(r1.id);
|
||||
expect(store.getExports(r1.id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("emits status events and throws for missing run mutations", () => {
|
||||
const onStatus = vi.fn();
|
||||
const onCompleted = vi.fn();
|
||||
store.on("run:status_changed", onStatus);
|
||||
store.on("run:completed", onCompleted);
|
||||
|
||||
const run = store.createRun({ query: "events" });
|
||||
store.updateStatus(run.id, "completed");
|
||||
expect(onStatus).toHaveBeenCalled();
|
||||
expect(onCompleted).toHaveBeenCalled();
|
||||
|
||||
expect(() => store.appendEvent("missing", { type: "info", message: "x" })).toThrow(/not found/i);
|
||||
expect(() => store.addSource("missing", { type: "web", reference: "x", status: "pending" })).toThrow(/not found/i);
|
||||
expect(() => store.setResults("missing", { findings: [] })).toThrow(/not found/i);
|
||||
});
|
||||
});
|
||||
@@ -742,7 +742,7 @@ describe("RoadmapStore", () => {
|
||||
|
||||
describe("schema version", () => {
|
||||
it("schema version is 40 after init", () => {
|
||||
expect(db.getSchemaVersion()).toBe(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ describe("Run Audit", () => {
|
||||
});
|
||||
|
||||
it("schema version is bumped to 40", () => {
|
||||
expect(db.getSchemaVersion()).toBe(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(54);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
|
||||
const index = db
|
||||
.prepare(
|
||||
|
||||
Reference in New Issue
Block a user