mirror of
https://github.com/Heretek-AI/openrig.git
synced 2026-11-07 11:06:04 -10:00
Scaffold (T01-T04): - npm workspaces monorepo with @rigged/daemon and @rigged/ui - Shared tsconfig.base.json with strict mode - Hono server with /healthz endpoint, guarded index.ts (no side-effect on import) - SQLite connection factory (WAL, FK enforcement) and migration runner - Vite/React shell with dev proxy to daemon Schema (T05-T07, QA-approved HIGH risk): - 001_core_schema: rigs, nodes (id=opaque PK, logical_id=spec name), edges with same-rig trigger enforcing source/target/rig_id consistency - 002_bindings_sessions: bindings (optional, 1:1 with node) and sessions (status includes detached). No resume_token (Phase 2). - 003_events: append-only event log with seq AUTOINCREMENT for SSE replay. events.node_id is intentionally NOT an FK — events survive node deletion. 42 tests, all passing. tsc clean in both packages. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
178 lines
5.7 KiB
TypeScript
178 lines
5.7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import type Database from "better-sqlite3";
|
|
import { createDb } from "../src/db/connection.js";
|
|
import { migrate } from "../src/db/migrate.js";
|
|
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
|
|
import { bindingsSessionsSchema } from "../src/db/migrations/002_bindings_sessions.js";
|
|
|
|
function seedRigWithNode(db: Database.Database) {
|
|
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
|
|
"rig-1",
|
|
"test-rig"
|
|
);
|
|
db.prepare(
|
|
"INSERT INTO nodes (id, rig_id, logical_id, role, runtime) VALUES (?, ?, ?, ?, ?)"
|
|
).run("node-1", "rig-1", "dev1-impl", "worker", "claude-code");
|
|
}
|
|
|
|
describe("002_bindings_sessions", () => {
|
|
let db: Database.Database;
|
|
|
|
beforeEach(() => {
|
|
db = createDb();
|
|
migrate(db, [coreSchema, bindingsSessionsSchema]);
|
|
});
|
|
|
|
afterEach(() => {
|
|
db.close();
|
|
});
|
|
|
|
it("creates bindings and sessions tables", () => {
|
|
const tables = db
|
|
.prepare(
|
|
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
|
)
|
|
.all() as { name: string }[];
|
|
const names = tables.map((t) => t.name);
|
|
expect(names).toContain("bindings");
|
|
expect(names).toContain("sessions");
|
|
});
|
|
|
|
describe("bindings", () => {
|
|
it("can insert a binding for a node", () => {
|
|
seedRigWithNode(db);
|
|
db.prepare(
|
|
"INSERT INTO bindings (id, node_id, tmux_session) VALUES (?, ?, ?)"
|
|
).run("bind-1", "node-1", "r01-dev1-impl");
|
|
|
|
const binding = db
|
|
.prepare("SELECT * FROM bindings WHERE node_id = ?")
|
|
.get("node-1") as { tmux_session: string };
|
|
expect(binding.tmux_session).toBe("r01-dev1-impl");
|
|
});
|
|
|
|
it("binding is optional — node can exist without binding", () => {
|
|
seedRigWithNode(db);
|
|
const binding = db
|
|
.prepare("SELECT * FROM bindings WHERE node_id = ?")
|
|
.get("node-1");
|
|
expect(binding).toBeUndefined();
|
|
});
|
|
|
|
it("enforces one binding per node (UNIQUE node_id)", () => {
|
|
seedRigWithNode(db);
|
|
db.prepare(
|
|
"INSERT INTO bindings (id, node_id, tmux_session) VALUES (?, ?, ?)"
|
|
).run("bind-1", "node-1", "r01-dev1-impl");
|
|
|
|
expect(() =>
|
|
db
|
|
.prepare(
|
|
"INSERT INTO bindings (id, node_id, tmux_session) VALUES (?, ?, ?)"
|
|
)
|
|
.run("bind-2", "node-1", "r01-dev1-impl-2")
|
|
).toThrow();
|
|
});
|
|
|
|
it("allows updating a binding", () => {
|
|
seedRigWithNode(db);
|
|
db.prepare(
|
|
"INSERT INTO bindings (id, node_id, tmux_session) VALUES (?, ?, ?)"
|
|
).run("bind-1", "node-1", "r01-dev1-impl");
|
|
|
|
db.prepare(
|
|
"UPDATE bindings SET cmux_surface = ? WHERE node_id = ?"
|
|
).run("surface-42", "node-1");
|
|
|
|
const binding = db
|
|
.prepare("SELECT * FROM bindings WHERE node_id = ?")
|
|
.get("node-1") as { cmux_surface: string | null };
|
|
expect(binding.cmux_surface).toBe("surface-42");
|
|
});
|
|
|
|
it("cascades on node delete", () => {
|
|
seedRigWithNode(db);
|
|
db.prepare(
|
|
"INSERT INTO bindings (id, node_id, tmux_session) VALUES (?, ?, ?)"
|
|
).run("bind-1", "node-1", "r01-dev1-impl");
|
|
|
|
db.prepare("DELETE FROM nodes WHERE id = ?").run("node-1");
|
|
const bindings = db.prepare("SELECT * FROM bindings").all();
|
|
expect(bindings).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("sessions", () => {
|
|
it("can insert a session for a node", () => {
|
|
seedRigWithNode(db);
|
|
db.prepare(
|
|
"INSERT INTO sessions (id, node_id, session_name, status) VALUES (?, ?, ?, ?)"
|
|
).run("sess-1", "node-1", "r01-dev1-impl", "running");
|
|
|
|
const session = db
|
|
.prepare("SELECT * FROM sessions WHERE id = ?")
|
|
.get("sess-1") as { session_name: string; status: string };
|
|
expect(session.session_name).toBe("r01-dev1-impl");
|
|
expect(session.status).toBe("running");
|
|
});
|
|
|
|
it("defaults status to unknown", () => {
|
|
seedRigWithNode(db);
|
|
db.prepare(
|
|
"INSERT INTO sessions (id, node_id, session_name) VALUES (?, ?, ?)"
|
|
).run("sess-1", "node-1", "r01-dev1-impl");
|
|
|
|
const session = db
|
|
.prepare("SELECT * FROM sessions WHERE id = ?")
|
|
.get("sess-1") as { status: string };
|
|
expect(session.status).toBe("unknown");
|
|
});
|
|
|
|
it("supports detached status", () => {
|
|
seedRigWithNode(db);
|
|
db.prepare(
|
|
"INSERT INTO sessions (id, node_id, session_name, status) VALUES (?, ?, ?, ?)"
|
|
).run("sess-1", "node-1", "r01-dev1-impl", "running");
|
|
|
|
db.prepare("UPDATE sessions SET status = ? WHERE id = ?").run(
|
|
"detached",
|
|
"sess-1"
|
|
);
|
|
|
|
const session = db
|
|
.prepare("SELECT * FROM sessions WHERE id = ?")
|
|
.get("sess-1") as { status: string };
|
|
expect(session.status).toBe("detached");
|
|
});
|
|
|
|
it("cascades on node delete", () => {
|
|
seedRigWithNode(db);
|
|
db.prepare(
|
|
"INSERT INTO sessions (id, node_id, session_name) VALUES (?, ?, ?)"
|
|
).run("sess-1", "node-1", "r01-dev1-impl");
|
|
|
|
db.prepare("DELETE FROM nodes WHERE id = ?").run("node-1");
|
|
const sessions = db.prepare("SELECT * FROM sessions").all();
|
|
expect(sessions).toHaveLength(0);
|
|
});
|
|
|
|
it("does NOT include resume_token (deferred to Phase 2)", () => {
|
|
const columns = db
|
|
.prepare("PRAGMA table_info(sessions)")
|
|
.all() as { name: string }[];
|
|
const columnNames = columns.map((c) => c.name);
|
|
expect(columnNames).not.toContain("resume_token");
|
|
});
|
|
|
|
it("enforces FK: session must reference valid node", () => {
|
|
expect(() =>
|
|
db
|
|
.prepare(
|
|
"INSERT INTO sessions (id, node_id, session_name) VALUES (?, ?, ?)"
|
|
)
|
|
.run("sess-1", "nonexistent", "r01-dev1-impl")
|
|
).toThrow();
|
|
});
|
|
});
|
|
});
|