mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
test: pin environment correction boundaries before effects
This commit is contained in:
1 parent
fb85b67911
commit
7dd1c5b486
5 files changed
+79
-10
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));
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
@@ -243,7 +244,8 @@ describe("MCP Server", () => {
|
||||
const additive = ["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",
|
||||
const destructive = ["rig_bind", "rig_discover", "rig_down",
|
||||
"rig_environment_export", "rig_queue_claim", "rig_queue_decide", "rig_queue_handoff", "rig_queue_unclaim",
|
||||
"rig_queue_update", "rig_restore", "rig_send", "rig_up"];
|
||||
const hints = Object.fromEntries(tools.map((tool) => [tool.name, tool.annotations]));
|
||||
expect(Object.keys(hints).sort()).toEqual([...reads, ...additive, ...destructive].sort());
|
||||
@@ -591,6 +593,7 @@ describe("MCP Server", () => {
|
||||
|
||||
const result = await mcpClient.callTool({
|
||||
name: "rig_down",
|
||||
"rig_environment_export",
|
||||
arguments: { rigId: "rig-1", delete: true, force: false, snapshot: false },
|
||||
});
|
||||
|
||||
@@ -656,6 +659,7 @@ describe("MCP Server", () => {
|
||||
|
||||
const result = await mcpClient.callTool({
|
||||
name: "rig_down",
|
||||
"rig_environment_export",
|
||||
arguments: { rigId: "missing" },
|
||||
});
|
||||
|
||||
@@ -676,6 +680,7 @@ describe("MCP Server", () => {
|
||||
|
||||
const result = await mcpClient.callTool({
|
||||
name: "rig_down",
|
||||
"rig_environment_export",
|
||||
arguments: { rigId: "rig-1" },
|
||||
});
|
||||
|
||||
@@ -689,7 +694,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 +708,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,3 +1,6 @@
|
||||
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";
|
||||
@@ -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 });
|
||||
@@ -53,7 +57,7 @@ function fixture() {
|
||||
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"),
|
||||
counts: () => ({ creates, downloads }), loseCreate: () => { loseCreate = true; }, restart: () => { f = build(); } };
|
||||
@@ -211,3 +215,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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user