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]>
This commit is contained in:
Mike SchwarzandClaude Opus 4.6 committed 2026-03-23 03:10:15 -07:00
1 parent f3a97b0277
commit f3742e5366
26 files changed
+5214

No files matched your search

+4116
View File
File diff suppressed because it is too large. Load diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "rigged",
"version": "0.1.0",
"private": true,
"type": "module",
"workspaces": [
"packages/daemon",
"packages/ui"
],
"scripts": {
"build": "npm run build --workspaces",
"test": "npm run test --workspaces",
"lint": "tsc --noEmit --project packages/daemon/tsconfig.json && tsc --noEmit --project packages/ui/tsconfig.json"
},
"engines": {
"node": ">=20"
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "@rigged/daemon",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"test": "vitest run",
"test:watch": "vitest",
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"hono": "^4.7.0",
"@hono/node-server": "^1.13.0",
"better-sqlite3": "^11.8.0",
"ulid": "^2.3.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
}
}
+14
View File
@@ -0,0 +1,14 @@
import Database from "better-sqlite3";
/**
* Create a SQLite database connection.
* Defaults to in-memory for tests. Pass a file path for persistence.
*/
export function createDb(filePath?: string): Database.Database {
const db = new Database(filePath ?? ":memory:");
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
return db;
}
+43
View File
@@ -0,0 +1,43 @@
import type Database from "better-sqlite3";
export interface Migration {
name: string;
sql: string;
}
/**
* Run migrations against a database.
* Tracks applied migrations in a schema_migrations table.
* Skips already-applied migrations. Applies new ones in order.
*/
export function migrate(db: Database.Database, migrations: Migration[]): void {
db.exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
name TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
const applied = new Set(
(
db
.prepare("SELECT name FROM schema_migrations")
.all() as { name: string }[]
).map((r) => r.name)
);
const sorted = [...migrations].sort((a, b) => a.name.localeCompare(b.name));
const applyOne = db.transaction((migration: Migration) => {
db.exec(migration.sql);
db.prepare("INSERT INTO schema_migrations (name) VALUES (?)").run(
migration.name
);
});
for (const migration of sorted) {
if (!applied.has(migration.name)) {
applyOne(migration);
}
}
}
@@ -0,0 +1,51 @@
import type { Migration } from "../migrate.js";
export const coreSchema: Migration = {
name: "001_core_schema.sql",
sql: `
-- rigs: top-level topology container
CREATE TABLE rigs (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- nodes: logical identity within a rig
-- id = opaque DB primary key (ulid). All FKs reference this.
-- logical_id = logical name from rig spec (e.g. "orchestrator"). Human-facing.
CREATE TABLE nodes (
id TEXT PRIMARY KEY,
rig_id TEXT NOT NULL REFERENCES rigs(id) ON DELETE CASCADE,
logical_id TEXT NOT NULL,
role TEXT,
runtime TEXT,
model TEXT,
cwd TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(rig_id, logical_id)
);
-- edges: relationships between nodes
CREATE TABLE edges (
id TEXT PRIMARY KEY,
rig_id TEXT NOT NULL REFERENCES rigs(id) ON DELETE CASCADE,
source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
kind TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Edge integrity: source and target must belong to the same rig.
-- SQLite CHECK cannot cross-reference tables, so we use a trigger.
CREATE TRIGGER edge_same_rig_insert
BEFORE INSERT ON edges
BEGIN
SELECT RAISE(ABORT, 'edge source and target must belong to the same rig')
WHERE (SELECT rig_id FROM nodes WHERE id = NEW.source_id)
!= (SELECT rig_id FROM nodes WHERE id = NEW.target_id);
SELECT RAISE(ABORT, 'edge rig_id must match source node rig_id')
WHERE NEW.rig_id != (SELECT rig_id FROM nodes WHERE id = NEW.source_id);
END;
`,
};
@@ -0,0 +1,30 @@
import type { Migration } from "../migrate.js";
export const bindingsSessionsSchema: Migration = {
name: "002_bindings_sessions.sql",
sql: `
-- bindings: how a node attaches to physical surfaces
-- A node MAY have zero or one binding (unbound = not yet materialized)
CREATE TABLE bindings (
id TEXT PRIMARY KEY,
node_id TEXT NOT NULL UNIQUE REFERENCES nodes(id) ON DELETE CASCADE,
tmux_session TEXT,
tmux_window TEXT,
tmux_pane TEXT,
cmux_workspace TEXT,
cmux_surface TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- sessions: live harness execution state
-- NOTE: resume_token deferred to Phase 2 migration
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
session_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'unknown',
last_seen_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`,
};
@@ -0,0 +1,22 @@
import type { Migration } from "../migrate.js";
export const eventsSchema: Migration = {
name: "003_events.sql",
sql: `
CREATE TABLE events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
rig_id TEXT REFERENCES rigs(id) ON DELETE CASCADE,
-- INTENTIONALLY NOT AN FK to nodes(id).
-- Events are an append-only history log. They must survive node deletion
-- so the full timeline is preserved for replay and audit. An FK with
-- CASCADE would destroy history; SET NULL would lose the node reference.
node_id TEXT,
type TEXT NOT NULL,
payload TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Primary query pattern: "all events for rig X after sequence N" (SSE replay)
CREATE INDEX idx_events_rig_seq ON events(rig_id, seq);
`,
};
+18
View File
@@ -0,0 +1,18 @@
import { serve } from "@hono/node-server";
import { app } from "./server.js";
export function startServer(port?: number) {
const p = port ?? parseInt(process.env["RIGGED_PORT"] ?? "7433", 10);
return serve({ fetch: app.fetch, port: p }, (info) => {
console.log(`rigged daemon listening on http://localhost:${info.port}`);
});
}
// Only start the server when this file is executed directly (not imported).
const isDirectRun =
process.argv[1] &&
import.meta.url === `file://${process.argv[1]}`;
if (isDirectRun) {
startServer();
}
+7
View File
@@ -0,0 +1,7 @@
import { Hono } from "hono";
export const app = new Hono();
app.get("/healthz", (c) => {
return c.json({ status: "ok" });
});
+159
View File
@@ -0,0 +1,159 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import Database from "better-sqlite3";
import { createDb } from "../src/db/connection.js";
import { migrate } from "../src/db/migrate.js";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
describe("createDb", () => {
it("creates an in-memory database with WAL and FK pragmas", () => {
const db = createDb();
const walMode = db.pragma("journal_mode", { simple: true });
// In-memory databases may report "memory" instead of "wal" — that's fine.
// WAL is set but only takes effect on file-backed DBs.
expect(walMode).toBeDefined();
const fkEnabled = db.pragma("foreign_keys", { simple: true });
expect(fkEnabled).toBe(1);
db.close();
});
it("creates a file-backed database when path is given", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "rigged-test-"));
const dbPath = path.join(tmpDir, "test.sqlite");
const db = createDb(dbPath);
expect(fs.existsSync(dbPath)).toBe(true);
db.close();
fs.rmSync(tmpDir, { recursive: true });
});
});
describe("migrate", () => {
let db: Database.Database;
beforeEach(() => {
db = createDb();
});
afterEach(() => {
db.close();
});
it("creates schema_migrations table on first run", () => {
migrate(db, []);
const tables = db
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='schema_migrations'"
)
.all();
expect(tables).toHaveLength(1);
});
it("runs zero migrations without error", () => {
expect(() => migrate(db, [])).not.toThrow();
});
it("applies a trivial migration", () => {
const migrations = [
{
name: "001_test.sql",
sql: "CREATE TABLE test_table (id TEXT PRIMARY KEY);",
},
];
migrate(db, migrations);
const tables = db
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='test_table'"
)
.all();
expect(tables).toHaveLength(1);
});
it("tracks applied migrations in schema_migrations", () => {
const migrations = [
{
name: "001_test.sql",
sql: "CREATE TABLE test_table (id TEXT PRIMARY KEY);",
},
];
migrate(db, migrations);
const applied = db
.prepare("SELECT name FROM schema_migrations ORDER BY name")
.all() as { name: string }[];
expect(applied).toEqual([{ name: "001_test.sql" }]);
});
it("refuses to re-run already-applied migrations", () => {
const migrations = [
{
name: "001_test.sql",
sql: "CREATE TABLE test_table (id TEXT PRIMARY KEY);",
},
];
migrate(db, migrations);
// Running again should not throw (idempotent) and should not re-execute SQL
expect(() => migrate(db, migrations)).not.toThrow();
const applied = db
.prepare("SELECT name FROM schema_migrations ORDER BY name")
.all() as { name: string }[];
expect(applied).toHaveLength(1);
});
it("applies migrations in order", () => {
const migrations = [
{
name: "001_first.sql",
sql: "CREATE TABLE first_table (id TEXT PRIMARY KEY);",
},
{
name: "002_second.sql",
sql: "CREATE TABLE second_table (id TEXT PRIMARY KEY, first_ref TEXT REFERENCES first_table(id));",
},
];
migrate(db, migrations);
const applied = db
.prepare("SELECT name FROM schema_migrations ORDER BY name")
.all() as { name: string }[];
expect(applied).toEqual([
{ name: "001_first.sql" },
{ name: "002_second.sql" },
]);
});
it("only applies new migrations on subsequent runs", () => {
const first = [
{
name: "001_first.sql",
sql: "CREATE TABLE first_table (id TEXT PRIMARY KEY);",
},
];
migrate(db, first);
const both = [
...first,
{
name: "002_second.sql",
sql: "CREATE TABLE second_table (id TEXT PRIMARY KEY);",
},
];
migrate(db, both);
const applied = db
.prepare("SELECT name FROM schema_migrations ORDER BY name")
.all() as { name: string }[];
expect(applied).toHaveLength(2);
const tables = db
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='second_table'"
)
.all();
expect(tables).toHaveLength(1);
});
});
@@ -0,0 +1,177 @@
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();
});
});
});
+200
View File
@@ -0,0 +1,200 @@
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";
describe("001_core_schema", () => {
let db: Database.Database;
beforeEach(() => {
db = createDb();
migrate(db, [coreSchema]);
});
afterEach(() => {
db.close();
});
it("creates rigs, nodes, and edges 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("rigs");
expect(names).toContain("nodes");
expect(names).toContain("edges");
});
it("can insert a rig", () => {
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-1",
"test-rig"
);
const rig = db.prepare("SELECT * FROM rigs WHERE id = ?").get("rig-1") as {
id: string;
name: string;
};
expect(rig.name).toBe("test-rig");
});
it("can insert nodes referencing a rig", () => {
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", "orchestrator", "orchestrator", "claude-code");
const node = db
.prepare("SELECT * FROM nodes WHERE id = ?")
.get("node-1") as { logical_id: string; role: string };
expect(node.logical_id).toBe("orchestrator");
expect(node.role).toBe("orchestrator");
});
it("enforces FK: node must reference valid rig", () => {
expect(() =>
db
.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
)
.run("node-1", "nonexistent-rig", "worker")
).toThrow();
});
it("enforces unique (rig_id, logical_id)", () => {
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-1",
"test-rig"
);
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-1", "rig-1", "worker");
expect(() =>
db
.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
)
.run("node-2", "rig-1", "worker")
).toThrow();
});
it("can insert edges referencing valid nodes", () => {
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-1",
"test-rig"
);
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-1", "rig-1", "orchestrator");
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-2", "rig-1", "worker");
db.prepare(
"INSERT INTO edges (id, rig_id, source_id, target_id, kind) VALUES (?, ?, ?, ?, ?)"
).run("edge-1", "rig-1", "node-1", "node-2", "delegates_to");
const edge = db
.prepare("SELECT * FROM edges WHERE id = ?")
.get("edge-1") as { kind: string };
expect(edge.kind).toBe("delegates_to");
});
it("enforces FK: edge must reference valid nodes", () => {
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-1",
"test-rig"
);
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-1", "rig-1", "orchestrator");
expect(() =>
db
.prepare(
"INSERT INTO edges (id, rig_id, source_id, target_id, kind) VALUES (?, ?, ?, ?, ?)"
)
.run("edge-1", "rig-1", "node-1", "nonexistent", "delegates_to")
).toThrow();
});
it("rejects edges where source and target belong to different rigs", () => {
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-1",
"rig-one"
);
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-2",
"rig-two"
);
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-1", "rig-1", "worker-a");
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-2", "rig-2", "worker-b");
expect(() =>
db
.prepare(
"INSERT INTO edges (id, rig_id, source_id, target_id, kind) VALUES (?, ?, ?, ?, ?)"
)
.run("edge-1", "rig-1", "node-1", "node-2", "delegates_to")
).toThrow(/same rig/);
});
it("rejects edges where rig_id does not match source node rig_id", () => {
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-1",
"rig-one"
);
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-2",
"rig-two"
);
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-1", "rig-1", "worker-a");
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-2", "rig-1", "worker-b");
// Nodes are in rig-1 but edge claims rig-2
expect(() =>
db
.prepare(
"INSERT INTO edges (id, rig_id, source_id, target_id, kind) VALUES (?, ?, ?, ?, ?)"
)
.run("edge-1", "rig-2", "node-1", "node-2", "delegates_to")
).toThrow(/rig_id must match/);
});
it("cascade deletes: deleting a rig removes its nodes and edges", () => {
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-1",
"test-rig"
);
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-1", "rig-1", "orchestrator");
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-2", "rig-1", "worker");
db.prepare(
"INSERT INTO edges (id, rig_id, source_id, target_id, kind) VALUES (?, ?, ?, ?, ?)"
).run("edge-1", "rig-1", "node-1", "node-2", "delegates_to");
db.prepare("DELETE FROM rigs WHERE id = ?").run("rig-1");
const nodes = db.prepare("SELECT * FROM nodes").all();
const edges = db.prepare("SELECT * FROM edges").all();
expect(nodes).toHaveLength(0);
expect(edges).toHaveLength(0);
});
});
+185
View File
@@ -0,0 +1,185 @@
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 { eventsSchema } from "../src/db/migrations/003_events.js";
describe("003_events", () => {
let db: Database.Database;
beforeEach(() => {
db = createDb();
migrate(db, [coreSchema, eventsSchema]);
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-1",
"test-rig"
);
});
afterEach(() => {
db.close();
});
it("creates events table", () => {
const tables = db
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='events'"
)
.all();
expect(tables).toHaveLength(1);
});
it("can insert an event with type and JSON payload", () => {
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-1", "node.added", JSON.stringify({ logicalId: "worker" }));
const event = db.prepare("SELECT * FROM events WHERE rig_id = ?").get(
"rig-1"
) as { seq: number; type: string; payload: string };
expect(event.type).toBe("node.added");
expect(JSON.parse(event.payload)).toEqual({ logicalId: "worker" });
expect(typeof event.seq).toBe("number");
});
it("seq is auto-incrementing and monotonic", () => {
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-1", "event.a", "{}");
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-1", "event.b", "{}");
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-1", "event.c", "{}");
const events = db
.prepare("SELECT seq FROM events ORDER BY seq")
.all() as { seq: number }[];
expect(events).toHaveLength(3);
expect(events[0]!.seq).toBeLessThan(events[1]!.seq);
expect(events[1]!.seq).toBeLessThan(events[2]!.seq);
});
it("primary invariant: query WHERE seq > N returns correct replay set", () => {
for (let i = 0; i < 5; i++) {
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-1", `event.${i}`, JSON.stringify({ index: i }));
}
const allEvents = db
.prepare("SELECT seq FROM events ORDER BY seq")
.all() as { seq: number }[];
const seq2 = allEvents[1]!.seq; // second event
// Replay after seq2: should get events 3, 4, 5 (indices 2, 3, 4)
const replay = db
.prepare(
"SELECT seq, type, payload FROM events WHERE rig_id = ? AND seq > ? ORDER BY seq"
)
.all("rig-1", seq2) as { seq: number; type: string; payload: string }[];
expect(replay).toHaveLength(3);
expect(JSON.parse(replay[0]!.payload)).toEqual({ index: 2 });
expect(JSON.parse(replay[1]!.payload)).toEqual({ index: 3 });
expect(JSON.parse(replay[2]!.payload)).toEqual({ index: 4 });
// Monotonic order
expect(replay[0]!.seq).toBeLessThan(replay[1]!.seq);
expect(replay[1]!.seq).toBeLessThan(replay[2]!.seq);
});
it("query by rig_id filters correctly", () => {
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run(
"rig-2",
"other-rig"
);
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-1", "event.a", "{}");
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-2", "event.b", "{}");
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-1", "event.c", "{}");
const rig1Events = db
.prepare("SELECT * FROM events WHERE rig_id = ? ORDER BY seq")
.all("rig-1") as { type: string }[];
expect(rig1Events).toHaveLength(2);
expect(rig1Events[0]!.type).toBe("event.a");
expect(rig1Events[1]!.type).toBe("event.c");
});
it("node_id is nullable — rig-level events work", () => {
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-1", "rig.created", "{}");
const event = db.prepare("SELECT node_id FROM events WHERE seq = 1").get() as {
node_id: string | null;
};
expect(event.node_id).toBeNull();
});
// -- Explicit contract: events.node_id is intentionally NOT an FK.
// -- Events are an append-only log. They must survive node deletion.
// -- See 003_events.ts for the design rationale.
it("allows event with node_id referencing a nonexistent node (orphan refs intentional)", () => {
// node_id is plain TEXT, not an FK — orphan refs are allowed by design
// because the event log records history, not current state
expect(() =>
db
.prepare(
"INSERT INTO events (rig_id, node_id, type, payload) VALUES (?, ?, ?, ?)"
)
.run("rig-1", "nonexistent-node", "node.added", "{}")
).not.toThrow();
const event = db
.prepare("SELECT node_id FROM events WHERE node_id = ?")
.get("nonexistent-node") as { node_id: string };
expect(event.node_id).toBe("nonexistent-node");
});
it("events survive node deletion (append-only history preserved)", () => {
// Create a node, emit events for it, delete the node — events must remain
db.prepare(
"INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)"
).run("node-1", "rig-1", "worker");
db.prepare(
"INSERT INTO events (rig_id, node_id, type, payload) VALUES (?, ?, ?, ?)"
).run("rig-1", "node-1", "node.added", '{"logicalId":"worker"}');
db.prepare(
"INSERT INTO events (rig_id, node_id, type, payload) VALUES (?, ?, ?, ?)"
).run("rig-1", "node-1", "session.status_changed", '{"status":"running"}');
// Delete the node
db.prepare("DELETE FROM nodes WHERE id = ?").run("node-1");
// Events must still exist with their original node_id intact
const events = db
.prepare("SELECT node_id, type FROM events WHERE node_id = ? ORDER BY seq")
.all("node-1") as { node_id: string; type: string }[];
expect(events).toHaveLength(2);
expect(events[0]!.node_id).toBe("node-1");
expect(events[0]!.type).toBe("node.added");
expect(events[1]!.node_id).toBe("node-1");
expect(events[1]!.type).toBe("session.status_changed");
});
it("cascades on rig delete", () => {
db.prepare(
"INSERT INTO events (rig_id, type, payload) VALUES (?, ?, ?)"
).run("rig-1", "event.a", "{}");
db.prepare("DELETE FROM rigs WHERE id = ?").run("rig-1");
const events = db.prepare("SELECT * FROM events").all();
expect(events).toHaveLength(0);
});
});
+16
View File
@@ -0,0 +1,16 @@
import { describe, it, expect } from "vitest";
import { app } from "../src/server.js";
describe("Hono server", () => {
it("GET /healthz returns 200 with status ok", async () => {
const res = await app.request("/healthz");
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toEqual({ status: "ok" });
});
it("GET /unknown returns 404", async () => {
const res = await app.request("/unknown");
expect(res.status).toBe(404);
});
});
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src"],
"exclude": ["dist", "test"]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["test/**/*.test.ts"],
},
});
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rigged</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@rigged/ui",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc && vite build",
"test": "vitest run",
"test:watch": "vitest",
"dev": "vite"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@testing-library/react": "^16.3.0",
"@testing-library/jest-dom": "^6.6.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.4.0",
"jsdom": "^26.0.0",
"typescript": "^5.7.0",
"vite": "^6.0.0",
"vitest": "^3.0.0"
}
}
+3
View File
@@ -0,0 +1,3 @@
export function App() {
return <div>Rigged</div>;
}
+12
View File
@@ -0,0 +1,12 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App.js";
const root = document.getElementById("root");
if (root) {
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { App } from "../src/App.js";
describe("App", () => {
it("renders without crash", () => {
render(<App />);
expect(screen.getByText("Rigged")).toBeDefined();
});
});
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"noEmit": true
},
"include": ["src"],
"exclude": ["dist", "test"]
}
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://localhost:7433",
changeOrigin: true,
},
},
},
});
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["test/**/*.test.ts", "test/**/*.test.tsx"],
environment: "jsdom",
},
});
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}