Compare commits

...
3 changed files with 92 additions and 2 deletions

No files matched your search

@@ -11,7 +11,7 @@ import { createFullTestDb, createTestApp } from "./helpers/test-app.js";
const cleanup: Array<() => void> = [];
afterEach(() => { for (const close of cleanup.splice(0).reverse()) close(); });
async function fixture(attached = false, automatic = true) {
async function fixture(attached = false, automatic = true, beforeGeneration = false) {
const root = realpathSync(mkdtempSync(join(tmpdir(), "environment-lifecycle-"))); cleanup.push(() => rmSync(root, { recursive: true, force: true }));
const db = createFullTestDb(); cleanup.push(() => db.close());
const resource = { id: "compute-one", name: "worker-one", workspace: "workspace", phase: "Ready", operationId: "create-one" };
@@ -40,16 +40,53 @@ async function fixture(attached = false, automatic = true) {
const rig = f.rigRepo.createRig("lifecycle"), node = f.rigRepo.addNode(rig.id, "worker", { runtime: "openshell-claude", cwd: root });
const request = async (path: string, body?: unknown) => { const res = await f.app.request(`/api/environments${path}`, { method: body ? "POST" : "GET", headers: { "content-type": "application/json", "x-openrig-session": "coordinator@lifecycle" }, ...(body ? { body: JSON.stringify(body) } : {}) }); return { status: res.status, body: await res.json() as Record<string, any> }; };
const policy = join(root, "policy.yaml"); writeFileSync(policy, "version: 1\n");
const input = { version: 1, bindingId: "binding-one", operationId: "create-one", nodeId: node.id, seat: "worker@lifecycle", baseRuntime: "claude-code", guestRoot: "/sandbox/openrig", guestBaseUrl: "http://guest.test:7433", placement: { gateway: "gateway", workspace: "workspace", name: "worker-one", image: `image@sha256:${"a".repeat(64)}`, provider: "provider", policy } };
const input = { version: 1, bindingId: "binding-one", operationId: "create-one", nodeId: beforeGeneration ? undefined : node.id, seat: "worker@lifecycle", baseRuntime: "claude-code", guestRoot: "/sandbox/openrig", guestBaseUrl: "http://guest.test:7433", placement: { gateway: "gateway", workspace: "workspace", name: "worker-one", image: `image@sha256:${"a".repeat(64)}`, provider: "provider", policy } };
const created = await request(attached ? "/attach" : "/create", { ...input, ...(attached ? { resourceId: resource.id } : {}) }); expect(created.status, JSON.stringify(created.body)).toBeLessThan(300);
if (!beforeGeneration) {
db.prepare("UPDATE environment_bindings SET generation='generation-one' WHERE binding_id='binding-one'").run();
const receipt = { version: 1, bindingId: "binding-one", nodeId: node.id, resourceId: resource.id, ...native, activityGrantId: "grant-one" };
db.prepare("INSERT INTO environment_operations(operation_id,binding_id,kind,request_json,state,receipt_json) VALUES ('launch-generation-one','binding-one','launch',?,'confirmed',?)").run(JSON.stringify(receipt), JSON.stringify(receipt));
}
const effect = (name: string, operationId = "effect-one") => request("/binding-one/effect", { operationId, generation: "generation-one", effect: name });
const lifecycle = new EnvironmentLifecycle(new EnvironmentPlatform(db, driver), queue, join(root, "retained"));
return { root, db, driver, queue, lifecycle, request, effect, resource, native, files, history, capture: () => f.snapshotCapture.captureSnapshot(rig.id, "manual"), disappear: () => { present = false; }, counts: () => ({ stopCalls, deleteCalls, creates }), loseStop: () => { loseStop = true; }, unavailable: () => { unavailable = true; }, dirtyCleanup: () => { dependents.volumes = true; } };
}
describe("owned and attached environment effects", () => {
async function failedProjection(attached = false) {
const f = await fixture(attached, true, true), platform = new EnvironmentPlatform(f.db, f.driver);
vi.spyOn(f.driver, "read").mockRejectedValueOnce(new Error("guest root read denied"));
const source = join(f.root, "policy.yaml");
await expect(platform.project("binding-one", { operationId: "projection-failed", generation: "not-established", files: [
{ path: "work/role.md", source, sha256: sha256(readFileSync(source)), required: true },
] })).rejects.toThrow("guest root read denied");
expect(platform.get("binding-one")).toMatchObject({ generation: null, resource: { id: f.resource.id } });
expect(platform.get("binding-one")!.nodeId).toBeUndefined();
return { ...f, cleanup: () => f.request("/binding-one/effect", { operationId: "cleanup-failed-create", effect: "delete-owned", generation: null, resourceId: f.resource.id }) };
}
it("cleans the exact owned create after projection fails before node and generation binding", async () => {
const f = await failedProjection(), result = await f.cleanup();
expect(result.status, JSON.stringify(result.body)).toBe(200);
expect(result.body).toMatchObject({ state: "confirmed", generation: null, resourceId: "compute-one", effect: "delete-owned" });
expect(f.counts()).toMatchObject({ creates: 1, deleteCalls: 1, stopCalls: 0 });
expect((await f.cleanup()).body).toEqual(result.body); expect(f.counts().deleteCalls).toBe(1);
});
it("reconciles uncertain pre-generation cleanup without another provider delete", async () => {
const f = await failedProjection();
let observe = false;
vi.spyOn(f.driver, "deleteEnvironment").mockImplementationOnce(async () => { f.disappear(); throw new Error("lost delete reply"); });
const original = f.driver.cleanupState;
vi.spyOn(f.driver, "cleanupState").mockImplementation(async () => { if (!observe) throw new Error("engine unavailable"); return original(); });
expect((await f.cleanup()).body.state).toBe("unconfirmed");
observe = true;
expect((await f.cleanup()).body.state).toBe("confirmed");
expect(f.driver.deleteEnvironment).toHaveBeenCalledTimes(1);
});
it("preserves an attached resource after projection fails before a generation exists", async () => {
const f = await failedProjection(true), result = await f.cleanup();
expect(result.status).toBe(409); expect(result.body.code).toBe("environment_attached_preserved");
expect(f.counts()).toMatchObject({ creates: 0, deleteCalls: 0, stopCalls: 0 });
});
it("stops only the selected native and reconciles a lost acknowledgement without a second signal", async () => {
const f = await fixture(); f.loseStop();
const first = await f.effect("stop-native"); expect(first.body.state).toBe("confirmed");
@@ -0,0 +1,32 @@
import { afterEach, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { RigSpecCodec } from "../src/domain/rigspec-codec.js";
import { RigSpecSchema } from "../src/domain/rigspec-schema.js";
import { validateRigSpecImport } from "../src/domain/spec-validation-service.js";
import { createFullTestDb, createTestApp } from "./helpers/test-app.js";
const sourceRef = resolve(import.meta.dirname, "../assets/openshell/rig.yaml");
const yaml = readFileSync(sourceRef, "utf8");
afterEach(() => vi.restoreAllMocks());
it("the shipped OpenShell example parses and validates with its edge inside the pod", () => {
expect(validateRigSpecImport(yaml)).toMatchObject({ valid: true, errors: [] });
const spec = RigSpecSchema.normalize(RigSpecCodec.parse(yaml) as Record<string, unknown>);
expect(spec.pods[0]!.edges).toContainEqual({ kind: "delegates_to", from: "coordinator", to: "worker" });
expect(spec.edges ?? []).toEqual([]);
});
it("the ordinary up plan classifies the shipped example without launching seats", async () => {
const db = createFullTestDb();
try {
const f = createTestApp(db, { upRouterFsOps: {
exists: (path: string) => path === sourceRef,
readFile: () => yaml,
readHead: (_path: string, bytes: number) => Buffer.from(yaml).subarray(0, bytes),
} });
const plan = vi.spyOn(f.bootstrapOrchestrator, "bootstrap").mockResolvedValue({ runId: "plan", status: "planned", stages: [], errors: [], warnings: [] } as never);
const result = await f.app.request("/api/up", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ sourceRef, plan: true }) });
expect(result.status, await result.text()).toBe(200);
expect(plan).toHaveBeenCalledWith(expect.objectContaining({ mode: "plan", sourceRef }));
expect(f.tmuxAdapter.createSession).not.toHaveBeenCalled();
} finally { db.close(); }
});
@@ -58,3 +58,24 @@ it("the shipped resume latch refuses missing exact history before provider prepa
writeFileSync(join(root,"control","launch.pending.json"),JSON.stringify({version:1,bindingId:"binding-one",generation:"generation-two",nativeId:id,resumeToken:id,nativeType:"community:openshell-claude",files:[],checkpoint:{nativeId:id,nativeType:"community:openshell-claude",files:[]}}));
expect(await command(root,"--prepare")).toMatchObject({ok:false,code:"environment_checkpoint_invalid"});
});
// Model the measured filesystem grant: the selected root is readable, its ancestors are not.
it("reads inside the granted workspace when opening every ancestor is denied", async () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), "openshell-granted-root-"))); roots.push(root);
mkdirSync(join(root, "work")); const file = join(root, "work", "role.md"); writeFileSync(file, "selected context");
const script = `import importlib.util,os,sys
from pathlib import Path
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.open
ancestors={str(p) for p in Path(sys.argv[2]).parents}
def granted(path, flags, *args, **kwargs):
if str(path) in ancestors: raise PermissionError(13,"ancestor not granted",str(path))
return original(path,flags,*args,**kwargs)
os.open=granted
try:
print(g.regular(sys.argv[2],sys.argv[3]).decode())
except Exception as error: print(type(error).__name__+":"+str(error))
`;
const result = await run("python3", ["-c", script, helper, root, file], { timeout: 5000 });
expect(result.stdout.trim()).toBe("selected context");
});