Files
openrig/packages/cli/test/environment-connect.test.ts
Mike Schwarz b35c2c5e93 fix: deliver maintained environment startup to the guest worker (#2418)
An environment seat's startup identity, selected role and context references, and text actions travel to the guest through a hash-checked first-input file, with a bounded initial prompt pointing to it; nothing is typed through the host viewer, so startup text can't reach the host shell if the viewer exits. Readiness is the guest execution's identity, generation and running state; only environment connect execs; the viewer does one live identity and generation lookup (driver 30 s, client 35 s) instead of serial file validation, while launch and the guest latch still validate. A failed owned delete reports its observed stage and error and whether dispatch is unknown, keeping the original intent, claim and same-operation reconciliation. An older guest helper's rejection of first input names the likely outdated image and the rebuild step.

Limits: a same-generation reconciled launch opens no viewer, and a non-text interactive startup action on an environment seat fails without host fallback.

313 lines added and 18 removed across 15 files, about two thirds of them tests.
2026-11-02 19:47:28 -08:00

34 lines
3.1 KiB
TypeScript

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(["slow-get", "slow-validate", "changed-generation"])("the viewer bounds its live lookup without the redundant validation deadline: %s", async mode => {
vi.useFakeTimers(); const generation = mode === "changed-generation" ? "changed-generation" : "generation-one";
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)); const validation = init?.method === "POST";
if (validation && mode === "changed-generation") return new Response(JSON.stringify({ error: "The prepared environment belongs to another generation" }), { status: 409 });
if ((mode === "slow-get" && !validation) || (mode === "slow-validate" && validation)) {
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;
if (mode !== "changed-generation") {
expect(paths).toEqual(["http://127.0.0.1:1/api/environments/binding-one?live=true"]);
expect(errors).not.toHaveBeenCalled(); expect(spawn).toHaveBeenCalledTimes(1);
} else { expect(spawn).not.toHaveBeenCalled(); expect(errors).toHaveBeenCalledWith(expect.stringContaining("generation")); }
});