From 3208892d08ce477acd454fdccdb1509bfbb71f4d Mon Sep 17 00:00:00 2001 From: Mike Schwarz Date: Mon, 23 Mar 2026 23:53:21 -0700 Subject: [PATCH] =?UTF-8?q?P3-T00:=20extended=20node=20fields=20=E2=80=94?= =?UTF-8?q?=20surface=5Fhint,=20workspace,=20restore=5Fpolicy,=20package?= =?UTF-8?q?=5Frefs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/db/migrations/007_node_spec_fields.ts | 12 ++++ packages/daemon/src/domain/rig-repository.ts | 20 +++++- packages/daemon/src/domain/types.ts | 4 ++ packages/daemon/src/startup.ts | 3 +- packages/daemon/test/checkpoint-store.test.ts | 3 +- packages/daemon/test/helpers/test-app.ts | 3 +- packages/daemon/test/node-launcher.test.ts | 3 +- packages/daemon/test/reconciler.test.ts | 3 +- .../daemon/test/restore-orchestrator.test.ts | 3 +- packages/daemon/test/rig-repository.test.ts | 60 +++++++++++++++++- .../test/schema-node-spec-fields.test.ts | 61 +++++++++++++++++++ packages/daemon/test/session-registry.test.ts | 3 +- packages/daemon/test/snapshot-capture.test.ts | 3 +- packages/daemon/test/startup.test.ts | 3 +- 14 files changed, 172 insertions(+), 12 deletions(-) create mode 100644 packages/daemon/src/db/migrations/007_node_spec_fields.ts create mode 100644 packages/daemon/test/schema-node-spec-fields.test.ts diff --git a/packages/daemon/src/db/migrations/007_node_spec_fields.ts b/packages/daemon/src/db/migrations/007_node_spec_fields.ts new file mode 100644 index 000000000..32ff10a8f --- /dev/null +++ b/packages/daemon/src/db/migrations/007_node_spec_fields.ts @@ -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 + `, +}; diff --git a/packages/daemon/src/domain/rig-repository.ts b/packages/daemon/src/domain/rig-repository.ts index d8fda7035..4b974e4cc 100644 --- a/packages/daemon/src/domain/rig-repository.ts +++ b/packages/daemon/src/domain/rig-repository.ts @@ -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; } diff --git a/packages/daemon/src/domain/types.ts b/packages/daemon/src/domain/types.ts index a7626c9ad..0a6fec3ef 100644 --- a/packages/daemon/src/domain/types.ts +++ b/packages/daemon/src/domain/types.ts @@ -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; } diff --git a/packages/daemon/src/startup.ts b/packages/daemon/src/startup.ts index dd0ca9733..9fad62828 100644 --- a/packages/daemon/src/startup.ts +++ b/packages/daemon/src/startup.ts @@ -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 { 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); diff --git a/packages/daemon/test/checkpoint-store.test.ts b/packages/daemon/test/checkpoint-store.test.ts index b8fcfb5c9..9b7e20b9d 100644 --- a/packages/daemon/test/checkpoint-store.test.ts +++ b/packages/daemon/test/checkpoint-store.test.ts @@ -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; } diff --git a/packages/daemon/test/helpers/test-app.ts b/packages/daemon/test/helpers/test-app.ts index 5cbda07b4..3d3207c4b 100644 --- a/packages/daemon/test/helpers/test-app.ts +++ b/packages/daemon/test/helpers/test-app.ts @@ -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; } diff --git a/packages/daemon/test/node-launcher.test.ts b/packages/daemon/test/node-launcher.test.ts index 80f2bec93..cf84b02de 100644 --- a/packages/daemon/test/node-launcher.test.ts +++ b/packages/daemon/test/node-launcher.test.ts @@ -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; } diff --git a/packages/daemon/test/reconciler.test.ts b/packages/daemon/test/reconciler.test.ts index 4c9241930..001c9c66e 100644 --- a/packages/daemon/test/reconciler.test.ts +++ b/packages/daemon/test/reconciler.test.ts @@ -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; } diff --git a/packages/daemon/test/restore-orchestrator.test.ts b/packages/daemon/test/restore-orchestrator.test.ts index e3a234a13..e3c62e1aa 100644 --- a/packages/daemon/test/restore-orchestrator.test.ts +++ b/packages/daemon/test/restore-orchestrator.test.ts @@ -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; } diff --git a/packages/daemon/test/rig-repository.test.ts b/packages/daemon/test/rig-repository.test.ts index a92028fc6..0617bdc06 100644 --- a/packages/daemon/test/rig-repository.test.ts +++ b/packages/daemon/test/rig-repository.test.ts @@ -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(); + }); }); diff --git a/packages/daemon/test/schema-node-spec-fields.test.ts b/packages/daemon/test/schema-node-spec-fields.test.ts new file mode 100644 index 000000000..6b05baede --- /dev/null +++ b/packages/daemon/test/schema-node-spec-fields.test.ts @@ -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(); + }); +}); diff --git a/packages/daemon/test/session-registry.test.ts b/packages/daemon/test/session-registry.test.ts index 0dd2eb8fd..ab36f541a 100644 --- a/packages/daemon/test/session-registry.test.ts +++ b/packages/daemon/test/session-registry.test.ts @@ -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; } diff --git a/packages/daemon/test/snapshot-capture.test.ts b/packages/daemon/test/snapshot-capture.test.ts index 51f46f4d1..5c616ad7e 100644 --- a/packages/daemon/test/snapshot-capture.test.ts +++ b/packages/daemon/test/snapshot-capture.test.ts @@ -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; } diff --git a/packages/daemon/test/startup.test.ts b/packages/daemon/test/startup.test.ts index cf9c9a5cc..f0d63a7fa 100644 --- a/packages/daemon/test/startup.test.ts +++ b/packages/daemon/test/startup.test.ts @@ -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);