import { describe, expect, it, vi } from "vitest"; import { Capsule } from "../src/capsule.js"; describe("CommandManager", () => { it("executes foreground commands through the capsule client", async () => { const capsule = new Capsule("cap_1", { baseUrl: "https://api.example.com", }); const exec = vi.spyOn(capsule.client.capsules, "exec").mockResolvedValue({ exit_code: 0, stdout: "ok\n", }); await expect( capsule.commands.exec("node", { args: ["--version"], timeoutSec: 5, }), ).resolves.toMatchObject({ stdout: "ok\n" }); expect(exec).toHaveBeenCalledWith("cap_1", { args: ["--version"], background: false, cmd: "node", timeout_sec: 5, }); }); it("starts, lists, and kills background processes", async () => { const capsule = new Capsule("cap_1", { baseUrl: "https://api.example.com", }); const exec = vi.spyOn(capsule.client.capsules, "exec").mockResolvedValue({ pid: 123, tag: "worker", }); const listProcesses = vi .spyOn(capsule.client.capsules, "listProcesses") .mockResolvedValue({ processes: [{ pid: 123, tag: "worker" }] }); const killProcess = vi .spyOn(capsule.client.capsules, "killProcess") .mockResolvedValue(undefined); await expect( capsule.commands.start("sleep", { args: ["60"], cwd: "/tmp", envs: { A: "1" }, tag: "worker", }), ).resolves.toMatchObject({ tag: "worker" }); await expect(capsule.commands.list()).resolves.toMatchObject({ processes: [{ tag: "worker" }], }); await expect( capsule.commands.kill("worker", "SIGTERM"), ).resolves.toBeUndefined(); expect(exec).toHaveBeenCalledWith("cap_1", { args: ["60"], background: true, cmd: "sleep", cwd: "/tmp", envs: { A: "1" }, tag: "worker", timeout_sec: 30, }); expect(listProcesses).toHaveBeenCalledWith("cap_1"); expect(killProcess).toHaveBeenCalledWith("cap_1", "worker", { signal: "SIGTERM", }); }); it("streams command events over the exec WebSocket", async () => { const capsule = new Capsule("cap_1", { baseUrl: "https://api.example.com", }); const sent: unknown[] = []; let onMessage: ((message: unknown) => void) | undefined; vi.spyOn(capsule.client.capsules, "execStream").mockImplementation( async (_id, opts) => { onMessage = opts.onMessage; return { close: vi.fn(), get isClosed() { return false; }, send: (message: unknown) => sent.push(message), } as never; }, ); const events = capsule.commands.stream("printf", { args: ["hello"] }); const first = events.next(); await vi.waitFor(() => expect(sent).toHaveLength(1)); expect(sent).toEqual([{ args: ["hello"], cmd: "printf", type: "start" }]); onMessage?.({ data: "hello", type: "stdout" }); await expect(first).resolves.toEqual({ done: false, value: { data: "hello", type: "stdout" }, }); const done = events.next(); onMessage?.({ exit_code: 0, type: "exit" }); await expect(done).resolves.toEqual({ done: false, value: { exit_code: 0, type: "exit" }, }); await expect(events.next()).resolves.toEqual({ done: true, value: undefined, }); }); });