mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
P3-T00: extended node fields — surface_hint, workspace, restore_policy, package_refs
Migration 007_node_spec_fields adds four columns to nodes table for RigSpec portability: - surface_hint: layout placement hint (e.g. "tab:workers") - workspace: workspace assignment - restore_policy: per-node restore behavior - package_refs: JSON array of package references Node type widened with explicit defaults: surfaceHint/workspace/restorePolicy = null, packageRefs = [] (never undefined). RigRepository addNode persists extended fields. rowToNode parses package_refs JSON to string[]. All boot paths wired: startup.ts + test-app.ts include 007. All test files with own migration lists updated. 10 new tests (5 schema + 4 domain + 1 wiring), 366 total, all passing. QA-approved HIGH risk. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
1 parent
1214f93a1e
commit
3208892d08
14 files changed
+172
-12
No files matched your search
@@ -0,0 +1,12 @@
|
||||
import type { Migration } from "../migrate.js";
|
||||
|
||||
export const nodeSpecFieldsSchema: Migration = {
|
||||
name: "007_node_spec_fields.sql",
|
||||
sql: `
|
||||
-- Extended node fields for RigSpec portability (Phase 3)
|
||||
ALTER TABLE nodes ADD COLUMN surface_hint TEXT;
|
||||
ALTER TABLE nodes ADD COLUMN workspace TEXT;
|
||||
ALTER TABLE nodes ADD COLUMN restore_policy TEXT;
|
||||
ALTER TABLE nodes ADD COLUMN package_refs TEXT; -- JSON array of strings
|
||||
`,
|
||||
};
|
||||
@@ -14,6 +14,10 @@ interface NodeOptions {
|
||||
runtime?: string;
|
||||
model?: string;
|
||||
cwd?: string;
|
||||
surfaceHint?: string;
|
||||
workspace?: string;
|
||||
restorePolicy?: string;
|
||||
packageRefs?: string[];
|
||||
}
|
||||
|
||||
export class RigRepository {
|
||||
@@ -37,7 +41,7 @@ export class RigRepository {
|
||||
const id = ulid();
|
||||
this.db
|
||||
.prepare(
|
||||
"INSERT INTO nodes (id, rig_id, logical_id, role, runtime, model, cwd) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
"INSERT INTO nodes (id, rig_id, logical_id, role, runtime, model, cwd, surface_hint, workspace, restore_policy, package_refs) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)
|
||||
.run(
|
||||
id,
|
||||
@@ -46,7 +50,11 @@ export class RigRepository {
|
||||
opts?.role ?? null,
|
||||
opts?.runtime ?? null,
|
||||
opts?.model ?? null,
|
||||
opts?.cwd ?? null
|
||||
opts?.cwd ?? null,
|
||||
opts?.surfaceHint ?? null,
|
||||
opts?.workspace ?? null,
|
||||
opts?.restorePolicy ?? null,
|
||||
opts?.packageRefs ? JSON.stringify(opts.packageRefs) : null
|
||||
);
|
||||
|
||||
return this.rowToNode(
|
||||
@@ -131,6 +139,10 @@ export class RigRepository {
|
||||
runtime: row.runtime,
|
||||
model: row.model,
|
||||
cwd: row.cwd,
|
||||
surfaceHint: row.surface_hint ?? null,
|
||||
workspace: row.workspace ?? null,
|
||||
restorePolicy: row.restore_policy ?? null,
|
||||
packageRefs: row.package_refs ? JSON.parse(row.package_refs) as string[] : [],
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
}
|
||||
@@ -177,6 +189,10 @@ interface NodeRow {
|
||||
runtime: string | null;
|
||||
model: string | null;
|
||||
cwd: string | null;
|
||||
surface_hint: string | null;
|
||||
workspace: string | null;
|
||||
restore_policy: string | null;
|
||||
package_refs: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ export interface Node {
|
||||
runtime: string | null;
|
||||
model: string | null;
|
||||
cwd: string | null;
|
||||
surfaceHint: string | null;
|
||||
workspace: string | null;
|
||||
restorePolicy: string | null;
|
||||
packageRefs: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { createApp, type AppDeps } from "./server.js";
|
||||
import { snapshotsSchema } from "./db/migrations/004_snapshots.js";
|
||||
import { checkpointsSchema } from "./db/migrations/005_checkpoints.js";
|
||||
import { resumeMetadataSchema } from "./db/migrations/006_resume_metadata.js";
|
||||
import { nodeSpecFieldsSchema } from "./db/migrations/007_node_spec_fields.js";
|
||||
|
||||
interface DaemonOptions {
|
||||
dbPath?: string;
|
||||
@@ -44,7 +45,7 @@ interface DaemonResult {
|
||||
export async function createDaemon(opts?: DaemonOptions): Promise<DaemonResult> {
|
||||
const dbPath = opts?.dbPath ?? ":memory:";
|
||||
const db = createDb(dbPath);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
|
||||
|
||||
const rigRepo = new RigRepository(db);
|
||||
const sessionRegistry = new SessionRegistry(db);
|
||||
|
||||
@@ -4,11 +4,12 @@ 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 { checkpointsSchema } from "../src/db/migrations/005_checkpoints.js";
|
||||
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
|
||||
import { CheckpointStore } from "../src/domain/checkpoint-store.js";
|
||||
|
||||
function setupDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, checkpointsSchema]);
|
||||
migrate(db, [coreSchema, checkpointsSchema, nodeSpecFieldsSchema]);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { eventsSchema } from "../../src/db/migrations/003_events.js";
|
||||
import { snapshotsSchema } from "../../src/db/migrations/004_snapshots.js";
|
||||
import { checkpointsSchema } from "../../src/db/migrations/005_checkpoints.js";
|
||||
import { resumeMetadataSchema } from "../../src/db/migrations/006_resume_metadata.js";
|
||||
import { nodeSpecFieldsSchema } from "../../src/db/migrations/007_node_spec_fields.js";
|
||||
import { RigRepository } from "../../src/domain/rig-repository.js";
|
||||
import { SessionRegistry } from "../../src/domain/session-registry.js";
|
||||
import { EventBus } from "../../src/domain/event-bus.js";
|
||||
@@ -25,7 +26,7 @@ import { createApp } from "../../src/server.js";
|
||||
|
||||
export function createFullTestDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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";
|
||||
import { eventsSchema } from "../src/db/migrations/003_events.js";
|
||||
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
|
||||
import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
import { SessionRegistry } from "../src/domain/session-registry.js";
|
||||
import { EventBus } from "../src/domain/event-bus.js";
|
||||
@@ -14,7 +15,7 @@ import type { PersistedEvent } from "../src/domain/types.js";
|
||||
|
||||
function setupDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, nodeSpecFieldsSchema]);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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";
|
||||
import { eventsSchema } from "../src/db/migrations/003_events.js";
|
||||
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
|
||||
import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
import { SessionRegistry } from "../src/domain/session-registry.js";
|
||||
import { EventBus } from "../src/domain/event-bus.js";
|
||||
@@ -14,7 +15,7 @@ import type { PersistedEvent } from "../src/domain/types.js";
|
||||
|
||||
function setupDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, nodeSpecFieldsSchema]);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { eventsSchema } from "../src/db/migrations/003_events.js";
|
||||
import { snapshotsSchema } from "../src/db/migrations/004_snapshots.js";
|
||||
import { checkpointsSchema } from "../src/db/migrations/005_checkpoints.js";
|
||||
import { resumeMetadataSchema } from "../src/db/migrations/006_resume_metadata.js";
|
||||
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
|
||||
import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
import { SessionRegistry } from "../src/domain/session-registry.js";
|
||||
import { EventBus } from "../src/domain/event-bus.js";
|
||||
@@ -27,7 +28,7 @@ import type { PersistedEvent, Snapshot } from "../src/domain/types.js";
|
||||
|
||||
function setupDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ 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";
|
||||
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
|
||||
import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
|
||||
function setupDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, nodeSpecFieldsSchema]);
|
||||
return db;
|
||||
}
|
||||
|
||||
@@ -138,4 +139,61 @@ describe("RigRepository", () => {
|
||||
expect(repo.getRig(rig.id)).toBeNull();
|
||||
expect(repo.listRigs()).toHaveLength(0);
|
||||
});
|
||||
|
||||
// -- P3-T00: Extended node fields --
|
||||
|
||||
it("addNode persists extended fields", () => {
|
||||
const rig = repo.createRig("test-rig");
|
||||
const node = repo.addNode(rig.id, "worker", {
|
||||
role: "worker",
|
||||
runtime: "claude-code",
|
||||
surfaceHint: "tab:workers",
|
||||
workspace: "review",
|
||||
restorePolicy: "checkpoint_only",
|
||||
packageRefs: ["github:example/pkg@v1", "local:./my-pkg"],
|
||||
});
|
||||
|
||||
expect(node.surfaceHint).toBe("tab:workers");
|
||||
expect(node.workspace).toBe("review");
|
||||
expect(node.restorePolicy).toBe("checkpoint_only");
|
||||
expect(node.packageRefs).toEqual(["github:example/pkg@v1", "local:./my-pkg"]);
|
||||
});
|
||||
|
||||
it("getRig returns nodes with extended fields (parsed packageRefs)", () => {
|
||||
const rig = repo.createRig("test-rig");
|
||||
repo.addNode(rig.id, "worker", {
|
||||
surfaceHint: "tab:main",
|
||||
packageRefs: ["pkg-a", "pkg-b"],
|
||||
});
|
||||
|
||||
const full = repo.getRig(rig.id);
|
||||
const node = full!.nodes[0]!;
|
||||
expect(node.surfaceHint).toBe("tab:main");
|
||||
expect(node.packageRefs).toEqual(["pkg-a", "pkg-b"]);
|
||||
expect(Array.isArray(node.packageRefs)).toBe(true);
|
||||
});
|
||||
|
||||
it("addNode with no extended fields: surfaceHint=null, workspace=null, restorePolicy=null, packageRefs=[]", () => {
|
||||
const rig = repo.createRig("test-rig");
|
||||
const node = repo.addNode(rig.id, "worker");
|
||||
|
||||
expect(node.surfaceHint).toBeNull();
|
||||
expect(node.workspace).toBeNull();
|
||||
expect(node.restorePolicy).toBeNull();
|
||||
expect(node.packageRefs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Wiring regression", () => {
|
||||
it("createFullTestDb schema includes 007 columns", async () => {
|
||||
const { createFullTestDb } = await import("./helpers/test-app.js");
|
||||
const db = createFullTestDb();
|
||||
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
|
||||
const names = cols.map((c) => c.name);
|
||||
expect(names).toContain("surface_hint");
|
||||
expect(names).toContain("workspace");
|
||||
expect(names).toContain("restore_policy");
|
||||
expect(names).toContain("package_refs");
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
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 { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
|
||||
|
||||
describe("007_node_spec_fields", () => {
|
||||
let db: Database.Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createDb();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("migration adds surface_hint column", () => {
|
||||
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
|
||||
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
|
||||
expect(cols.map((c) => c.name)).toContain("surface_hint");
|
||||
});
|
||||
|
||||
it("migration adds workspace column", () => {
|
||||
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
|
||||
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
|
||||
expect(cols.map((c) => c.name)).toContain("workspace");
|
||||
});
|
||||
|
||||
it("migration adds restore_policy column", () => {
|
||||
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
|
||||
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
|
||||
expect(cols.map((c) => c.name)).toContain("restore_policy");
|
||||
});
|
||||
|
||||
it("migration adds package_refs column", () => {
|
||||
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
|
||||
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
|
||||
expect(cols.map((c) => c.name)).toContain("package_refs");
|
||||
});
|
||||
|
||||
it("existing nodes get NULL for all new columns", () => {
|
||||
migrate(db, [coreSchema]);
|
||||
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run("rig-1", "r01");
|
||||
db.prepare("INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)").run("node-1", "rig-1", "worker");
|
||||
|
||||
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
|
||||
|
||||
const node = db.prepare("SELECT surface_hint, workspace, restore_policy, package_refs FROM nodes WHERE id = ?").get("node-1") as {
|
||||
surface_hint: string | null;
|
||||
workspace: string | null;
|
||||
restore_policy: string | null;
|
||||
package_refs: string | null;
|
||||
};
|
||||
expect(node.surface_hint).toBeNull();
|
||||
expect(node.workspace).toBeNull();
|
||||
expect(node.restore_policy).toBeNull();
|
||||
expect(node.package_refs).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -5,11 +5,12 @@ 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";
|
||||
import { resumeMetadataSchema } from "../src/db/migrations/006_resume_metadata.js";
|
||||
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
|
||||
import { SessionRegistry } from "../src/domain/session-registry.js";
|
||||
|
||||
function setupDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, resumeMetadataSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { eventsSchema } from "../src/db/migrations/003_events.js";
|
||||
import { snapshotsSchema } from "../src/db/migrations/004_snapshots.js";
|
||||
import { checkpointsSchema } from "../src/db/migrations/005_checkpoints.js";
|
||||
import { resumeMetadataSchema } from "../src/db/migrations/006_resume_metadata.js";
|
||||
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
|
||||
import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
import { SessionRegistry } from "../src/domain/session-registry.js";
|
||||
import { EventBus } from "../src/domain/event-bus.js";
|
||||
@@ -18,7 +19,7 @@ import type { PersistedEvent } from "../src/domain/types.js";
|
||||
|
||||
function setupDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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";
|
||||
import { eventsSchema } from "../src/db/migrations/003_events.js";
|
||||
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
|
||||
import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
import { SessionRegistry } from "../src/domain/session-registry.js";
|
||||
import type { CmuxTransportFactory } from "../src/adapters/cmux.js";
|
||||
@@ -15,7 +16,7 @@ import type { ExecFn } from "../src/adapters/tmux.js";
|
||||
|
||||
function seedDbWithStaleSessions(dbPath: string, rigs: { rigName: string; logicalId: string; sessionName: string }[]) {
|
||||
const db = createDb(dbPath);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, nodeSpecFieldsSchema]);
|
||||
const rigRepo = new RigRepository(db);
|
||||
const sessionRegistry = new SessionRegistry(db);
|
||||
|
||||
|
||||
Reference in new issue
Block a user