diff --git a/apps/daemon/src/core/db/migrate.ts b/apps/daemon/src/core/db/migrate.ts index b40ac2d..6505b09 100644 --- a/apps/daemon/src/core/db/migrate.ts +++ b/apps/daemon/src/core/db/migrate.ts @@ -30,6 +30,7 @@ export function runMigrations(db: BazilionDb): void { const sql = readFileSync(join(migrationsDir, file), 'utf8') const tx = db.raw.transaction(() => { + if (version === '0013_harness_slot_layout') ensureHarnessSlotLayout(db) db.raw.exec(sql) db.raw.run('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)', [ version, @@ -39,3 +40,21 @@ export function runMigrations(db: BazilionDb): void { tx() } } + +function ensureHarnessSlotLayout(db: BazilionDb): void { + for (const table of ['harness_template_slots', 'harness_template_revision_slots']) { + const existing = new Set( + db.raw + .query<{ name: string }, []>(`PRAGMA table_info(${table})`) + .all() + .map((column) => column.name), + ) + for (const [name, type] of [ + ['position_x', 'REAL'], + ['position_y', 'REAL'], + ['display_json', 'TEXT'], + ] as const) { + if (!existing.has(name)) db.raw.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`) + } + } +} diff --git a/apps/daemon/src/core/db/migrations/0009_canonical_harness.sql b/apps/daemon/src/core/db/migrations/0009_canonical_harness.sql index 018c594..8f761da 100644 --- a/apps/daemon/src/core/db/migrations/0009_canonical_harness.sql +++ b/apps/daemon/src/core/db/migrations/0009_canonical_harness.sql @@ -33,11 +33,7 @@ CREATE TABLE harness_template_slots ( agent_name TEXT NOT NULL, model_override TEXT, reasoning_level TEXT CHECK (reasoning_level IS NULL OR reasoning_level IN ('off','minimal','low','medium','high','xhigh')), - position_x REAL, - position_y REAL, - display_json TEXT, tombstoned_at INTEGER, - CHECK ((position_x IS NULL) = (position_y IS NULL)), PRIMARY KEY (template_id, slot_id) ); CREATE UNIQUE INDEX harness_template_active_position @@ -74,10 +70,6 @@ CREATE TABLE harness_template_revision_slots ( agent_name TEXT NOT NULL, model_override TEXT, reasoning_level TEXT CHECK (reasoning_level IS NULL OR reasoning_level IN ('off','minimal','low','medium','high','xhigh')), - position_x REAL, - position_y REAL, - display_json TEXT, - CHECK ((position_x IS NULL) = (position_y IS NULL)), PRIMARY KEY (template_id, revision, slot_id), UNIQUE (template_id, revision, position), FOREIGN KEY (template_id, revision) @@ -238,13 +230,13 @@ FROM profile_groups; INSERT INTO harness_template_slots (template_id, slot_id, position, profile_id, agent_name, model_override, reasoning_level, - position_x, position_y, display_json, tombstoned_at) + tombstoned_at) SELECT profile_group_id, lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))), 2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))), 2) || '-' || lower(hex(randomblob(6))), - position, profile_id, agent_name, model_override, reasoning_level, NULL, NULL, NULL, NULL + position, profile_id, agent_name, model_override, reasoning_level, NULL FROM profile_group_members; -- Exact Open Team current policy: every distinct slot pair and four boundary edges per slot. @@ -264,8 +256,7 @@ SELECT template_id, 'slot', slot_id, 'outside_group', '' FROM harness_template_s INSERT INTO harness_template_revisions (template_id, revision, name, user_md, created_at) SELECT id, 1, name, user_md, updated_at FROM harness_templates; INSERT INTO harness_template_revision_slots -SELECT template_id, 1, slot_id, position, profile_id, agent_name, model_override, reasoning_level, - position_x, position_y, display_json +SELECT template_id, 1, slot_id, position, profile_id, agent_name, model_override, reasoning_level FROM harness_template_slots WHERE tombstoned_at IS NULL; INSERT INTO harness_template_revision_edges SELECT template_id, 1, source_kind, source_id, target_kind, target_id diff --git a/apps/daemon/src/core/db/migrations/0013_harness_slot_layout.sql b/apps/daemon/src/core/db/migrations/0013_harness_slot_layout.sql new file mode 100644 index 0000000..fa1409d --- /dev/null +++ b/apps/daemon/src/core/db/migrations/0013_harness_slot_layout.sql @@ -0,0 +1,4 @@ +-- BAZ-012 added optional visual layout metadata to Team-template slots after +-- 0009 had shipped. The migration runner adds each column only when absent so +-- it can converge both older upgraded databases and databases created during +-- the interval in which 0009 incorrectly contained the later columns. diff --git a/apps/daemon/test/core/migrations.test.ts b/apps/daemon/test/core/migrations.test.ts index 66bfeb0..8bff798 100644 --- a/apps/daemon/test/core/migrations.test.ts +++ b/apps/daemon/test/core/migrations.test.ts @@ -1,4 +1,5 @@ import { afterAll, beforeAll, expect, test } from 'vitest' +import { openInMemoryDb } from '../../src/core/db/client.ts' import { runMigrations } from '../../src/core/db/migrate.ts' import { makeTestEnv, type TestEnv } from './helpers.ts' @@ -86,6 +87,40 @@ test('migrations are idempotent', () => { expect(after[0]?.version).toBe('0001_init') }) +test('0013 repairs slot layout columns missing from an already-applied 0009', () => { + const db = openInMemoryDb() + try { + runMigrations(db) + for (const table of ['harness_template_slots', 'harness_template_revision_slots']) { + db.raw.exec(`ALTER TABLE ${table} DROP COLUMN display_json`) + db.raw.exec(`ALTER TABLE ${table} DROP COLUMN position_y`) + db.raw.exec(`ALTER TABLE ${table} DROP COLUMN position_x`) + } + db.raw.run("DELETE FROM schema_migrations WHERE version = '0013_harness_slot_layout'") + + runMigrations(db) + + for (const table of ['harness_template_slots', 'harness_template_revision_slots']) { + const columns = db.raw + .query<{ name: string }, []>(`PRAGMA table_info(${table})`) + .all() + .map((column) => column.name) + expect(columns).toEqual( + expect.arrayContaining(['position_x', 'position_y', 'display_json']), + ) + } + expect( + db.raw + .query<{ count: number }, []>( + "SELECT COUNT(*) count FROM schema_migrations WHERE version = '0013_harness_slot_layout'", + ) + .get()?.count, + ).toBe(1) + } finally { + db.close() + } +}) + test('nested transactions use savepoints without weakening outer rollback', () => { env.db.raw.transaction(() => { env.db.raw.run("INSERT INTO config (key, value, updated_at) VALUES ('outer', '1', 1)")