fix(sf): guard escalation index migration

This commit is contained in:
Mikael Hugo 2026-05-02 20:05:12 +02:00
parent 62dacb6270
commit d3574f3c4d
2 changed files with 93 additions and 5 deletions

View file

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

View file

@ -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({