From d3574f3c4d308f82a3ca0c595a075302f8e804bb Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 20:05:12 +0200 Subject: [PATCH] fix(sf): guard escalation index migration --- src/resources/extensions/sf/sf-db.ts | 8 +- .../extensions/sf/tests/sf-db.test.ts | 90 ++++++++++++++++++- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/sf/sf-db.ts b/src/resources/extensions/sf/sf-db.ts index 55caae6f4..722e36faa 100644 --- a/src/resources/extensions/sf/sf-db.ts +++ b/src/resources/extensions/sf/sf-db.ts @@ -374,9 +374,11 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void { ) `); - db.exec(` - CREATE INDEX IF NOT EXISTS idx_tasks_escalation_pending ON tasks(milestone_id, slice_id, escalation_pending) - `); + if (columnExists(db, "tasks", "escalation_pending")) { + db.exec(` + CREATE INDEX IF NOT EXISTS idx_tasks_escalation_pending ON tasks(milestone_id, slice_id, escalation_pending) + `); + } db.exec(` CREATE TABLE IF NOT EXISTS verification_evidence ( diff --git a/src/resources/extensions/sf/tests/sf-db.test.ts b/src/resources/extensions/sf/tests/sf-db.test.ts index 0d4aec315..f9e349d42 100644 --- a/src/resources/extensions/sf/tests/sf-db.test.ts +++ b/src/resources/extensions/sf/tests/sf-db.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; +import { DatabaseSync } from "node:sqlite"; import { describe, test } from 'vitest'; import { _getAdapter, @@ -83,8 +84,34 @@ describe("sf.db", () => { .get(); assert.deepStrictEqual( version?.["version"], - 21, - "schema version should be 21", + 23, + "schema version should be 23", + ); + + const sliceColumns = adapter + .prepare("PRAGMA table_info(slices)") + .all() + .map((row) => row["name"]); + assert.ok( + sliceColumns.includes("is_sketch"), + "slices table should include progressive-planning is_sketch column", + ); + assert.ok( + sliceColumns.includes("sketch_scope"), + "slices table should include progressive-planning sketch_scope column", + ); + + const taskColumns = adapter + .prepare("PRAGMA table_info(tasks)") + .all() + .map((row) => row["name"]); + assert.ok( + taskColumns.includes("escalation_pending"), + "tasks table should include escalation_pending column", + ); + assert.ok( + taskColumns.includes("escalation_artifact_path"), + "tasks table should include escalation_artifact_path column", ); // Check tables exist by querying them @@ -153,6 +180,65 @@ describe("sf.db", () => { cleanup(dbPath); }); + test("sf-db: v22 database migrates task escalation columns before index", () => { + const dbPath = tempDbPath(); + const db = new DatabaseSync(dbPath); + db.exec(` + CREATE TABLE schema_version (version INTEGER NOT NULL, applied_at TEXT NOT NULL); + INSERT INTO schema_version VALUES (22, '2026-01-01T00:00:00.000Z'); + CREATE TABLE milestones ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'active', + created_at TEXT NOT NULL, + completed_at TEXT DEFAULT NULL + ); + CREATE TABLE slices ( + milestone_id TEXT NOT NULL, + id TEXT NOT NULL, + title TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + risk TEXT NOT NULL DEFAULT 'medium', + created_at TEXT NOT NULL DEFAULT '', + completed_at TEXT DEFAULT NULL, + PRIMARY KEY (milestone_id, id) + ); + CREATE TABLE tasks ( + milestone_id TEXT NOT NULL, + slice_id TEXT NOT NULL, + id TEXT NOT NULL, + title TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + PRIMARY KEY (milestone_id, slice_id, id) + ); + `); + db.close(); + + openDatabase(dbPath); + const adapter = _getAdapter()!; + const version = adapter + .prepare("SELECT MAX(version) as version FROM schema_version") + .get(); + assert.deepStrictEqual(version?.["version"], 23); + + const taskColumns = adapter + .prepare("PRAGMA table_info(tasks)") + .all() + .map((row) => row["name"]); + assert.ok(taskColumns.includes("escalation_pending")); + assert.ok(taskColumns.includes("escalation_artifact_path")); + assert.ok( + adapter + .prepare( + "SELECT 1 as present FROM sqlite_master WHERE type = 'index' AND name = 'idx_tasks_escalation_pending'", + ) + .get(), + "escalation index should exist after migration", + ); + + cleanup(dbPath); + }); + test("sf-db: insert + get decision", () => { openDatabase(":memory:"); insertDecision({