test: exercise corrected startup and ingestion regression controls

This commit is contained in:
dev-guard committed 2026-10-30 04:14:06 +00:00
1 parent fb85b67911
commit ad31f5d250
8 files changed
+169 -17

No files matched your search

@@ -1,3 +1,6 @@
import { QueueRepository } from "../../daemon/src/domain/queue-repository.js";
import { OutboxHandler } from "../../daemon/src/domain/outbox-handler.js";
import { EventBus } from "../../daemon/src/domain/event-bus.js";
import { afterEach, expect, it, vi } from "vitest";
import { createHash } from "node:crypto";
import { mkdtempSync, realpathSync, writeFileSync, readFileSync, rmSync } from "node:fs";
@@ -32,7 +35,8 @@ it("returns real artifact bytes under the authenticated work grant, refusing wro
const driver = { version: 1 as const, list: async () => ({ complete: true, resources: [] }), create: async (input: { operationId: string }) => { resource.operationId = input.operationId; return resource; },
inspect: async () => resource, upload: async () => {}, read: async () => Buffer.alloc(0),
download: async (_binding: unknown, _source: string, destination: string) => { downloads++; writeFileSync(destination, artifact); } };
const fixture = createTestApp(db, { appDeps: { environmentDriver: driver, environmentArtifactRoot: join(root, "artifacts") } as never });
const queue = new QueueRepository(db, new EventBus(db)); queue.attachOutbox(new OutboxHandler(db));
const fixture = createTestApp(db, { appDeps: { queueRepo: queue, environmentDriver: driver, environmentArtifactRoot: join(root, "artifacts") } as never });
const rig = fixture.rigRepo.createRig("team"), node = fixture.rigRepo.addNode(rig.id, "team.worker", { runtime: "openshell-claude", cwd: root });
fixture.sessionRegistry.registerSession(node.id, "worker@team");
const policy = join(root, "policy.yaml"); writeFileSync(policy, "version: 1\n");
@@ -42,7 +46,7 @@ it("returns real artifact bytes under the authenticated work grant, refusing wro
await platform.project("binding-one", { operationId: "projection-one", generation: "generation-one", files: [] });
// This is the existing queue writer, not a separate guest task store.
const created = await fixture.app.request("/api/queue/create", { method: "POST", headers: { "content-type": "application/json", "x-openrig-session": "coordinator@team" }, body: JSON.stringify({ qitemId: "export-task", destinationSession: "worker@team", body: "Return the selected report" }) });
expect(created.status).toBeLessThan(300);
expect(created.status, JSON.stringify(await created.clone().json())).toBeLessThan(300);
const daemon = await new Promise<ReturnType<typeof serve>>(resolve => {
const server = serve({ fetch: fixture.app.fetch, hostname: "127.0.0.1", port: 0 }, () => resolve(server));
});
+6 -5
View File
@@ -44,10 +44,10 @@ describe("MCP Server", () => {
if (cleanup) await cleanup();
});
// T1: MCP server lists all 36 tools: the original 18, the five queue tools, the telemetry events read, restore status, whoami,
// T1: MCP server lists all 37 tools: the original 18, the five queue tools, the telemetry events read, restore status, whoami,
// the team, slice, mission and two context reads, the artifact read, the decision room and delegated decision, and the
// worker's handoff and unclaim
it("lists all 36 tools", async () => {
it("lists all 37 tools", async () => {
await setup();
const result = await mcpClient.listTools();
const names = result.tools.map((t) => t.name).sort();
@@ -64,6 +64,7 @@ describe("MCP Server", () => {
"rig_context_pack",
"rig_discover",
"rig_down",
"rig_environment_export",
"rig_human_decision_room",
"rig_mission",
"rig_ps",
@@ -240,7 +241,7 @@ describe("MCP Server", () => {
const reads = ["rig_agent_validate", "rig_bundle_inspect", "rig_capture", "rig_chatroom_watch", "rig_ps", "rig_queue_list", "rig_queue_show",
"rig_restore_status", "rig_rig_nodes", "rig_rig_validate", "rig_snapshot_list", "rig_status", "rig_telemetry_events", "rig_whoami",
"rig_team", "rig_slices", "rig_mission", "rig_context_pack", "rig_context_get", "rig_artifact_read", "rig_human_decision_room"];
const additive = ["rig_add", "rig_chatroom_send", "rig_queue_create", "rig_snapshot_create"];
const additive = ["rig_environment_export", "rig_add", "rig_chatroom_send", "rig_queue_create", "rig_snapshot_create"];
// Discover refreshes and retires discovered facts; claim ends a blocked row's wait. Both change existing state.
// A delegated decision closes the person's ask; a handoff closes the item passed on; an unclaim ends a claim.
const destructive = ["rig_bind", "rig_discover", "rig_down", "rig_queue_claim", "rig_queue_decide", "rig_queue_handoff", "rig_queue_unclaim",
@@ -689,7 +690,7 @@ describe("MCP Server", () => {
// Verify server is responsive
const result = await mcpClient.listTools();
expect(result.tools.length).toBe(36);
expect(result.tools.length).toBe(37);
// Clean disconnect
await cleanup();
@@ -703,7 +704,7 @@ describe("MCP Server", () => {
await client2.connect(ct2);
const result2 = await client2.listTools();
expect(result2.tools.length).toBe(36);
expect(result2.tools.length).toBe(37);
await client2.close();
await server2.close();
@@ -1,9 +1,12 @@
import { QueueRepository } from "../src/domain/queue-repository.js";
import { OutboxHandler } from "../src/domain/outbox-handler.js";
import { EventBus } from "../src/domain/event-bus.js";
import { afterEach, describe, expect, it } from "vitest";
import { createHash } from "node:crypto";
import { mkdtempSync, readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { EnvironmentError } from "../src/domain/environment-platform.js";
import { EnvironmentError, EnvironmentPlatform } from "../src/domain/environment-platform.js";
import { createFullTestDb, createTestApp } from "./helpers/test-app.js";
const cleanup: Array<() => void> = [];
@@ -35,7 +38,8 @@ function fixture() {
async read(_binding: unknown, destination: string) { if (!files.has(destination)) throw new EnvironmentError("environment_file_missing", "Guest file is missing"); return Buffer.from(files.get(destination)!); },
async download(_binding: unknown, source: string, destination: string) { downloads++; if (!files.has(source)) throw new Error("guest unavailable"); writeFileSync(destination, files.get(source)!); },
};
const build = () => createTestApp(db, { appDeps: { environmentDriver: driver, environmentArtifactRoot: join(root, "exports") } as never });
const queue = new QueueRepository(db, new EventBus(db)); queue.attachOutbox(new OutboxHandler(db));
const build = () => createTestApp(db, { appDeps: { queueRepo: queue, environmentDriver: driver, environmentArtifactRoot: join(root, "exports") } as never });
let f = build();
const rig = f.rigRepo.createRig("environment-fixture");
const node = f.rigRepo.addNode(rig.id, "worker", { runtime: "openshell-claude", cwd: root });
@@ -49,13 +53,17 @@ function fixture() {
seat: "worker@environment-fixture", baseRuntime: "claude-code", guestRoot: "/sandbox/openrig", guestBaseUrl: "http://host.test:7433",
placement: { gateway: "gateway", workspace: "workspace", name: "worker-one", image: `image@sha256:${"a".repeat(64)}`, provider: "provider-ref", policy: join(root, "policy.yaml") } };
writeFileSync(input.placement.policy, "version: 1\n"); mkdirSync(join(root, "exports"));
const project = async (body: any) => {
try { return { status: 200, body: await new EnvironmentPlatform(db, driver as never).project("binding-one", body) }; }
catch (error) { return { status: error instanceof EnvironmentError ? error.status : 503, body: {} }; }
};
const create = () => request("/create", input);
const task = async () => {
const response = await f.app.request("/api/queue/create", { method: "POST", headers: { "content-type": "application/json", "x-openrig-session": "coordinator@environment-fixture" },
body: JSON.stringify({ qitemId: "task-one", destinationSession: "worker@environment-fixture", body: "Produce the selected artifact" }) });
expect(response.status).toBeLessThan(300);
expect(response.status, JSON.stringify(await response.clone().json())).toBeLessThan(300);
};
return { root, db, resources, files, driver, input, request, create, task, health: () => f.app.request("/healthz"),
return { root, db, resources, files, driver, input, request, create, task, project, health: () => f.app.request("/healthz"),
counts: () => ({ creates, downloads }), loseCreate: () => { loseCreate = true; }, restart: () => { f = build(); } };
}
@@ -114,7 +122,7 @@ describe("maintained environment binding and projection", () => {
expect(result.status).toBe(201);
expect(result.body).toMatchObject({ ownership: "attached", resource: { id: "external-id" } });
expect(f.counts().creates).toBe(0);
expect((await f.request("/binding-one/effect", { effect: "delete_environment" })).status).toBe(409);
expect((await f.request("/binding-one/effect", { effect: "delete_environment" })).status).toBe(404);
expect(f.resources[0].id).toBe("external-id");
});
@@ -127,7 +135,7 @@ describe("maintained environment binding and projection", () => {
it("verifies explicit destination bytes and refuses a changed required projection", async () => {
const f = fixture(); expect((await f.create()).status).toBe(201);
const local = join(f.root, "role.md"); writeFileSync(local, "current guidance");
const result = await f.request("/binding-one/project", { operationId: "projection-one", generation: "generation-one",
const result = await f.project({ operationId: "projection-one", generation: "generation-one",
files: [{ source: local, path: "work/CLAUDE.md", sha256: hash("current guidance"), required: true }] });
expect(result.status).toBe(200);
f.files.set("/sandbox/openrig/work/CLAUDE.md", "changed after projection");
@@ -138,7 +146,7 @@ describe("maintained environment binding and projection", () => {
const f = fixture(); expect((await f.create()).status).toBe(201);
const local = join(f.root, "role.md"); writeFileSync(local, "current guidance");
f.files.set("/sandbox/openrig/work/CLAUDE.md", "owner content");
expect((await f.request("/binding-one/project", { operationId: "projection-one", generation: "generation-one",
expect((await f.project({ operationId: "projection-one", generation: "generation-one",
files: [{ source: local, path: "work/CLAUDE.md", sha256: hash("current guidance"), required: true }] })).status).toBe(409);
expect(f.files.get("/sandbox/openrig/work/CLAUDE.md")).toBe("owner content");
});
@@ -146,7 +154,7 @@ describe("maintained environment binding and projection", () => {
it("rejects projection traversal before a provider write", async () => {
const f = fixture(); expect((await f.create()).status).toBe(201);
const local = join(f.root, "role.md"); writeFileSync(local, "current guidance");
expect((await f.request("/binding-one/project", { operationId: "projection-one", generation: "generation-one",
expect((await f.project({ operationId: "projection-one", generation: "generation-one",
files: [{ source: local, path: "../outside", sha256: hash("current guidance"), required: true }] })).status).toBe(400);
expect(f.files.size).toBe(0);
});
@@ -160,7 +168,7 @@ describe("maintained environment binding and projection", () => {
describe("managed environment artifact return", () => {
async function prepared() {
const f = fixture(); expect((await f.create()).status).toBe(201); await f.task();
expect((await f.request("/binding-one/project", { operationId: "projection-one", generation: "generation-one", files: [] })).status).toBe(200);
expect((await f.project({ operationId: "projection-one", generation: "generation-one", files: [] })).status).toBe(200);
const body = JSON.stringify({ task: "task-one", revision: 2, report: "actual artifact bytes" });
f.files.set("/sandbox/openrig/work/artifacts/report.json", body);
const input = { operationId: "export-one", taskId: "task-one", generation: "generation-one", path: "report.json", sha256: hash(body), bytes: Buffer.byteLength(body),
@@ -211,3 +219,21 @@ describe("managed environment artifact return", () => {
expect((await f.export()).status).toBe(400); expect(f.counts().downloads).toBe(0);
});
});
it("allows a corrected attach after a refused immutable identity", async () => {
const f = fixture(); f.resources.push({ id: "correct-id", name: "worker-one", workspace: "workspace", phase: "Ready" });
expect((await f.request("/attach", { ...f.input, operationId: "bad-attach", resourceId: "wrong-id" })).status).toBe(409);
const result = await f.request("/attach", { ...f.input, operationId: "correct-attach", resourceId: "correct-id" });
expect(result.status).toBe(201); expect(result.body.resource.id).toBe("correct-id");
});
it("allows a corrected create when the old binding has never requested a provider effect", async () => {
const f = fixture(); f.resources.push({ id: "foreign", name: "worker-one", workspace: "workspace", phase: "Ready" });
expect((await f.create()).status).toBe(409);
const result = await f.request("/create", { ...f.input, operationId: "correct-create", placement: { ...f.input.placement, name: "worker-two" } });
expect(result.status).toBe(201); expect(f.counts().creates).toBe(1);
});
it("reports an already-bound seat as a typed conflict", async () => {
const f = fixture(); expect((await f.create()).status).toBe(201);
const result = await f.request("/create", { ...f.input, bindingId: "other-binding", operationId: "other-create" });
expect(result.status).toBe(409); expect(result.body.code).toBe("environment_seat_bound");
});
@@ -82,3 +82,18 @@ it("refuses an exact-resume request without launching a fresh native", async ()
expect(result).toMatchObject({ ok: false, recovery: "attention_required" });
expect(f.activity).not.toHaveBeenCalled(); expect(f.tmux.sendShellCommand).not.toHaveBeenCalled();
});
it("refuses a new generation before projecting over a saved execution", async () => {
const f = await fixture(); expect((await f.startup.launch(f.binding, {}, f.tmux)).ok).toBe(true);
const before = [...f.files].map(([key,value]) => [key,value.toString("base64")]);
const generation = f.platform.get("binding-one")!.generation;
await expect(f.startup.project({ entries: [] } as unknown as ProjectionPlan, { ...f.binding, launchGeneration: "generation-two" })).rejects.toMatchObject({ code: "environment_prior_execution_unconfirmed" });
expect([...f.files].map(([key,value]) => [key,value.toString("base64")])).toEqual(before);
expect(f.platform.get("binding-one")!.generation).toBe(generation);
});
it("refuses startup input replacement before touching a saved execution", async () => {
const f = await fixture(); expect((await f.startup.launch(f.binding, {}, f.tmux)).ok).toBe(true);
const before = [...f.files].map(([key,value]) => [key,value.toString("base64")]);
await expect(f.startup.deliverStartup([], f.binding)).rejects.toMatchObject({ code: "environment_prior_execution_unconfirmed" });
expect([...f.files].map(([key,value]) => [key,value.toString("base64")])).toEqual(before);
});
@@ -0,0 +1,48 @@
import { afterEach, expect, it } from "vitest";
import { serve } from "@hono/node-server";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { createGuestActivityApp, type AppDeps } from "../src/server.js";
import { createFullTestDb } from "./helpers/test-app.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";
import { AgentActivityStore } from "../src/domain/agent-activity-store.js";
import { ActivityGrantStore } from "../src/domain/activity-grants.js";
const cleanups: Array<() => void | Promise<void>> = [];
afterEach(async () => { for (const close of cleanups.splice(0).reverse()) await close(); });
it("confines the actual guest HTTP port to grant-bound hooks", async () => {
const db = createFullTestDb(); cleanups.push(() => db.close());
const rigs = new RigRepository(db), rig = rigs.createRig("guest-fixture");
const node = rigs.addNode(rig.id,"team.worker",{runtime:"synth"});
const sessions = new SessionRegistry(db), eventBus = new EventBus(db);
const seat = "team-worker@guest-fixture";
const session = sessions.registerSession(node.id,seat); sessions.updateStatus(session.id,"running"); sessions.recordCommunityHarnessSelection(session.id,"synth",true);
const store = new AgentActivityStore({db,eventBus,resolveOccupantGeneration: id => sessions.currentOccupantTenure(id)?.generationUuid ?? null,
isRegisteredOccupantGeneration:(id,g) => sessions.isOccupantGenerationRegistered(id,g)});
const app = createGuestActivityApp({rigRepo:rigs,sessionRegistry:sessions,eventBus,agentActivityStore:store, activityHookToken:"install-token"} as AppDeps);
const server = serve({fetch:app.fetch,hostname:"127.0.0.1",port:0});
cleanups.push(() => new Promise<void>((done,reject) => server.close(error => error ? reject(error) : done())));
if (!server.listening) await new Promise<void>(done => server.once("listening",done));
const address = server.address(); if (!address || typeof address === "string") throw new Error("No test port");
const origin = `http://127.0.0.1:${address.port}`, grants = new ActivityGrantStore(db);
const issue = () => grants.issue({nodeId:node.id,generation:sessions.currentOccupantTenure(node.id)!.generationUuid,issuedBy:"test"});
const post = (token:string,body:Record<string,unknown>={}) => fetch(origin+"/api/activity/hooks",{method:"POST",headers:{authorization:`Bearer ${token}`,"content-type":"application/json"},
body:JSON.stringify({runtime:"synth",hookEvent:"UserPromptSubmit",subtype:"accepted",occurredAt:new Date().toISOString(),...body})});
const grant=issue(); expect((await post(grant.token)).status).toBe(200);
expect((await post(grant.token,{sessionName:"other@guest-fixture"})).status).toBe(403);
expect((await post("install-token")).status).toBe(401);
for (const path of ["/api/config","/api/queue","/api/activity/events","/api/activity/parked","/api/activity/pi-usage","/healthz"]) {
const response=await fetch(origin+path,{headers:{authorization:`Bearer ${grant.token}`}});
expect(response.status,path).toBe(404);
}
sessions.registerSession(node.id,seat,"handover");
expect((await post(grant.token)).status).toBe(409);
const next=issue(); grants.revoke({grantId:next.grantId},"test");
expect((await post(next.token)).status).toBe(401);
});
it("the shipped provider policy exposes the two confined ports and no main API port", () => {
const policy=readFileSync(resolve(import.meta.dirname,"../assets/openshell/policy.yaml"),"utf8");
expect(policy).toContain("port: 7781"); expect(policy).toContain("port: 7782"); expect(policy).not.toContain("7433");
});
+24 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, expect, it, vi } from "vitest";
import { mkdtempSync, realpathSync, writeFileSync, rmSync } from "node:fs";
import { mkdtempSync, realpathSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { OpenShellDriver } from "../src/adapters/openshell-driver.js";
@@ -26,3 +26,26 @@ it("preserves a typed missing-file result from the fixed guest helper", async ()
const binding = { guestRoot: "/sandbox/openrig", placement: { gateway: "gateway", workspace: "workspace", name: "worker-one" }, resource: { id: "immutable", name: "worker-one", workspace: "workspace" } } as EnvironmentBinding;
await expect(new OpenShellDriver(command).read(binding, "/sandbox/openrig/work/role.md")).rejects.toMatchObject({ code: "environment_file_missing" });
});
it("ingests bounded raw bytes instead of vendor tar after a growing stat", async () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), "openshell-ingest-"))); cleanup.push(() => rmSync(root, { recursive: true, force: true }));
let vendorDownloads = 0;
const command = vi.fn(async (args: string[]) => {
if (args.includes("get")) return JSON.stringify({ id: "immutable", name: "worker-one", workspace: "workspace", phase: "Ready", labels: {} });
if (args.includes("download")) { vendorDownloads++; writeFileSync(join(root, "artifact.json"), Buffer.alloc(1024*1024+1)); return ""; }
if (args.includes("--stat")) return JSON.stringify({ ok: true, bytes: 2 });
return JSON.stringify({ ok: true, base64: Buffer.alloc(1024*1024+1).toString("base64") });
});
const binding = { guestRoot: "/sandbox/openrig", placement: { gateway: "gateway", workspace: "workspace", name: "worker-one" }, resource: { id: "immutable", name: "worker-one", workspace: "workspace" } } as EnvironmentBinding;
await expect(new OpenShellDriver(command).download(binding, "/sandbox/openrig/work/artifacts/report.json", join(root,"artifact.json"))).rejects.toMatchObject({ code: "environment_file_invalid" });
expect(vendorDownloads).toBe(0); expect(existsSync(join(root,"artifact.json"))).toBe(false);
});
it("transfers one bounded file without extracting a vendor archive", async () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), "openshell-ingest-"))); cleanup.push(() => rmSync(root, { recursive: true, force: true }));
const command = vi.fn(async (args: string[]) => args.includes("get") ? JSON.stringify({ id: "immutable", name: "worker-one", workspace: "workspace", phase: "Ready" })
: JSON.stringify({ ok: true, base64: Buffer.from('{"ok":true}').toString("base64") }));
const binding = { guestRoot: "/sandbox/openrig", placement: { gateway: "gateway", workspace: "workspace", name: "worker-one" }, resource: { id: "immutable", name: "worker-one", workspace: "workspace" } } as EnvironmentBinding;
await new OpenShellDriver(command).download(binding, "/sandbox/openrig/work/artifacts/report.json", join(root,"artifact.json"));
expect(readFileSync(join(root,"artifact.json"),"utf8")).toBe('{"ok":true}');
expect(command.mock.calls.some(([args]) => args.includes("download"))).toBe(false);
});
@@ -28,3 +28,26 @@ it("rejects traversal and symlink reads instead of copying native or owner files
expect(await command(root, "--read", join(root, "work", "link"))).toMatchObject({ ok: false, code: "environment_symlink" });
expect(await command(root, "--read", root + "/../outside")).toMatchObject({ ok: false });
});
it("caps a file that grows after descriptor stat without reading its full content", async () => {
const root=realpathSync(mkdtempSync(join(tmpdir(),"openshell-helper-"))); roots.push(root);
mkdirSync(join(root,"work")); const file=join(root,"work","report.json"); writeFileSync(file,"{}");
const script=`import importlib.util,os,sys
spec=importlib.util.spec_from_file_location("guest",sys.argv[1]); g=importlib.util.module_from_spec(spec); spec.loader.exec_module(g)
original=os.fstat
changed=False
def race(fd):
global changed
value=original(fd)
if not changed:
changed=True
with open(sys.argv[3],"ab") as stream: stream.write(b"x"*(g.LIMIT+1))
return value
os.fstat=race
try:
g.regular(sys.argv[2],sys.argv[3]); print("unexpected-success")
except g.Refused as error: print(str(error))
`;
const result=await run("python3",["-c",script,helper,root,file],{timeout:5000});
expect(result.stdout.trim()).toBe("environment_file_invalid");
});
@@ -875,6 +875,18 @@ describe("StartupOrchestrator", () => {
expect(tmux.sendText).not.toHaveBeenCalled();
});
it("does not mark pending or project when saved environment execution refuses the prelude", async () => {
const seed=seedSession();
sessionRegistry.updateStartupStatus(seed.sessionId,"ready");
const preflight=vi.fn(async () => { throw new Error("saved execution preserved"); });
const adapter=mockAdapter({runtime:"openshell-claude",environment:{} as RuntimeAdapter["environment"],preflightEnvironmentStartup:preflight});
const input=makeInput(seed,{adapter});
const result=await createOrchestrator().startNode(input);
expect(result).toMatchObject({ok:false,startupStatus:"attention_required",errors:["saved execution preserved"]});
expect(preflight).toHaveBeenCalledTimes(1); expect(adapter.project).not.toHaveBeenCalled(); expect(adapter.launchHarness).not.toHaveBeenCalled();
expect(db.prepare("SELECT startup_status FROM sessions WHERE id=?").get(seed.sessionId)).toEqual({startup_status:"ready"});
});
// T1: fresh launch enters pending before startup delivery
it("marks pending before delivery", async () => {
const seed = seedSession();