Files
js-sdk/tests/integration/higher-level-abstractions.integration.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

233 lines
6.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { Capsule, type CapsuleCreateOptions } from "../../src/capsule.js";
import { CodeInterpreter } from "../../src/code-interpreter/index.js";
import { DEFAULT_BASE_URL } from "../../src/config.js";
import { Git } from "../../src/git/index.js";
const baseUrl = process.env.WRENN_BASE_URL ?? DEFAULT_BASE_URL;
const apiKey = process.env.WRENN_API_KEY;
const template = process.env.WRENN_TEST_TEMPLATE ?? "minimal";
const codeTemplate = process.env.WRENN_TEST_CODE_TEMPLATE ?? "jupyter";
const codeCapsuleId = process.env.WRENN_TEST_CODE_CAPSULE_ID;
const waitTimeoutMs = Number(process.env.WRENN_TEST_WAIT_TIMEOUT_MS ?? 120_000);
const testTimeoutMs = waitTimeoutMs + 60_000;
const describeWithApiKey = apiKey ? describe : describe.skip;
const clientOpts: CapsuleCreateOptions = { baseUrl };
if (apiKey) clientOpts.apiKey = apiKey;
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRetryableServerError(error: unknown): boolean {
return (
typeof error === "object" &&
error !== null &&
"statusCode" in error &&
typeof error.statusCode === "number" &&
error.statusCode >= 500
);
}
async function expectCommandOk(
promise: Promise<{ exit_code?: number; stderr?: string }>,
): Promise<void> {
const result = await promise;
expect(result.exit_code, result.stderr).toBe(0);
}
async function withLiveCapsule(
fn: (capsule: Capsule) => Promise<void>,
): Promise<void> {
let capsule: Capsule | undefined;
try {
capsule = await Capsule.create(template, {
...clientOpts,
timeout_sec: 120,
});
await capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
await fn(capsule);
} finally {
if (capsule) {
await capsule.destroy().catch(() => undefined);
}
}
}
async function createCodeInterpreterWithRetry(): Promise<CodeInterpreter> {
let lastError: unknown;
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
return await CodeInterpreter.create({
...clientOpts,
template: codeTemplate,
timeout_sec: 120,
});
} catch (error) {
lastError = error;
if (!isRetryableServerError(error) || attempt === 3) break;
await delay(2_000);
}
}
throw lastError;
}
async function getCodeInterpreter(): Promise<{
interpreter: CodeInterpreter;
shouldDestroy: boolean;
}> {
if (!codeCapsuleId) {
return {
interpreter: await createCodeInterpreterWithRetry(),
shouldDestroy: true,
};
}
const interpreter = CodeInterpreter.connect(codeCapsuleId, clientOpts);
const info = await interpreter.capsule.getInfo();
if (info.status === "paused") {
await interpreter.capsule.resume({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
wait: true,
});
} else {
await interpreter.capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
}
return { interpreter, shouldDestroy: false };
}
describeWithApiKey("Higher-level abstractions live integration", () => {
it(
"runs git workflows inside a live capsule",
async () => {
await withLiveCapsule(async (capsule) => {
const root = `/tmp/wrenn-git-${Date.now()}`;
const origin = `${root}/origin.git`;
const work = `${root}/work`;
await expectCommandOk(
capsule.commands.exec("mkdir", { args: ["-p", root] }),
);
await expectCommandOk(
capsule.commands.exec("git", { args: ["init", "--bare", origin] }),
);
await expectCommandOk(capsule.git.clone(origin, { path: work }));
const git = new Git(capsule, { cwd: work });
const pwd = await capsule.commands.exec("pwd", { cwd: work });
expect(pwd.stdout?.trim(), pwd.stderr).toBe(work);
await expectCommandOk(
capsule.commands.exec("git", {
args: ["config", "user.email", "integration@example.com"],
cwd: work,
}),
);
await expectCommandOk(
capsule.commands.exec("git", {
args: ["config", "user.name", "Integration Test"],
cwd: work,
}),
);
await expectCommandOk(
capsule.commands.exec("sh", {
args: ["-lc", "printf 'phase5-git\\n' > README.md"],
cwd: work,
}),
);
const dirty = await git.status();
expect(dirty.exit_code, dirty.stderr).toBe(0);
expect(dirty.stdout, dirty.stderr).toContain("README.md");
await expectCommandOk(git.add("README.md"));
const commit = await git.commit("initial commit");
expect(commit.exit_code, commit.stderr).toBe(0);
const log = await git.log({ maxCount: 1 });
expect(log.exit_code, log.stderr).toBe(0);
expect(log.stdout, log.stderr).toContain("initial commit");
await expectCommandOk(git.branch());
await expectCommandOk(git.checkout("feature", { create: true }));
const branch = await git.branch();
expect(branch.exit_code, branch.stderr).toBe(0);
expect(branch.stdout?.trim(), branch.stderr).toBe("feature");
await expectCommandOk(
git.push({ branch: "feature", remote: "origin" }),
);
await expectCommandOk(
git.pull({ branch: "feature", remote: "origin" }),
);
});
},
testTimeoutMs,
);
it(
"executes a code interpreter cell in a live capsule",
async () => {
let interpreter: CodeInterpreter | undefined;
let shouldDestroy = false;
try {
const codeInterpreter = await getCodeInterpreter();
interpreter = codeInterpreter.interpreter;
shouldDestroy = codeInterpreter.shouldDestroy;
const result = await interpreter.notebook.execCell("print(6 * 7)", {
timeoutSec: 15,
});
expect(result.exitCode).toBe(0);
expect(result.stdout.trim()).toBe("42");
expect(result.stderr).toBe("");
} finally {
if (interpreter && shouldDestroy) {
await interpreter.capsule.destroy().catch(() => undefined);
}
}
},
testTimeoutMs,
);
it(
"destroys an owned code interpreter capsule when async disposed",
async () => {
let capsuleId: string | undefined;
try {
const interpreter = await createCodeInterpreterWithRetry();
capsuleId = interpreter.capsule.id;
await interpreter.capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
await interpreter[Symbol.asyncDispose]();
await expect(
Capsule.connect(capsuleId, clientOpts).getInfo(),
).resolves.toMatchObject({ id: capsuleId, status: "stopped" });
capsuleId = undefined;
} finally {
if (capsuleId) {
await Capsule.destroy(capsuleId, clientOpts).catch(() => undefined);
}
}
},
testTimeoutMs,
);
});