mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
test: reproduce contained worker startup and effect reporting gaps
This commit is contained in:
1 parent
95f0ae6d67
commit
9cb7240b35
4 files changed
+96
No files matched your search
@@ -0,0 +1,29 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { spawn } from "node:child_process";
|
||||
import { environmentCommand } from "../src/commands/environment.js";
|
||||
import { DaemonClient } from "../src/client.js";
|
||||
import { STATE_FILE, type LifecycleDeps } from "../src/daemon-lifecycle.js";
|
||||
vi.mock("node:child_process", async original => ({ ...await original<typeof import("node:child_process")>(), spawn: vi.fn() }));
|
||||
afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); process.exitCode = undefined; });
|
||||
it.each(["generation-one", "changed-generation"])("the viewer bounds its one live lookup and never repeats full projection validation: %s", async generation => {
|
||||
vi.useFakeTimers();
|
||||
const paths: string[] = [];
|
||||
const client = new DaemonClient("http://127.0.0.1:1", { sessionName: "worker@test", ownerProbe: async () => ({ state: "unknown" }) as never,
|
||||
fetchImpl: (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
paths.push(String(url));
|
||||
await new Promise<void>((resolve, reject) => { const timer = setTimeout(resolve, 6000);
|
||||
init?.signal?.addEventListener("abort", () => { clearTimeout(timer); reject(init.signal?.reason); }, { once: true }); });
|
||||
return new Response(JSON.stringify({ generation, placement: { gateway: "gateway", workspace: "workspace", name: "worker" } }), { status: 200 });
|
||||
}) as typeof fetch });
|
||||
const lifecycleDeps: LifecycleDeps = { spawn: vi.fn(), fetch: vi.fn(async () => ({ ok: true })), kill: vi.fn(() => true),
|
||||
readFile: vi.fn((path: string) => path === STATE_FILE ? JSON.stringify({ pid: 123, port: 7433, db: "test.sqlite", startedAt: "2026-04-20T00:00:00Z" }) : null),
|
||||
writeFile: vi.fn(), removeFile: vi.fn(), exists: vi.fn((path: string) => path === STATE_FILE), mkdirp: vi.fn(), openForAppend: vi.fn(() => 3), isProcessAlive: vi.fn(() => true) };
|
||||
vi.mocked(spawn).mockImplementation(() => { const child = new EventEmitter(); queueMicrotask(() => child.emit("exit", 0)); return child as ReturnType<typeof spawn>; });
|
||||
const errors = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const run = environmentCommand({ lifecycleDeps, clientFactory: () => client }).parseAsync(["node", "rig", "connect", "binding-one", "--generation", "generation-one"]);
|
||||
await vi.advanceTimersByTimeAsync(6500); await run;
|
||||
expect(paths).toEqual(["http://127.0.0.1:1/api/environments/binding-one?live=true"]);
|
||||
if (generation === "generation-one") { expect(errors).not.toHaveBeenCalled(); expect(spawn).toHaveBeenCalledTimes(1); }
|
||||
else { expect(spawn).not.toHaveBeenCalled(); expect(errors).toHaveBeenCalledWith(expect.stringContaining("generation")); }
|
||||
});
|
||||
@@ -267,3 +267,17 @@ describe("finite completion is selected work plus a verified actual export", ()
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it.each(["provider-list", "cleanup-inventory"])("reports the actual pre-dispatch stage and error without claiming a delete: %s", async stage => {
|
||||
const f = await fixture(); const error = Object.assign(new Error("fixture engine connection refused"), { code: "ECONNREFUSED" });
|
||||
if (stage === "provider-list") vi.spyOn(f.driver, "list").mockRejectedValue(error);
|
||||
else Object.assign(f.driver, { captureCleanup: async () => { throw error; } });
|
||||
const request = { effect: "delete-owned", operationId: "delete-before-dispatch", generation: "generation-one", engineSocket: "/fixture/engine.sock" };
|
||||
for (let observation = 0; observation < 2; observation++) {
|
||||
const result = await f.request("/binding-one/effect", request);
|
||||
expect(result.body).toMatchObject({ state: "unconfirmed", stage, dispatched: false, error: { code: "ECONNREFUSED", message: "fixture engine connection refused" } });
|
||||
expect(f.counts().deleteCalls).toBe(0);
|
||||
const row = f.db.prepare("SELECT state FROM environment_operations WHERE operation_id=?").get(request.operationId) as { state: string };
|
||||
expect(row.state).toBe("intent");
|
||||
}
|
||||
});
|
||||
@@ -232,3 +232,28 @@ it.each(["unchanged", "missing-source", "changed", "added", "removed", "stale-de
|
||||
else { expect(restored).toBe(false); expect(restoredNode?.status).not.toBe("resumed"); }
|
||||
expect(textInput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// No provider or native binary: use the real contained adapter and launch journal.
|
||||
it.each(["unavailable", "different-generation", "stopped", "ready-at-shell"])("contained readiness observes the selected guest, not the connect process: %s", async kind => {
|
||||
const f = await fixture(); await f.startup.launch(f.binding, {}, f.tmux);
|
||||
const row = f.db.prepare("SELECT receipt_json FROM environment_operations WHERE kind='launch'").get() as { receipt_json: string };
|
||||
const native = JSON.parse(row.receipt_json);
|
||||
f.guest.mockImplementation(async () => {
|
||||
if (kind === "unavailable") throw new Error("guest identity unavailable");
|
||||
return { ...native, generation: kind === "different-generation" ? "other" : native.generation, running: kind !== "stopped", ok: true };
|
||||
});
|
||||
const adapter = new CommunityHarnessRuntimeAdapter("openshell-claude", { environmentStartup: f.startup,
|
||||
tmux: { ...f.tmux, hasSession: async () => true, getPaneCommand: async () => kind === "ready-at-shell" ? "bash" : "node" } });
|
||||
expect((await adapter.checkReady(f.binding)).ready).toBe(kind === "ready-at-shell");
|
||||
});
|
||||
it("passes generated first input through verified guest files and execs the short viewer command", async () => {
|
||||
const f = await fixture();
|
||||
const opts = { firstInputText: "Selected identity; read projected role.md. Literal $(not-a-host-command)." };
|
||||
const result = await f.startup.launch(f.binding, opts, f.tmux);
|
||||
expect(result.firstInput).toMatchObject({ submission: "unconfirmed" });
|
||||
const descriptor = JSON.parse(f.files.get("/sandbox/openrig/control/launch.pending.json")!.toString());
|
||||
expect(descriptor.firstInput).toMatchObject({ path: "control/first-input.txt", sha256: sha256(opts.firstInputText) });
|
||||
expect(f.files.get("/sandbox/openrig/control/first-input.txt")!.toString()).toBe(opts.firstInputText);
|
||||
const command = f.tmux.sendShellCommand.mock.calls[0] as unknown as [string, string];
|
||||
expect(command[1]).toMatch(/^exec /); expect(command[1]).not.toContain("Selected identity");
|
||||
});
|
||||
@@ -1478,6 +1478,34 @@ describe("StartupOrchestrator", () => {
|
||||
} finally { nodeFs.rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("delivers contained identity, role and actions without host input when the viewer exits after readiness", async () => {
|
||||
const dir = nodeFs.mkdtempSync(nodePath.join(nodeOs.tmpdir(), "contained-startup-race-"));
|
||||
try {
|
||||
const marker = nodePath.join(dir, "host-executed");
|
||||
const seed = seedSession(); db.prepare("UPDATE nodes SET runtime='openshell-claude' WHERE id=?").run(seed.nodeId);
|
||||
let passed: string | undefined; let viewerExited = false;
|
||||
const contained = { platform: {}, preflight: async () => {}, project: async () => ({ projected: [], skipped: [], failed: [] }),
|
||||
deliverStartup: async () => ({ delivered: 1, failed: [] }),
|
||||
launch: async (_binding: unknown, opts: { firstInputText?: string }) => { passed = opts.firstInputText;
|
||||
return { ok: true, ...(passed ? { firstInput: { path: "/guest/control/first-input.txt", submission: "unconfirmed" } } : {}) }; },
|
||||
checkReady: async () => { viewerExited = true; return { ready: true }; } };
|
||||
const adapter = new CommunityHarnessRuntimeAdapter("openshell-claude", { environmentStartup: contained as never,
|
||||
tmux: { hasSession: async () => true, getPaneCommand: async () => { viewerExited = true; return "node"; }, sendShellCommand: async () => ({ ok: true }) } });
|
||||
vi.mocked(tmux.sendText).mockImplementation(async (_target, text) => {
|
||||
if (viewerExited) { try { execFileSync("/bin/sh", ["-c", text], { stdio: "ignore" }); } catch {} }
|
||||
return { ok: true };
|
||||
});
|
||||
const role: ResolvedStartupFile = { path: "role.md", absolutePath: "/role.md", ownerRoot: "/", deliveryHint: "send_text", required: true, appliesOn: ["fresh_start"] };
|
||||
const result = await createOrchestrator({ readFile: () => `Selected role\ntouch '${marker}'` }).startNode(makeInput(seed, { adapter,
|
||||
resolvedStartupFiles: [role], startupActions: [makeIdentityAction(), makeAction({ type: "send_text", value: "selected after-ready instruction" })] }));
|
||||
expect(nodeFs.existsSync(marker)).toBe(false);
|
||||
expect(tmux.sendText).not.toHaveBeenCalled(); expect(tmux.sendKeys).not.toHaveBeenCalled();
|
||||
expect(passed).toContain("OpenRig-generated identity context");
|
||||
expect(passed).toContain("role.md"); expect(passed).toContain("selected after-ready instruction");
|
||||
expect(result.ok).toBe(true);
|
||||
} finally { nodeFs.rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it.each([[false, false, false], [true, false, false], [true, true, false], [true, false, true]])("passes the composed first input at launch (%s), assistance (%s), uncertain launch (%s)", async (firstInput, assisted, uncertain) => {
|
||||
const dir = nodeFs.mkdtempSync(nodePath.join(nodeOs.tmpdir(), "first-input-"));
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user