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
This commit is contained in:
@ -109,8 +109,8 @@ describe("Capsule", () => {
|
||||
capsuleResponse("cap_1"),
|
||||
Response.json({ sandbox_id: "cap_1", range: "10m", points: [] }),
|
||||
new Response(null, { status: 204 }),
|
||||
capsuleResponse("cap_1", "paused"),
|
||||
capsuleResponse("cap_1", "running"),
|
||||
capsuleResponse("cap_1", "pausing"),
|
||||
capsuleResponse("cap_1", "resuming"),
|
||||
new Response(null, { status: 204 }),
|
||||
]);
|
||||
const capsule = new Capsule("cap_1", {
|
||||
@ -122,9 +122,9 @@ describe("Capsule", () => {
|
||||
range: "10m",
|
||||
});
|
||||
await expect(capsule.ping()).resolves.toBeUndefined();
|
||||
await expect(capsule.pause()).resolves.toMatchObject({ status: "paused" });
|
||||
await expect(capsule.pause()).resolves.toMatchObject({ status: "pausing" });
|
||||
await expect(capsule.resume()).resolves.toMatchObject({
|
||||
status: "running",
|
||||
status: "resuming",
|
||||
});
|
||||
await expect(capsule.destroy()).resolves.toBeUndefined();
|
||||
|
||||
@ -157,14 +157,19 @@ describe("Capsule", () => {
|
||||
expect(calls).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("fails waitForReady on terminal capsule states", async () => {
|
||||
setupFetch([capsuleResponse("cap_1", "error")]);
|
||||
it.each([
|
||||
"error",
|
||||
"missing",
|
||||
"stopping",
|
||||
"stopped",
|
||||
])("fails waitForReady on terminal capsule state %s", async (status) => {
|
||||
setupFetch([capsuleResponse("cap_1", status)]);
|
||||
const capsule = new Capsule("cap_1", {
|
||||
baseUrl: "https://api.example.com",
|
||||
});
|
||||
|
||||
await expect(capsule.waitForReady()).rejects.toThrow(
|
||||
'Capsule cap_1 reached terminal status "error"',
|
||||
`Capsule cap_1 reached terminal status "${status}"`,
|
||||
);
|
||||
});
|
||||
|
||||
@ -188,10 +193,64 @@ describe("Capsule", () => {
|
||||
await assertion;
|
||||
});
|
||||
|
||||
it("does not mutate the remote capsule when closed or disposed", async () => {
|
||||
it("resumes and waits until the capsule is running", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { calls } = setupFetch([
|
||||
capsuleResponse("cap_1", "resuming"),
|
||||
capsuleResponse("cap_1", "running"),
|
||||
]);
|
||||
const capsule = new Capsule("cap_1", {
|
||||
baseUrl: "https://api.example.com",
|
||||
});
|
||||
|
||||
const ready = capsule.resume({
|
||||
wait: true,
|
||||
intervalMs: 100,
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
await expect(ready).resolves.toMatchObject({ status: "running" });
|
||||
expect(calls.map((call) => `${call.init.method} ${call.url}`)).toEqual([
|
||||
"POST https://api.example.com/v1/capsules/cap_1/resume",
|
||||
"GET https://api.example.com/v1/capsules/cap_1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("aborts waitForReady when the signal is already aborted", async () => {
|
||||
const capsule = new Capsule("cap_1", {
|
||||
baseUrl: "https://api.example.com",
|
||||
});
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
await expect(
|
||||
capsule.waitForReady({ signal: controller.signal }),
|
||||
).rejects.toThrow("Operation aborted");
|
||||
});
|
||||
|
||||
it("aborts waitForReady when the signal fires during polling", async () => {
|
||||
vi.useFakeTimers();
|
||||
setupFetch([capsuleResponse("cap_1", "starting")]);
|
||||
const capsule = new Capsule("cap_1", {
|
||||
baseUrl: "https://api.example.com",
|
||||
});
|
||||
const controller = new AbortController();
|
||||
|
||||
const ready = capsule.waitForReady({
|
||||
intervalMs: 100,
|
||||
timeoutMs: 5_000,
|
||||
signal: controller.signal,
|
||||
});
|
||||
controller.abort();
|
||||
|
||||
await expect(ready).rejects.toThrow("Operation aborted");
|
||||
});
|
||||
|
||||
it("does not mutate the remote capsule when connected capsules are closed or disposed", async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const capsule = new Capsule("cap_1", {
|
||||
const capsule = Capsule.connect("cap_1", {
|
||||
baseUrl: "https://api.example.com",
|
||||
});
|
||||
|
||||
@ -201,6 +260,43 @@ describe("Capsule", () => {
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("destroys created capsules when async disposed", async () => {
|
||||
const { calls } = setupFetch([
|
||||
capsuleResponse("cap_created"),
|
||||
new Response(null, { status: 204 }),
|
||||
]);
|
||||
|
||||
const capsule = await Capsule.create({
|
||||
baseUrl: "https://api.example.com",
|
||||
});
|
||||
|
||||
await capsule[Symbol.asyncDispose]();
|
||||
|
||||
expect(calls.map((call) => `${call.init.method} ${call.url}`)).toEqual([
|
||||
"POST https://api.example.com/v1/capsules",
|
||||
"DELETE https://api.example.com/v1/capsules/cap_created",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not destroy an already destroyed owned capsule twice", async () => {
|
||||
const { calls } = setupFetch([
|
||||
capsuleResponse("cap_created"),
|
||||
new Response(null, { status: 204 }),
|
||||
]);
|
||||
|
||||
const capsule = await Capsule.create({
|
||||
baseUrl: "https://api.example.com",
|
||||
});
|
||||
|
||||
await capsule.destroy();
|
||||
await capsule[Symbol.asyncDispose]();
|
||||
|
||||
expect(calls.map((call) => `${call.init.method} ${call.url}`)).toEqual([
|
||||
"POST https://api.example.com/v1/capsules",
|
||||
"DELETE https://api.example.com/v1/capsules/cap_created",
|
||||
]);
|
||||
});
|
||||
|
||||
it("exports Sandbox as a deprecated Capsule alias", () => {
|
||||
expect(Sandbox).toBe(Capsule);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user