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:
Fusion
2026-04-30 20:33:23 -07:00
committed by gsxdsm
parent d7fdff4c76
commit 97bb80e75c
6 changed files with 199 additions and 6 deletions

View File

@@ -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", () => {

View File

@@ -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");

View File

@@ -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;