feat(FN-3034): add legacy routines agentId compatibility and migration back
Merges FN-3009 (remote settings migrated to global scope with updated schema and docs), FN-3034 (legacy routines compatibility layer: agentId backfill on load and schema validation for old format), and FN-3039 (Nerd Font glyph asset bundled into the terminal with CSS prioritization). Core settings s Fusion-Task-Id: FN-3034
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
runBackupCommand,
|
||||
syncBackupRoutine,
|
||||
} from "../backup.js";
|
||||
import { Database } from "../db.js";
|
||||
import { RoutineStore } from "../routine-store.js";
|
||||
import type { ProjectSettings } from "../types.js";
|
||||
|
||||
@@ -522,6 +523,52 @@ describe("syncBackupRoutine", () => {
|
||||
|
||||
expect(await routineStore.listRoutines()).toEqual([]);
|
||||
});
|
||||
|
||||
it("creates backup routine after upgrading legacy routines schema missing agentId", async () => {
|
||||
const diskDir = mkdtempSync(join(tmpdir(), "kb-backup-routine-legacy-"));
|
||||
const db = new Database(join(diskDir, ".fusion"));
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value 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 routines (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
triggerType TEXT NOT NULL,
|
||||
triggerConfig TEXT NOT NULL,
|
||||
command TEXT,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '55')");
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
|
||||
db.close();
|
||||
|
||||
const diskRoutineStore = new RoutineStore(diskDir);
|
||||
await diskRoutineStore.init();
|
||||
|
||||
await expect(syncBackupRoutine(diskRoutineStore, {
|
||||
...baseSettings,
|
||||
autoBackupEnabled: true,
|
||||
autoBackupSchedule: "0 1 * * *",
|
||||
})).resolves.toBeDefined();
|
||||
|
||||
const routines = await diskRoutineStore.listRoutines();
|
||||
expect(routines).toHaveLength(1);
|
||||
expect(routines[0]?.name).toBe("Database Backup");
|
||||
expect(routines[0]?.agentId).toBe("");
|
||||
|
||||
await rm(diskDir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("runBackupCommand", () => {
|
||||
|
||||
@@ -965,6 +965,50 @@ describe("schema migrations", () => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("backfills legacy routines table missing agentId with safe defaults", () => {
|
||||
tmpDir = makeTmpDir();
|
||||
const fusionDir = join(tmpDir, ".fusion");
|
||||
const db = new Database(fusionDir);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value 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 routines (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
triggerType TEXT NOT NULL,
|
||||
triggerConfig TEXT NOT NULL,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '55')");
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
|
||||
db.exec(`
|
||||
INSERT INTO routines (id, name, description, triggerType, triggerConfig, enabled, createdAt, updatedAt)
|
||||
VALUES ('routine-1', 'Database Backup', 'legacy row', 'cron', '{}', 1, '2026-01-01', '2026-01-01')
|
||||
`);
|
||||
|
||||
db.init();
|
||||
|
||||
const columns = db.prepare("PRAGMA table_info(routines)").all() as Array<{ name: string }>;
|
||||
expect(columns.map((column) => column.name)).toContain("agentId");
|
||||
|
||||
const row = db.prepare("SELECT agentId FROM routines WHERE id = 'routine-1'").get() as { agentId: string | null };
|
||||
expect(row.agentId).toBe("");
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("migrates v50 databases by adding chat message attachments column", () => {
|
||||
tmpDir = makeTmpDir();
|
||||
const fusionDir = join(tmpDir, ".fusion");
|
||||
|
||||
@@ -565,8 +565,6 @@ CREATE TABLE IF NOT EXISTS routines (
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idxRoutinesNextRunAt ON routines(nextRunAt);
|
||||
CREATE INDEX IF NOT EXISTS idxRoutinesEnabled ON routines(enabled);
|
||||
|
||||
-- Roadmap persistence tables (FN-1690)
|
||||
-- Standalone roadmap: Roadmap → RoadmapMilestone → RoadmapFeature
|
||||
@@ -851,6 +849,9 @@ export class Database {
|
||||
// Run schema migrations
|
||||
this.migrate();
|
||||
|
||||
// Compatibility backfills that must run even when schemaVersion is current.
|
||||
this.ensureRoutinesSchemaCompatibility();
|
||||
|
||||
// Seed config row idempotently with default settings
|
||||
const configNow = new Date().toISOString();
|
||||
this.db.exec(
|
||||
@@ -869,6 +870,41 @@ export class Database {
|
||||
* Column additions use `hasColumn()` so they are idempotent — safe to
|
||||
* re-run even if a previous migration partially applied.
|
||||
*/
|
||||
/**
|
||||
* Applies idempotent compatibility fixes for legacy routines table shapes.
|
||||
*
|
||||
* Some older databases contain `routines` without `agentId`, or with NULL
|
||||
* agent IDs from earlier table definitions. `RoutineStore.rowToRoutine()` and
|
||||
* backup routine sync expect a safe string value, so normalize to ''.
|
||||
*/
|
||||
private ensureRoutinesSchemaCompatibility(): void {
|
||||
if (!this.hasTable("routines")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addColumnIfMissing("routines", "agentId", "TEXT NOT NULL DEFAULT ''");
|
||||
this.addColumnIfMissing("routines", "command", "TEXT");
|
||||
this.addColumnIfMissing("routines", "steps", "TEXT");
|
||||
this.addColumnIfMissing("routines", "timeoutMs", "INTEGER");
|
||||
this.addColumnIfMissing("routines", "catchUpPolicy", "TEXT NOT NULL DEFAULT 'run_one'");
|
||||
this.addColumnIfMissing("routines", "executionPolicy", "TEXT NOT NULL DEFAULT 'queue'");
|
||||
this.addColumnIfMissing("routines", "catchUpLimit", "INTEGER DEFAULT 5");
|
||||
this.addColumnIfMissing("routines", "lastRunAt", "TEXT");
|
||||
this.addColumnIfMissing("routines", "lastRunResult", "TEXT");
|
||||
this.addColumnIfMissing("routines", "nextRunAt", "TEXT");
|
||||
this.addColumnIfMissing("routines", "runCount", "INTEGER DEFAULT 0");
|
||||
this.addColumnIfMissing("routines", "runHistory", "TEXT DEFAULT '[]'");
|
||||
this.addColumnIfMissing("routines", "scope", "TEXT DEFAULT 'project'");
|
||||
this.addColumnIfMissing("routines", "enabled", "INTEGER DEFAULT 1");
|
||||
|
||||
this.db.exec("UPDATE routines SET agentId = '' WHERE agentId IS NULL");
|
||||
this.db.exec("UPDATE routines SET scope = 'project' WHERE scope IS NULL OR TRIM(scope) = ''");
|
||||
|
||||
this.db.exec("CREATE INDEX IF NOT EXISTS idxRoutinesNextRunAt ON routines(nextRunAt)");
|
||||
this.db.exec("CREATE INDEX IF NOT EXISTS idxRoutinesEnabled ON routines(enabled)");
|
||||
this.db.exec("CREATE INDEX IF NOT EXISTS idxRoutinesScope ON routines(scope)");
|
||||
}
|
||||
|
||||
private migrate(): void {
|
||||
const version = this.getSchemaVersion() || 1;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user