Files
js-sdk/tests/commands.test.ts
Tasnim Kabir Sadik 349b230913 feat: align SDK with updated OpenAPI spec and add missing unit tests
- 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
2026-05-16 19:14:55 +06:00

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,
});
});
});