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
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/*)
137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
/**
|
|
* Boot-reconcile tests for Critique Theater (Defect 6).
|
|
*
|
|
* Verifies that reconcileStaleRuns is called on daemon boot (simulated here
|
|
* by calling it directly as the server would) and that it flips old 'running'
|
|
* rows to 'interrupted' with recoveryReason='daemon_restart'.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync } from 'node:fs';
|
|
import { rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import Database from 'better-sqlite3';
|
|
import {
|
|
migrateCritique,
|
|
insertCritiqueRun,
|
|
getCritiqueRun,
|
|
reconcileStaleRuns,
|
|
} from '../src/critique/persistence.js';
|
|
import { defaultCritiqueConfig } from '@open-design/contracts/critique';
|
|
|
|
function freshDb(): Database.Database {
|
|
const db = new Database(':memory:');
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('foreign_keys = ON');
|
|
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);
|
|
`);
|
|
migrateCritique(db);
|
|
return db;
|
|
}
|
|
|
|
let tmpDir: string;
|
|
let db: Database.Database;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = mkdtempSync(join(tmpdir(), 'od-boot-reconcile-test-'));
|
|
db = freshDb();
|
|
});
|
|
afterEach(async () => {
|
|
db.close();
|
|
await rm(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('boot reconcile (Defect 6)', () => {
|
|
it('seeds an old running row then flips it to interrupted on simulated boot', () => {
|
|
const cfg = defaultCritiqueConfig();
|
|
const staleAfterMs = cfg.totalTimeoutMs;
|
|
|
|
// Insert a 'running' row whose updated_at is older than staleAfterMs.
|
|
const oldTs = Date.now() - staleAfterMs - 10_000;
|
|
insertCritiqueRun(db, {
|
|
id: 'stale-run-1',
|
|
projectId: 'p1',
|
|
conversationId: null,
|
|
status: 'running',
|
|
protocolVersion: 1,
|
|
createdAt: oldTs,
|
|
updatedAt: oldTs,
|
|
});
|
|
|
|
// Simulate what the daemon boot path does after openDatabase.
|
|
const flipped = reconcileStaleRuns(db, { staleAfterMs });
|
|
expect(flipped).toBe(1);
|
|
|
|
// The row should now be 'interrupted' with recoveryReason='daemon_restart'.
|
|
const row = getCritiqueRun(db, 'stale-run-1');
|
|
expect(row?.status).toBe('interrupted');
|
|
// rounds_json is accessible via row.rounds; the recoveryReason is an internal
|
|
// field not exposed on CritiqueRunRow. Access the raw value via the DB directly.
|
|
const raw = db
|
|
.prepare(`SELECT rounds_json FROM critique_runs WHERE id = ?`)
|
|
.get('stale-run-1') as { rounds_json: string } | undefined;
|
|
const payload = raw ? (JSON.parse(raw.rounds_json) as { recoveryReason?: string }) : {};
|
|
expect(payload.recoveryReason).toBe('daemon_restart');
|
|
});
|
|
|
|
it('does not flip a recently-running row (within staleAfterMs)', () => {
|
|
const cfg = defaultCritiqueConfig();
|
|
const staleAfterMs = cfg.totalTimeoutMs;
|
|
|
|
// Insert a 'running' row whose updated_at is recent (not stale).
|
|
const recentTs = Date.now() - 100;
|
|
insertCritiqueRun(db, {
|
|
id: 'fresh-run-1',
|
|
projectId: 'p1',
|
|
conversationId: null,
|
|
status: 'running',
|
|
protocolVersion: 1,
|
|
createdAt: recentTs,
|
|
updatedAt: recentTs,
|
|
});
|
|
|
|
const flipped = reconcileStaleRuns(db, { staleAfterMs });
|
|
expect(flipped).toBe(0);
|
|
|
|
const row = getCritiqueRun(db, 'fresh-run-1');
|
|
expect(row?.status).toBe('running');
|
|
});
|
|
|
|
it('is idempotent: a second call on the same db flips 0 rows', () => {
|
|
const cfg = defaultCritiqueConfig();
|
|
const staleAfterMs = cfg.totalTimeoutMs;
|
|
|
|
const oldTs = Date.now() - staleAfterMs - 10_000;
|
|
insertCritiqueRun(db, {
|
|
id: 'stale-run-2',
|
|
projectId: 'p1',
|
|
conversationId: null,
|
|
status: 'running',
|
|
protocolVersion: 1,
|
|
createdAt: oldTs,
|
|
updatedAt: oldTs,
|
|
});
|
|
|
|
const first = reconcileStaleRuns(db, { staleAfterMs });
|
|
expect(first).toBe(1);
|
|
|
|
// Second call: the row is now 'interrupted', not 'running', so nothing more to flip.
|
|
const second = reconcileStaleRuns(db, { staleAfterMs });
|
|
expect(second).toBe(0);
|
|
});
|
|
});
|