test(S5b): cover all import running-name refusal surfaces

This commit is contained in:
v-openrig-build authored and mvschwarz committed 2026-08-26 03:10:45 -07:00
1 parent 279e98e5d3
commit a0ce8763dd
2 files changed
+239

No files matched your search

@@ -0,0 +1,113 @@
// S5b final-fix round 2 (r2 artifact 8d9ec788) — every CLI consumer of the
// direct instantiate/materialize routes must render the locked rig_name_running
// teaching refusal VERBATIM, not wrap it in check-your-spec/validate noise.
// The stub serves the daemon's post-fix 409 outcome shape.
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
import * as http from "node:http";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { Command } from "commander";
import { adoptCommand } from "../src/commands/adopt.js";
import { importCommand } from "../src/commands/import.js";
import { DaemonClient } from "../src/client.js";
import { STATE_FILE, type DaemonState, type LifecycleDeps } from "../src/daemon-lifecycle.js";
const TEACHING =
'A rig named "dupe-cli" is already RUNNING: 01RIGID with 1 running session(s) ' +
"(checked: existing rigs with this name for sessions in status 'running'). " +
"Nothing was created or launched. Alternatives: work with the running rig " +
"(rig ps --nodes / rig send), stop it first with 'rig down dupe-cli', " +
"or launch this spec under a different name.";
function mockLifecycleDeps(port: number): LifecycleDeps {
return {
spawn: vi.fn(() => ({ pid: 1, unref: vi.fn() }) as never),
fetch: vi.fn(async () => ({ ok: true })),
kill: vi.fn(() => true),
readFile: vi.fn((p: string) => {
if (p === STATE_FILE) {
return JSON.stringify({ pid: 123, port, db: "test.sqlite", startedAt: "2026-04-20T00:00:00Z" } as DaemonState);
}
return null;
}),
writeFile: vi.fn(),
removeFile: vi.fn(),
exists: vi.fn((p: string) => p === STATE_FILE),
mkdirp: vi.fn(),
openForAppend: vi.fn(() => 3),
isProcessAlive: vi.fn(() => true),
};
}
describe("rig import rendering — rig_name_running teaching refusal (S5b r2)", () => {
let server: http.Server;
let port: number;
let specPath: string;
let tempDir: string;
beforeAll(async () => {
server = http.createServer(async (req, res) => {
let body = "";
for await (const chunk of req) body += chunk;
if ((req.url === "/api/rigs/import" || req.url === "/api/rigs/import/materialize") && req.method === "POST") {
res.writeHead(409, { "Content-Type": "application/json" });
res.end(JSON.stringify({
ok: false,
code: "rig_name_running",
error: TEACHING,
runningRig: { id: "01RIGID", name: "dupe-cli", runningSessionCount: 1 },
}));
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "not found" }));
});
await new Promise<void>((resolve) => { server.listen(0, resolve); });
port = (server.address() as { port: number }).port;
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "import-rnr-"));
specPath = path.join(tempDir, "rig.yaml");
fs.writeFileSync(specPath, 'version: "0.2"\nname: dupe-cli\npods: []\nedges: []\n');
});
afterAll(async () => {
await new Promise<void>((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
fs.rmSync(tempDir, { recursive: true, force: true });
});
async function run(mode: "--instantiate" | "--materialize-only" | "adopt"): Promise<{ out: string; exitCode: number | undefined }> {
const captured: string[] = [];
const originalExit = process.exitCode;
process.exitCode = undefined;
vi.spyOn(console, "log").mockImplementation((...args) => captured.push(args.join(" ")));
vi.spyOn(console, "error").mockImplementation((...args) => captured.push(args.join(" ")));
try {
const prog = new Command();
prog.exitOverride();
const deps = {
lifecycleDeps: mockLifecycleDeps(port),
clientFactory: (baseUrl: string) => new DaemonClient(baseUrl),
readFile: (p: string) => fs.readFileSync(p, "utf-8"),
};
const args = mode === "adopt"
? ["node", "rig", "adopt", specPath, "--bind", "crew.a=existing-session"]
: ["node", "rig", "import", specPath, mode];
prog.addCommand(mode === "adopt" ? adoptCommand(deps as never) : importCommand(deps as never));
await prog.parseAsync(args).catch(() => {});
} finally {
vi.restoreAllMocks();
}
const exitCode = process.exitCode;
process.exitCode = originalExit;
return { out: captured.join("\n"), exitCode };
}
for (const mode of ["--instantiate", "--materialize-only", "adopt"] as const) {
it(`${mode}: renders the teaching refusal verbatim — no spec-blaming or daemon-logs noise`, async () => {
const { out, exitCode } = await run(mode);
expect(out).toBe(TEACHING);
expect(exitCode).toBe(1);
});
}
});
@@ -0,0 +1,126 @@
// S5b final-fix round 2 (r2 artifact 8d9ec788) — the WHOLE import/materialize
// delivery class, table-shaped so the typed rig_name_running variant cannot
// drift among sibling handlers: pod-aware import (fixed round 1, control),
// legacy import and materialize-only (the two 500-mapping siblings).
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import type Database from "better-sqlite3";
import { createFullTestDb, createTestApp } from "./helpers/test-app.js";
const POD_YAML = (name: string) => [
'version: "0.2"',
`name: ${name}`,
"pods:",
" - id: crew",
" label: Crew",
" members:",
" - id: a",
' agent_ref: "builtin:terminal"',
' profile: "none"',
" runtime: terminal",
" cwd: /",
" edges: []",
"edges: []",
].join("\n");
const LEGACY_YAML = (name: string) => [
"schema_version: 1",
`name: ${name}`,
'version: "1.0"',
"nodes:",
" - id: solo",
" runtime: claude-code",
" cwd: /",
"edges: []",
].join("\n");
describe("import/materialize routes — rig_name_running is a 409 conflict on EVERY sibling (S5b r2)", () => {
let db: Database.Database;
let setup: ReturnType<typeof createTestApp>;
beforeEach(() => {
db = createFullTestDb();
setup = createTestApp(db);
});
afterEach(() => { db.close(); });
function seedRunningRig(name: string) {
const rig = setup.rigRepo.createRig(name);
const node = setup.rigRepo.addNode(rig.id, "crew.a", { runtime: "claude-code", cwd: "/" });
const session = setup.sessionRegistry.registerSession(node.id, `crew-a@${name}`);
setup.sessionRegistry.updateStatus(session.id, "running");
return rig;
}
function rigCount(name: string): number {
return (db.prepare("SELECT COUNT(*) AS c FROM rigs WHERE name = ?").get(name) as { c: number }).c;
}
// The table: every production route that surfaces an instantiate/materialize
// outcome, driven identically. Adding a sibling handler without adding a row
// here is the drift this test exists to catch.
const ROUTES: Array<{
label: string;
path: string;
yaml: (name: string) => string;
headers: (name: string) => Record<string, string>;
}> = [
{
label: "pod-aware POST /api/rigs/import (round-1 control)",
path: "/api/rigs/import",
yaml: POD_YAML,
headers: () => ({ "Content-Type": "text/yaml", "X-Rig-Root": "/tmp" }),
},
{
label: "legacy POST /api/rigs/import",
path: "/api/rigs/import",
yaml: LEGACY_YAML,
headers: () => ({ "Content-Type": "text/yaml" }),
},
{
label: "POST /api/rigs/import/materialize",
path: "/api/rigs/import/materialize",
yaml: POD_YAML,
headers: () => ({ "Content-Type": "text/yaml", "X-Rig-Root": "/tmp" }),
},
];
for (const route of ROUTES) {
it(`${route.label}: RUNNING same-name rig -> 409, top-level code, teaching text, nothing created or launched`, async () => {
const name = "dupe-import";
const rig = seedRunningRig(name);
const createSession = (setup.tmuxAdapter as unknown as { createSession: ReturnType<typeof vi.fn> }).createSession;
const res = await setup.app.request(route.path, {
method: "POST",
headers: route.headers(name),
body: route.yaml(name),
});
const body = await res.json() as Record<string, unknown>;
expect(res.status, `${route.label} status`).toBe(409);
expect(body["code"], `${route.label} top-level code`).toBe("rig_name_running");
const teaching = String(body["error"] ?? "");
expect(body["message"], `${route.label} message/error parity`).toBe(teaching);
expect(teaching).toContain(name);
expect(teaching).toContain(rig.id);
expect(teaching).toMatch(/nothing was created or launched/i);
expect(teaching).toMatch(/rig down/);
expect(rigCount(name), `${route.label} rig count`).toBe(1);
expect(createSession, `${route.label} launches`).not.toHaveBeenCalled();
});
}
it("control: materialize with X-Target-Rig-Id (adopt/expand family) targets the EXISTING rig — guard never fires", async () => {
const rig = seedRunningRig("target-mat");
const res = await setup.app.request("/api/rigs/import/materialize", {
method: "POST",
headers: { "Content-Type": "text/yaml", "X-Rig-Root": "/tmp", "X-Target-Rig-Id": rig.id },
body: POD_YAML("target-mat"),
});
const body = await res.json() as Record<string, unknown>;
expect(body["code"]).not.toBe("rig_name_running");
expect(rigCount("target-mat")).toBe(1);
});
});