open-design/apps/daemon/tests/critique-persistence.test.ts
marco 5dd70b5016
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m3s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 11s
Initial import: open-design source for helix-mind.ai distribution
This repository contains the open-design daemon CLI source code, built
and packaged at https://helix-mind.ai/cli/open-design/latest.tgz for use
by the HelixMind /design slash command.

Licenses: Apache-2.0 (root) + MIT (skills/*)
2026-05-06 20:50:24 +02:00

181 lines
6.6 KiB
TypeScript

import { describe, expect, it, beforeEach } from 'vitest';
import Database from 'better-sqlite3';
import {
migrateCritique,
insertCritiqueRun,
getCritiqueRun,
updateCritiqueRun,
listCritiqueRunsByProject,
deleteCritiqueRun,
reconcileStaleRuns,
CRITIQUE_RUN_STATUSES,
type CritiqueRunRow,
} from '../src/critique/persistence.js';
function freshDb(): Database.Database {
const db = new Database(':memory:');
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
// The persistence module has FKs into projects/conversations; create stubs
// with the columns the FK references actually need.
db.exec(`
CREATE TABLE projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE conversations (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
);
INSERT INTO projects (id, name, created_at, updated_at) VALUES ('p1', 'p1', 0, 0);
INSERT INTO projects (id, name, created_at, updated_at) VALUES ('p2', 'p2', 0, 0);
INSERT INTO conversations (id, project_id, created_at, updated_at) VALUES ('c1', 'p1', 0, 0);
`);
migrateCritique(db);
return db;
}
describe('critique persistence', () => {
let db: Database.Database;
beforeEach(() => { db = freshDb(); });
it('migrate is idempotent', () => {
expect(() => { migrateCritique(db); migrateCritique(db); }).not.toThrow();
const tables = db.prepare(
`SELECT name FROM sqlite_master WHERE type='table' AND name='critique_runs'`,
).all() as Array<{ name: string }>;
expect(tables.length).toBe(1);
});
it('insert + get round-trips a row with rounds payload preserved', () => {
const now = 1700000000000;
const row = insertCritiqueRun(db, {
id: 'crun_1',
projectId: 'p1',
conversationId: 'c1',
artifactPath: '.od/artifacts/crun_1/v1.html',
status: 'shipped',
score: 8.6,
rounds: [
{ n: 1, composite: 6.18, mustFix: 7, decision: 'continue' },
{ n: 2, composite: 7.86, mustFix: 3, decision: 'continue' },
{ n: 3, composite: 8.62, mustFix: 0, decision: 'ship' },
],
transcriptPath: '.od/artifacts/crun_1/transcript.ndjson',
protocolVersion: 1,
createdAt: now,
updatedAt: now,
});
expect(row.id).toBe('crun_1');
expect(row.rounds).toHaveLength(3);
expect(row.rounds[2]?.decision).toBe('ship');
const fetched = getCritiqueRun(db, 'crun_1');
expect(fetched).toEqual(row);
});
it('default rounds is an empty array when not provided', () => {
insertCritiqueRun(db, {
id: 'crun_empty',
projectId: 'p1',
status: 'failed',
protocolVersion: 1,
});
const row = getCritiqueRun(db, 'crun_empty');
expect(row?.rounds).toEqual([]);
});
it('rejects an invalid status at insert time', () => {
expect(() => insertCritiqueRun(db, {
id: 'crun_bad',
projectId: 'p1',
status: 'not_a_status' as never,
protocolVersion: 1,
})).toThrow(RangeError);
});
it('updateCritiqueRun bumps updated_at and applies the patch', async () => {
const r1 = insertCritiqueRun(db, {
id: 'crun_upd',
projectId: 'p1',
status: 'shipped',
protocolVersion: 1,
createdAt: 1,
updatedAt: 1,
});
expect(r1.updatedAt).toBe(1);
const r2 = updateCritiqueRun(db, 'crun_upd', {
score: 9.1,
status: 'shipped',
updatedAt: 1234,
});
expect(r2?.score).toBe(9.1);
expect(r2?.updatedAt).toBe(1234);
});
it('updateCritiqueRun returns null for unknown id', () => {
expect(updateCritiqueRun(db, 'crun_missing', { score: 1 })).toBeNull();
});
it('listCritiqueRunsByProject returns rows ordered by updated_at DESC', () => {
insertCritiqueRun(db, { id: 'a', projectId: 'p1', status: 'shipped', protocolVersion: 1, createdAt: 100, updatedAt: 100 });
insertCritiqueRun(db, { id: 'b', projectId: 'p1', status: 'shipped', protocolVersion: 1, createdAt: 200, updatedAt: 200 });
insertCritiqueRun(db, { id: 'c', projectId: 'p2', status: 'shipped', protocolVersion: 1, createdAt: 300, updatedAt: 300 });
const rows = listCritiqueRunsByProject(db, 'p1');
expect(rows.map(r => r.id)).toEqual(['b', 'a']);
});
it('deleteCritiqueRun removes the row', () => {
insertCritiqueRun(db, { id: 'gone', projectId: 'p1', status: 'shipped', protocolVersion: 1 });
deleteCritiqueRun(db, 'gone');
expect(getCritiqueRun(db, 'gone')).toBeNull();
});
it('CRITIQUE_RUN_STATUSES exposes every public status', () => {
expect(CRITIQUE_RUN_STATUSES).toEqual([
'shipped', 'below_threshold', 'timed_out', 'interrupted',
'degraded', 'failed', 'legacy',
]);
});
it('reconcileStaleRuns flips stale running rows to interrupted with recoveryReason', () => {
db.prepare(
`INSERT INTO critique_runs
(id, project_id, status, rounds_json, protocol_version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
).run('stuck1', 'p1', 'running', '[]', 1, 0, 100);
db.prepare(
`INSERT INTO critique_runs
(id, project_id, status, rounds_json, protocol_version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
).run('stuck2', 'p1', 'running', '[]', 1, 0, 200);
db.prepare(
`INSERT INTO critique_runs
(id, project_id, status, rounds_json, protocol_version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
).run('fresh', 'p1', 'running', '[]', 1, 0, 1_000_000);
const now = 1_000_500;
const flipped = reconcileStaleRuns(db, { staleAfterMs: 1000, now });
expect(flipped).toBe(2);
const r1 = getCritiqueRun(db, 'stuck1');
expect(r1?.status).toBe('interrupted');
const fresh = getCritiqueRun(db, 'fresh');
expect(fresh?.status).toBe('running');
// recoveryReason is on rounds_json (top-level alongside the round entries).
const raw = db.prepare(`SELECT rounds_json AS j FROM critique_runs WHERE id = 'stuck1'`).get() as { j: string };
const parsed = JSON.parse(raw.j);
expect(parsed.recoveryReason).toBe('daemon_restart');
});
it('CASCADEs critique_runs deletion when project is deleted', () => {
insertCritiqueRun(db, { id: 'doomed', projectId: 'p2', status: 'shipped', protocolVersion: 1 });
db.prepare(`DELETE FROM projects WHERE id = ?`).run('p2');
expect(getCritiqueRun(db, 'doomed')).toBeNull();
});
});