Files
openrig/packages/daemon/test/schema-bindings.test.ts
T
Mike SchwarzandClaude Opus 4.6 f3742e5366 T01-T07: monorepo scaffold, Hono server, SQLite migrations, Vite/React shell
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]>
2026-03-23 03:10:15 -07:00

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();
});
});
});