- Update generated types from new openapi.yaml (capsule stats, usage, metrics, pause/resume lifecycle, host/channel management, auth flow) - Add Capsule pause/resume/ping/getMetrics lifecycle methods - Add Capsule.waitForReady abort signal support - Add PtyManager.connect and PtySession disposal - Fix HttpClient empty-body response handling (content-length: 0) - Add streamProcess() to CommandManager for background process streams - Add integration tests for capsule lifecycle, git, and PTY features - Add unit tests for AsyncQueue error paths, PtySession.close, Git.checkout without create, Git.add single string, Notebook.execCell error case, and PtyStartOptions fields
161 lines
4.2 KiB
TypeScript
161 lines
4.2 KiB
TypeScript
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"],
|
|
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("connects to an existing background process stream", async () => {
|
|
const capsule = new Capsule("cap_1", {
|
|
baseUrl: "https://api.example.com",
|
|
});
|
|
const mockConnection = {
|
|
close: vi.fn(),
|
|
get isClosed() {
|
|
return false;
|
|
},
|
|
send: vi.fn(),
|
|
};
|
|
const connectProcess = vi
|
|
.spyOn(capsule.client.capsules, "connectProcess")
|
|
.mockResolvedValue(mockConnection as never);
|
|
|
|
const result = await capsule.commands.streamProcess("123");
|
|
|
|
expect(connectProcess).toHaveBeenCalledWith("cap_1", "123", {
|
|
onMessage: expect.any(Function),
|
|
});
|
|
expect(result).toBe(mockConnection);
|
|
});
|
|
|
|
it("passes timeout to streamProcess", async () => {
|
|
const capsule = new Capsule("cap_1", {
|
|
baseUrl: "https://api.example.com",
|
|
});
|
|
const connectProcess = vi
|
|
.spyOn(capsule.client.capsules, "connectProcess")
|
|
.mockResolvedValue({
|
|
close: vi.fn(),
|
|
get isClosed() {
|
|
return false;
|
|
},
|
|
send: vi.fn(),
|
|
} as never);
|
|
|
|
await capsule.commands.streamProcess("worker", { timeoutMs: 5000 });
|
|
|
|
expect(connectProcess).toHaveBeenCalledWith("cap_1", "worker", {
|
|
onMessage: expect.any(Function),
|
|
timeoutMs: 5000,
|
|
});
|
|
});
|
|
|
|
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,
|
|
});
|
|
});
|
|
});
|