feat(FN-671): preserve quick entry disclosure and normalize comment migration

- Keep QuickEntryBox disclosure state persisted and covered by updated UI tests
- Add normalized legacy comment migration logic to unify steering comments without duplication
- Update database migration tests to verify schema v5 comment handling behavior
- Carry merged dashboard, engine, and routing changes into the squash commit
This commit is contained in:
gsxdsm
2026-04-01 13:07:26 -07:00
parent 1ccaf34d9b
commit 0da3fea13c
14 changed files with 511 additions and 230 deletions

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { Database, createDatabase, toJson, toJsonNullable, fromJson } from "./db.js";
import { Database, createDatabase, toJson, toJsonNullable, fromJson, normalizeTaskComments } from "./db.js";
import { DEFAULT_PROJECT_SETTINGS } from "./types.js";
import { mkdtempSync, existsSync } from "node:fs";
import { join } from "node:path";
@@ -491,6 +491,26 @@ describe("Database", () => {
});
});
describe("comment normalization", () => {
it("merges overlapping legacy and unified comments exactly once", () => {
const normalized = normalizeTaskComments(
[{ id: "c1", text: "Legacy note", author: "user", createdAt: "2025-01-01T00:00:00.000Z" }],
[{ id: "c1", text: "Legacy note", author: "user", createdAt: "2025-01-01T00:00:00.000Z", updatedAt: "2025-01-02T00:00:00.000Z" }],
);
expect(normalized.comments).toEqual([
{
id: "c1",
text: "Legacy note",
author: "user",
createdAt: "2025-01-01T00:00:00.000Z",
updatedAt: "2025-01-02T00:00:00.000Z",
},
]);
expect(normalized.steeringComments).toHaveLength(1);
});
});
describe("JSON helpers", () => {
describe("toJson", () => {
it("stringifies arrays", () => {
@@ -683,7 +703,7 @@ describe("schema migrations", () => {
// Now run init() which should trigger migration
db.init();
// Verify version bumped to 4 (includes v1→v2, v2→v3, and v3→v4 migrations)
// Verify version bumped to 5 (includes v1→v2, v2→v3, v3→v4, and v4→v5 migrations)
expect(db.getSchemaVersion()).toBe(5);
// Verify new columns exist and existing data is intact
@@ -807,7 +827,7 @@ describe("schema migrations", () => {
// Now run init() which should trigger migrations v2→v3→v4
db.init();
// Verify version bumped to 4
// Verify version bumped to 5
expect(db.getSchemaVersion()).toBe(5);
// Verify new columns exist and existing data is intact
@@ -836,6 +856,159 @@ describe("schema migrations", () => {
db.close();
});
it("migrates pre-comments databases by copying steering comments into unified comments exactly once", () => {
tmpDir = makeTmpDir();
const kbDir = join(tmpDir, ".fusion");
const db = new Database(kbDir);
db.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,
steeringComments TEXT DEFAULT '[]'
);
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
);
CREATE TABLE IF NOT EXISTS activityLog (
id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, type TEXT NOT NULL,
taskId TEXT, taskTitle TEXT, details TEXT NOT NULL, metadata TEXT
);
CREATE TABLE IF NOT EXISTS archivedTasks (id TEXT PRIMARY KEY, data TEXT NOT NULL, archivedAt TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS automations (
id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT,
scheduleType TEXT NOT NULL, cronExpression TEXT NOT NULL, command TEXT NOT NULL,
enabled INTEGER DEFAULT 1, timeoutMs INTEGER, steps TEXT,
nextRunAt TEXT, lastRunAt TEXT, lastRunResult TEXT,
runCount INTEGER DEFAULT 0, runHistory TEXT DEFAULT '[]',
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY, name TEXT NOT NULL, role TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'idle', taskId TEXT,
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
lastHeartbeatAt TEXT, metadata TEXT DEFAULT '{}'
);
CREATE TABLE IF NOT EXISTS agentHeartbeats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agentId TEXT NOT NULL, timestamp TEXT NOT NULL, status TEXT NOT NULL, runId TEXT NOT NULL,
FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE
);
`);
db.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '1')");
db.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
db.prepare("INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt, steeringComments) VALUES (?, ?, ?, ?, ?, ?)")
.run(
"FN-100",
"legacy comments",
"todo",
"2025-01-01T00:00:00.000Z",
"2025-01-01T00:00:00.000Z",
JSON.stringify([{ id: "legacy-1", text: "Use TypeScript", author: "user", createdAt: "2025-01-01T00:00:00.000Z" }]),
);
db.init();
const row = db.prepare("SELECT steeringComments, comments FROM tasks WHERE id = 'FN-100'").get() as any;
expect(JSON.parse(row.steeringComments)).toHaveLength(1);
expect(JSON.parse(row.comments)).toEqual([
{
id: "legacy-1",
text: "Use TypeScript",
author: "user",
createdAt: "2025-01-01T00:00:00.000Z",
},
]);
db.close();
});
it("deduplicates overlapping steeringComments and comments during schema upgrade", () => {
tmpDir = makeTmpDir();
const kbDir = join(tmpDir, ".fusion");
const db = new Database(kbDir);
db.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,
steeringComments TEXT DEFAULT '[]',
comments TEXT DEFAULT '[]',
mergeDetails TEXT
);
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
);
CREATE TABLE IF NOT EXISTS activityLog (
id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, type TEXT NOT NULL,
taskId TEXT, taskTitle TEXT, details TEXT NOT NULL, metadata TEXT
);
CREATE TABLE IF NOT EXISTS archivedTasks (id TEXT PRIMARY KEY, data TEXT NOT NULL, archivedAt TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS automations (
id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT,
scheduleType TEXT NOT NULL, cronExpression TEXT NOT NULL, command TEXT NOT NULL,
enabled INTEGER DEFAULT 1, timeoutMs INTEGER, steps TEXT,
nextRunAt TEXT, lastRunAt TEXT, lastRunResult TEXT,
runCount INTEGER DEFAULT 0, runHistory TEXT DEFAULT '[]',
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY, name TEXT NOT NULL, role TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'idle', taskId TEXT,
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
lastHeartbeatAt TEXT, metadata TEXT DEFAULT '{}'
);
CREATE TABLE IF NOT EXISTS agentHeartbeats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agentId TEXT NOT NULL, timestamp TEXT NOT NULL, status TEXT NOT NULL, runId TEXT NOT NULL,
FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE
);
`);
db.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '4')");
db.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
db.prepare("INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt, steeringComments, comments) VALUES (?, ?, ?, ?, ?, ?, ?)")
.run(
"FN-101",
"mixed comments",
"todo",
"2025-01-01T00:00:00.000Z",
"2025-01-01T00:00:00.000Z",
JSON.stringify([{ id: "c1", text: "Keep it simple", author: "user", createdAt: "2025-01-01T00:00:00.000Z" }]),
JSON.stringify([
{ id: "c1", text: "Keep it simple", author: "user", createdAt: "2025-01-01T00:00:00.000Z", updatedAt: "2025-01-02T00:00:00.000Z" },
{ id: "c2", text: "Already unified", author: "alice", createdAt: "2025-01-03T00:00:00.000Z" },
]),
);
db.init();
const row = db.prepare("SELECT comments FROM tasks WHERE id = 'FN-101'").get() as any;
expect(JSON.parse(row.comments)).toEqual([
{ id: "c1", text: "Keep it simple", author: "user", createdAt: "2025-01-01T00:00:00.000Z", updatedAt: "2025-01-02T00:00:00.000Z" },
{ id: "c2", text: "Already unified", author: "alice", createdAt: "2025-01-03T00:00:00.000Z" },
]);
db.close();
});
});
describe("createDatabase factory", () => {