@miosa/sdk 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/dist/index.d.ts +4689 -0
- package/dist/index.js +6045 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/src/client.ts +249 -0
- package/src/errors.ts +136 -0
- package/src/http.test.ts +374 -0
- package/src/http.ts +390 -0
- package/src/index.ts +452 -0
- package/src/resources/admin.ts +348 -0
- package/src/resources/analytics.ts +60 -0
- package/src/resources/api-keys.ts +119 -0
- package/src/resources/audit-log.ts +64 -0
- package/src/resources/benchmarks.ts +104 -0
- package/src/resources/builder-sessions.ts +75 -0
- package/src/resources/channels.ts +143 -0
- package/src/resources/checkpoints.ts +225 -0
- package/src/resources/command-center.ts +73 -0
- package/src/resources/community.ts +103 -0
- package/src/resources/completions.ts +104 -0
- package/src/resources/computer-auto-stop.ts +43 -0
- package/src/resources/computer-env.ts +76 -0
- package/src/resources/computer-logs.ts +50 -0
- package/src/resources/computer-osa.ts +76 -0
- package/src/resources/computer-ports.ts +91 -0
- package/src/resources/computer-terminal.ts +61 -0
- package/src/resources/computer-volumes.ts +64 -0
- package/src/resources/computer.ts +530 -0
- package/src/resources/computers.ts +75 -0
- package/src/resources/credits.ts +43 -0
- package/src/resources/cron-jobs.ts +191 -0
- package/src/resources/custom_domains.ts +123 -0
- package/src/resources/dashboard.ts +49 -0
- package/src/resources/databases.ts +218 -0
- package/src/resources/deployments.ts +777 -0
- package/src/resources/desktop.ts +134 -0
- package/src/resources/email.ts +212 -0
- package/src/resources/embeddings.ts +36 -0
- package/src/resources/events.ts +296 -0
- package/src/resources/exec.ts +319 -0
- package/src/resources/external-keys.ts +79 -0
- package/src/resources/files.test.ts +339 -0
- package/src/resources/files.ts +220 -0
- package/src/resources/flat-custom-domains.ts +127 -0
- package/src/resources/functions.ts +178 -0
- package/src/resources/health-checks.ts +165 -0
- package/src/resources/integrations.ts +183 -0
- package/src/resources/mcp.ts +70 -0
- package/src/resources/models.ts +45 -0
- package/src/resources/network_policy.ts +73 -0
- package/src/resources/open-computers/agents.ts +91 -0
- package/src/resources/open-computers/apps.ts +102 -0
- package/src/resources/open-computers/clusters.ts +88 -0
- package/src/resources/open-computers/desktop.ts +34 -0
- package/src/resources/open-computers/files.ts +97 -0
- package/src/resources/open-computers/hosts.ts +85 -0
- package/src/resources/open-computers/index.ts +98 -0
- package/src/resources/open-computers/jobs.ts +75 -0
- package/src/resources/open-computers/open_computers.test.ts +288 -0
- package/src/resources/open-computers/secrets.ts +115 -0
- package/src/resources/open-computers/terminal.ts +33 -0
- package/src/resources/open-computers/tunnels.ts +87 -0
- package/src/resources/open-computers/types.ts +343 -0
- package/src/resources/open-computers/workspaces.ts +135 -0
- package/src/resources/project-auth.ts +142 -0
- package/src/resources/project-integrations.ts +133 -0
- package/src/resources/provider-defaults.ts +89 -0
- package/src/resources/regions.ts +94 -0
- package/src/resources/sandbox-templates.ts +195 -0
- package/src/resources/sandboxes.live.test.ts +92 -0
- package/src/resources/sandboxes.test.ts +624 -0
- package/src/resources/sandboxes.ts +1173 -0
- package/src/resources/settings.ts +143 -0
- package/src/resources/snapshots-standalone.ts +51 -0
- package/src/resources/storage.ts +221 -0
- package/src/resources/tenant.ts +39 -0
- package/src/resources/usage.ts +85 -0
- package/src/resources/volumes.ts +117 -0
- package/src/resources/webhooks.ts +171 -0
- package/src/types.ts +460 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { MiosaError } from "../errors.js";
|
|
3
|
+
import { HttpClient } from "../http.js";
|
|
4
|
+
import { Sandbox, Sandboxes } from "./sandboxes.js";
|
|
5
|
+
|
|
6
|
+
const mockGet = vi.fn();
|
|
7
|
+
const mockPost = vi.fn();
|
|
8
|
+
const mockDelete = vi.fn();
|
|
9
|
+
const mockRequest = vi.fn();
|
|
10
|
+
const mockGetBinary = vi.fn();
|
|
11
|
+
const mockStream = vi.fn();
|
|
12
|
+
|
|
13
|
+
function makeHttp(): HttpClient {
|
|
14
|
+
const http = {} as HttpClient;
|
|
15
|
+
http.get = mockGet;
|
|
16
|
+
http.post = mockPost;
|
|
17
|
+
http.delete = mockDelete;
|
|
18
|
+
http.request = mockRequest;
|
|
19
|
+
http.getBinary = mockGetBinary;
|
|
20
|
+
http.stream = mockStream;
|
|
21
|
+
return http;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sandboxData(overrides: Record<string, unknown> = {}) {
|
|
25
|
+
return {
|
|
26
|
+
id: "sbx_123",
|
|
27
|
+
state: "running",
|
|
28
|
+
ready: true,
|
|
29
|
+
template_id: "miosa-sandbox",
|
|
30
|
+
cpu_count: 1,
|
|
31
|
+
memory_mb: 1024,
|
|
32
|
+
timeout_sec: 300,
|
|
33
|
+
metadata: {},
|
|
34
|
+
created_at: "2026-05-13T00:00:00Z",
|
|
35
|
+
started_at: "2026-05-13T00:00:01Z",
|
|
36
|
+
destroyed_at: null,
|
|
37
|
+
total_runtime_sec: null,
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("Sandboxes", () => {
|
|
47
|
+
it("create() calls native /sandboxes with template_id", async () => {
|
|
48
|
+
mockRequest.mockResolvedValue({ data: sandboxData() });
|
|
49
|
+
|
|
50
|
+
const client = new Sandboxes(makeHttp());
|
|
51
|
+
const sandbox = await client.create({
|
|
52
|
+
templateId: "nextjs",
|
|
53
|
+
cpuCount: 2,
|
|
54
|
+
memoryMb: 2048,
|
|
55
|
+
idempotencyKey: "idem-1",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(mockRequest).toHaveBeenCalledWith("/sandboxes", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
body: {
|
|
61
|
+
template_id: "nextjs",
|
|
62
|
+
cpu_count: 2,
|
|
63
|
+
memory_mb: 2048,
|
|
64
|
+
},
|
|
65
|
+
headers: { "Idempotency-Key": "idem-1" },
|
|
66
|
+
});
|
|
67
|
+
expect(sandbox).toBeInstanceOf(Sandbox);
|
|
68
|
+
expect(sandbox.id).toBe("sbx_123");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("create() defaults to miosa-sandbox", async () => {
|
|
72
|
+
mockRequest.mockResolvedValue(sandboxData());
|
|
73
|
+
|
|
74
|
+
await new Sandboxes(makeHttp()).create();
|
|
75
|
+
|
|
76
|
+
expect(mockRequest).toHaveBeenCalledWith("/sandboxes", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
body: { template_id: "miosa-sandbox" },
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("list() calls native /sandboxes and maps sandbox handles", async () => {
|
|
83
|
+
mockGet.mockResolvedValue({ data: [sandboxData()] });
|
|
84
|
+
|
|
85
|
+
const result = await new Sandboxes(makeHttp()).list({
|
|
86
|
+
state: "running",
|
|
87
|
+
tags: ["agent", "preview"],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(mockGet).toHaveBeenCalledWith("/sandboxes", {
|
|
91
|
+
state: "running",
|
|
92
|
+
tags: "agent,preview",
|
|
93
|
+
});
|
|
94
|
+
expect(result[0]).toBeInstanceOf(Sandbox);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("get() and connect() call native /sandboxes/:id", async () => {
|
|
98
|
+
mockGet.mockResolvedValue(sandboxData());
|
|
99
|
+
|
|
100
|
+
const client = new Sandboxes(makeHttp());
|
|
101
|
+
await client.get("sbx_123");
|
|
102
|
+
await client.connect("sbx_123");
|
|
103
|
+
|
|
104
|
+
expect(mockGet).toHaveBeenNthCalledWith(1, "/sandboxes/sbx_123");
|
|
105
|
+
expect(mockGet).toHaveBeenNthCalledWith(2, "/sandboxes/sbx_123");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("delete() calls native /sandboxes/:id", async () => {
|
|
109
|
+
mockDelete.mockResolvedValue(undefined);
|
|
110
|
+
|
|
111
|
+
await new Sandboxes(makeHttp()).delete("sbx_123");
|
|
112
|
+
|
|
113
|
+
expect(mockDelete).toHaveBeenCalledWith("/sandboxes/sbx_123");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("gets and validates the public BuildSpec contract", async () => {
|
|
117
|
+
mockGet.mockResolvedValue({ version: "2026-05-13" });
|
|
118
|
+
mockPost.mockResolvedValue({
|
|
119
|
+
valid: true,
|
|
120
|
+
build_spec: { from: "node:22-bookworm" },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const client = new Sandboxes(makeHttp());
|
|
124
|
+
const schema = await client.getBuildSpecSchema();
|
|
125
|
+
const validation = await client.validateBuildSpec({
|
|
126
|
+
from: "node:22-bookworm",
|
|
127
|
+
startCmd: "pnpm dev --host 0.0.0.0 --port 3000",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(mockGet).toHaveBeenCalledWith("/sandbox-templates/build-spec");
|
|
131
|
+
expect(mockPost).toHaveBeenCalledWith("/sandbox-templates/validate", {
|
|
132
|
+
build_spec: {
|
|
133
|
+
from: "node:22-bookworm",
|
|
134
|
+
startCmd: "pnpm dev --host 0.0.0.0 --port 3000",
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
expect(schema.version).toBe("2026-05-13");
|
|
138
|
+
expect(validation.valid).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("creates templates and queued template builds", async () => {
|
|
142
|
+
mockPost
|
|
143
|
+
.mockResolvedValueOnce({
|
|
144
|
+
data: {
|
|
145
|
+
id: "tpl_123",
|
|
146
|
+
name: "Agent Web App",
|
|
147
|
+
slug: "agent-web-app",
|
|
148
|
+
built_in: false,
|
|
149
|
+
status: "draft",
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
.mockResolvedValueOnce({
|
|
153
|
+
data: {
|
|
154
|
+
id: "build_123",
|
|
155
|
+
sandbox_template_id: "tpl_123",
|
|
156
|
+
source_type: "build_spec",
|
|
157
|
+
state: "queued",
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const client = new Sandboxes(makeHttp());
|
|
162
|
+
const template = await client.createTemplate({
|
|
163
|
+
name: "Agent Web App",
|
|
164
|
+
slug: "agent-web-app",
|
|
165
|
+
buildSpec: { from: "node:22-bookworm" },
|
|
166
|
+
});
|
|
167
|
+
const build = await client.createTemplateBuild(template.id);
|
|
168
|
+
|
|
169
|
+
expect(mockPost).toHaveBeenNthCalledWith(1, "/sandbox-templates", {
|
|
170
|
+
name: "Agent Web App",
|
|
171
|
+
slug: "agent-web-app",
|
|
172
|
+
build_spec: { from: "node:22-bookworm" },
|
|
173
|
+
});
|
|
174
|
+
expect(mockPost).toHaveBeenNthCalledWith(
|
|
175
|
+
2,
|
|
176
|
+
"/sandbox-templates/tpl_123/builds",
|
|
177
|
+
{},
|
|
178
|
+
);
|
|
179
|
+
expect(template.id).toBe("tpl_123");
|
|
180
|
+
expect(build.state).toBe("queued");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("lists and gets template builds", async () => {
|
|
184
|
+
mockGet
|
|
185
|
+
.mockResolvedValueOnce({
|
|
186
|
+
data: [
|
|
187
|
+
{
|
|
188
|
+
id: "build_123",
|
|
189
|
+
sandbox_template_id: "tpl_123",
|
|
190
|
+
source_type: "build_spec",
|
|
191
|
+
state: "queued",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
})
|
|
195
|
+
.mockResolvedValueOnce({
|
|
196
|
+
data: {
|
|
197
|
+
id: "build_123",
|
|
198
|
+
sandbox_template_id: "tpl_123",
|
|
199
|
+
source_type: "build_spec",
|
|
200
|
+
state: "queued",
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const client = new Sandboxes(makeHttp());
|
|
205
|
+
const builds = await client.listTemplateBuilds("tpl_123");
|
|
206
|
+
const build = await client.getTemplateBuild("build_123");
|
|
207
|
+
|
|
208
|
+
expect(mockGet).toHaveBeenNthCalledWith(
|
|
209
|
+
1,
|
|
210
|
+
"/sandbox-templates/tpl_123/builds",
|
|
211
|
+
);
|
|
212
|
+
expect(mockGet).toHaveBeenNthCalledWith(
|
|
213
|
+
2,
|
|
214
|
+
"/sandbox-template-builds/build_123",
|
|
215
|
+
);
|
|
216
|
+
expect(builds[0]?.id).toBe("build_123");
|
|
217
|
+
expect(build.id).toBe("build_123");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("Sandbox handle", () => {
|
|
222
|
+
it("exec() posts to native /sandboxes/:id/exec", async () => {
|
|
223
|
+
mockPost.mockResolvedValue({
|
|
224
|
+
data: { stdout: "ok\n", stderr: "", exit_code: 0, duration_ms: 12 },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
228
|
+
const result = await sandbox.exec("echo ok", {
|
|
229
|
+
workingDir: "/workspace",
|
|
230
|
+
timeoutSec: 5,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(mockPost).toHaveBeenCalledWith("/sandboxes/sbx_123/exec", {
|
|
234
|
+
command: "echo ok",
|
|
235
|
+
cwd: "/workspace",
|
|
236
|
+
timeout: 5,
|
|
237
|
+
});
|
|
238
|
+
expect(result.stdout).toBe("ok\n");
|
|
239
|
+
expect(result.exitCode).toBe(0);
|
|
240
|
+
expect(result.exit_code).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("commands.run() delegates to exec()", async () => {
|
|
244
|
+
mockPost.mockResolvedValue({
|
|
245
|
+
data: { stdout: "ok", stderr: "", exit_code: 0 },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
249
|
+
await sandbox.commands.run("echo ok");
|
|
250
|
+
|
|
251
|
+
expect(mockPost).toHaveBeenCalledWith("/sandboxes/sbx_123/exec", {
|
|
252
|
+
command: "echo ok",
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("exec.run() keeps callable exec parity", async () => {
|
|
257
|
+
mockPost.mockResolvedValue({
|
|
258
|
+
data: { stdout: "ok", stderr: "", exit_code: 0 },
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
262
|
+
await sandbox.exec.run("echo ok");
|
|
263
|
+
await sandbox.exec("echo ok");
|
|
264
|
+
|
|
265
|
+
expect(mockPost).toHaveBeenNthCalledWith(1, "/sandboxes/sbx_123/exec", {
|
|
266
|
+
command: "echo ok",
|
|
267
|
+
});
|
|
268
|
+
expect(mockPost).toHaveBeenNthCalledWith(2, "/sandboxes/sbx_123/exec", {
|
|
269
|
+
command: "echo ok",
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("exec.stream() opens native sandbox exec SSE", () => {
|
|
274
|
+
const iter = (async function* () {})();
|
|
275
|
+
mockStream.mockReturnValue(iter);
|
|
276
|
+
|
|
277
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
278
|
+
const stream = sandbox.exec.stream("pnpm test", {
|
|
279
|
+
cwd: "/workspace",
|
|
280
|
+
timeout: 30,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(stream).toBe(iter);
|
|
284
|
+
expect(mockStream).toHaveBeenCalledWith("/sandboxes/sbx_123/exec/stream", {
|
|
285
|
+
method: "POST",
|
|
286
|
+
body: {
|
|
287
|
+
command: "pnpm test",
|
|
288
|
+
cwd: "/workspace",
|
|
289
|
+
timeout: 30,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("files.write() writes through native sandbox file API", async () => {
|
|
295
|
+
mockPost.mockResolvedValue({});
|
|
296
|
+
|
|
297
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
298
|
+
await sandbox.files.write("/workspace/app.txt", "hello");
|
|
299
|
+
|
|
300
|
+
expect(mockPost).toHaveBeenCalledWith("/sandboxes/sbx_123/files", {
|
|
301
|
+
path: "/workspace/app.txt",
|
|
302
|
+
content: "aGVsbG8=",
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("files.list() and files.stat() use native sandbox file endpoints", async () => {
|
|
307
|
+
mockGet.mockResolvedValue({ data: { path: "/workspace", entries: [] } });
|
|
308
|
+
mockPost.mockResolvedValue({
|
|
309
|
+
data: { path: "/workspace/app.txt", size: 5 },
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
313
|
+
const files = await sandbox.files.list("/workspace");
|
|
314
|
+
const stat = await sandbox.files.stat("/workspace/app.txt");
|
|
315
|
+
|
|
316
|
+
expect(mockGet).toHaveBeenCalledWith("/sandboxes/sbx_123/files", {
|
|
317
|
+
path: "/workspace",
|
|
318
|
+
});
|
|
319
|
+
expect(mockPost).toHaveBeenCalledWith("/sandboxes/sbx_123/files/stat", {
|
|
320
|
+
path: "/workspace/app.txt",
|
|
321
|
+
});
|
|
322
|
+
expect(files.entries).toEqual([]);
|
|
323
|
+
expect(stat.size).toBe(5);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("preview.expose() returns native sandbox preview URL", async () => {
|
|
327
|
+
mockPost.mockResolvedValue({
|
|
328
|
+
data: { url: "https://5173-sbx.sandbox.miosa.app" },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
332
|
+
const url = await sandbox.preview.expose(5173);
|
|
333
|
+
|
|
334
|
+
expect(mockPost).toHaveBeenCalledWith("/sandboxes/sbx_123/expose", {
|
|
335
|
+
port: 5173,
|
|
336
|
+
});
|
|
337
|
+
expect(url).toBe("https://5173-sbx.sandbox.miosa.app");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("logs get/stream use native sandbox log endpoints", async () => {
|
|
341
|
+
const iter = (async function* () {})();
|
|
342
|
+
mockGet.mockResolvedValue({ data: { lines: ["ok"] } });
|
|
343
|
+
mockStream.mockReturnValue(iter);
|
|
344
|
+
|
|
345
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
346
|
+
const logs = await sandbox.logs.get(100);
|
|
347
|
+
const stream = sandbox.logs.stream();
|
|
348
|
+
|
|
349
|
+
expect(mockGet).toHaveBeenCalledWith("/sandboxes/sbx_123/logs", {
|
|
350
|
+
lines: 100,
|
|
351
|
+
});
|
|
352
|
+
expect(mockStream).toHaveBeenCalledWith("/sandboxes/sbx_123/logs/stream");
|
|
353
|
+
expect(logs).toEqual({ lines: ["ok"] });
|
|
354
|
+
expect(stream).toBe(iter);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("snapshots create/list/restore/delete use native sandbox snapshot endpoints", async () => {
|
|
358
|
+
mockPost
|
|
359
|
+
.mockResolvedValueOnce({ data: { id: "snap_1", sandbox_id: "sbx_123" } })
|
|
360
|
+
.mockResolvedValueOnce({ data: sandboxData({ id: "sbx_restored" }) });
|
|
361
|
+
mockGet.mockResolvedValue({
|
|
362
|
+
data: [{ id: "snap_1", sandbox_id: "sbx_123" }],
|
|
363
|
+
});
|
|
364
|
+
mockDelete.mockResolvedValue(undefined);
|
|
365
|
+
|
|
366
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
367
|
+
const created = await sandbox.snapshots.create("checkpoint");
|
|
368
|
+
const snapshots = await sandbox.snapshots.list();
|
|
369
|
+
const restored = await sandbox.snapshots.restore("snap_1");
|
|
370
|
+
await sandbox.snapshots.delete("snap_1");
|
|
371
|
+
|
|
372
|
+
expect(mockPost).toHaveBeenNthCalledWith(
|
|
373
|
+
1,
|
|
374
|
+
"/sandboxes/sbx_123/snapshots",
|
|
375
|
+
{
|
|
376
|
+
comment: "checkpoint",
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
expect(mockGet).toHaveBeenCalledWith("/sandboxes/sbx_123/snapshots");
|
|
380
|
+
expect(mockPost).toHaveBeenNthCalledWith(
|
|
381
|
+
2,
|
|
382
|
+
"/sandboxes/sbx_123/restore/snap_1",
|
|
383
|
+
{},
|
|
384
|
+
);
|
|
385
|
+
expect(mockDelete).toHaveBeenCalledWith(
|
|
386
|
+
"/sandboxes/sbx_123/snapshots/snap_1",
|
|
387
|
+
);
|
|
388
|
+
expect(created.id).toBe("snap_1");
|
|
389
|
+
expect(snapshots[0]?.id).toBe("snap_1");
|
|
390
|
+
expect(restored.id).toBe("sbx_restored");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("pause/resume/deploy use native sandbox lifecycle endpoints", async () => {
|
|
394
|
+
mockPost
|
|
395
|
+
.mockResolvedValueOnce({ data: sandboxData({ state: "paused" }) })
|
|
396
|
+
.mockResolvedValueOnce({ data: sandboxData({ state: "running" }) })
|
|
397
|
+
.mockResolvedValueOnce({
|
|
398
|
+
data: { deployment_id: "dep_1", url: "https://app.miosa.app" },
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
402
|
+
await sandbox.pause();
|
|
403
|
+
await sandbox.resume();
|
|
404
|
+
const deployment = await sandbox.deploy({
|
|
405
|
+
name: "site",
|
|
406
|
+
sourcePath: "/workspace/dist",
|
|
407
|
+
customDomain: "example.com",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
expect(mockPost).toHaveBeenNthCalledWith(1, "/sandboxes/sbx_123/pause", {});
|
|
411
|
+
expect(mockPost).toHaveBeenNthCalledWith(
|
|
412
|
+
2,
|
|
413
|
+
"/sandboxes/sbx_123/resume",
|
|
414
|
+
{},
|
|
415
|
+
);
|
|
416
|
+
expect(mockPost).toHaveBeenNthCalledWith(3, "/sandboxes/sbx_123/deploy", {
|
|
417
|
+
name: "site",
|
|
418
|
+
path: "/workspace/dist",
|
|
419
|
+
custom_domain: "example.com",
|
|
420
|
+
});
|
|
421
|
+
expect(deployment.deployment_id).toBe("dep_1");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("throws before operations when not running", async () => {
|
|
425
|
+
const sandbox = new Sandbox(
|
|
426
|
+
makeHttp(),
|
|
427
|
+
sandboxData({ state: "provisioning" }),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
await expect(sandbox.exec("echo no")).rejects.toBeInstanceOf(MiosaError);
|
|
431
|
+
expect(mockPost).not.toHaveBeenCalled();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("keeps the canonical sandbox surface off /computers", async () => {
|
|
435
|
+
const paths: string[] = [];
|
|
436
|
+
const iter = (async function* () {})();
|
|
437
|
+
const http = {
|
|
438
|
+
request: vi.fn(async (path: string) => {
|
|
439
|
+
paths.push(path);
|
|
440
|
+
return { data: sandboxData() };
|
|
441
|
+
}),
|
|
442
|
+
get: vi.fn(async (path: string) => {
|
|
443
|
+
paths.push(path);
|
|
444
|
+
if (path.endsWith("/files"))
|
|
445
|
+
return { data: { path: "/workspace", entries: [] } };
|
|
446
|
+
if (path.endsWith("/logs")) return { data: { lines: [] } };
|
|
447
|
+
if (path.endsWith("/snapshots")) return { data: [] };
|
|
448
|
+
if (path === "/sandbox-templates") return { data: [] };
|
|
449
|
+
if (path.startsWith("/sandbox-templates/"))
|
|
450
|
+
return { id: "miosa-sandbox" };
|
|
451
|
+
return { data: sandboxData() };
|
|
452
|
+
}),
|
|
453
|
+
post: vi.fn(async (path: string) => {
|
|
454
|
+
paths.push(path);
|
|
455
|
+
if (path.endsWith("/exec")) {
|
|
456
|
+
return { data: { stdout: "ok\n", stderr: "", exit_code: 0 } };
|
|
457
|
+
}
|
|
458
|
+
if (path.endsWith("/expose"))
|
|
459
|
+
return { data: { url: "https://sbx.sandbox.miosa.app" } };
|
|
460
|
+
if (path.endsWith("/artifacts")) return { data: { artifacts: {} } };
|
|
461
|
+
if (path.endsWith("/files/stat"))
|
|
462
|
+
return { data: { path: "/workspace/app.txt", size: 2 } };
|
|
463
|
+
if (path.endsWith("/snapshots")) return { data: { id: "snap_1" } };
|
|
464
|
+
if (path.includes("/restore/"))
|
|
465
|
+
return { data: sandboxData({ id: "sbx_restored" }) };
|
|
466
|
+
if (path.endsWith("/deploy"))
|
|
467
|
+
return { data: { deployment_id: "dep_1" } };
|
|
468
|
+
return { data: sandboxData() };
|
|
469
|
+
}),
|
|
470
|
+
delete: vi.fn(async (path: string) => {
|
|
471
|
+
paths.push(path);
|
|
472
|
+
}),
|
|
473
|
+
getBinary: vi.fn(async (path: string) => {
|
|
474
|
+
paths.push(path);
|
|
475
|
+
return new TextEncoder().encode("ok");
|
|
476
|
+
}),
|
|
477
|
+
stream: vi.fn((path: string) => {
|
|
478
|
+
paths.push(path);
|
|
479
|
+
return iter;
|
|
480
|
+
}),
|
|
481
|
+
} as unknown as HttpClient;
|
|
482
|
+
|
|
483
|
+
const client = new Sandboxes(http);
|
|
484
|
+
await client.create();
|
|
485
|
+
await client.list();
|
|
486
|
+
await client.get("sbx_123");
|
|
487
|
+
await client.connect("sbx_123");
|
|
488
|
+
await client.delete("sbx_123");
|
|
489
|
+
await client.listTemplates();
|
|
490
|
+
await client.getTemplate("miosa-sandbox");
|
|
491
|
+
|
|
492
|
+
const sandbox = new Sandbox(http, sandboxData());
|
|
493
|
+
await sandbox.commands.run("echo ok");
|
|
494
|
+
sandbox.commands.stream("echo ok");
|
|
495
|
+
await sandbox.exec.run("echo ok");
|
|
496
|
+
sandbox.exec.stream("echo ok");
|
|
497
|
+
await sandbox.files.write("/workspace/app.txt", "ok");
|
|
498
|
+
await sandbox.files.readText("/workspace/app.txt");
|
|
499
|
+
await sandbox.files.list("/workspace");
|
|
500
|
+
await sandbox.files.stat("/workspace/app.txt");
|
|
501
|
+
await sandbox.preview.expose(5173);
|
|
502
|
+
await sandbox.artifacts.list();
|
|
503
|
+
await sandbox.logs.get(10);
|
|
504
|
+
sandbox.logs.stream();
|
|
505
|
+
await sandbox.snapshots.create("checkpoint");
|
|
506
|
+
await sandbox.snapshots.list();
|
|
507
|
+
await sandbox.snapshots.restore("snap_1");
|
|
508
|
+
await sandbox.snapshots.delete("snap_1");
|
|
509
|
+
await sandbox.pause();
|
|
510
|
+
await sandbox.resume();
|
|
511
|
+
await sandbox.deploy({ name: "site", sourcePath: "/workspace/dist" });
|
|
512
|
+
|
|
513
|
+
expect(paths).not.toEqual(
|
|
514
|
+
expect.arrayContaining([expect.stringContaining("/computers")]),
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// ─── waitUntilReady() ──────────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
function makeHttpForStream(): HttpClient {
|
|
522
|
+
const http = makeHttp();
|
|
523
|
+
// Public properties used by the SSE fetch fallback. Cast through unknown to
|
|
524
|
+
// assign readonly fields on the mocked instance.
|
|
525
|
+
(http as unknown as { baseUrl: string }).baseUrl =
|
|
526
|
+
"https://api.miosa.test/api/v1";
|
|
527
|
+
(http as unknown as { apiKey: string }).apiKey = "msk_u_test";
|
|
528
|
+
return http;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function sseResponse(body: string): Response {
|
|
532
|
+
const encoder = new TextEncoder();
|
|
533
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
534
|
+
start(controller) {
|
|
535
|
+
controller.enqueue(encoder.encode(body));
|
|
536
|
+
controller.close();
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
return new Response(stream, {
|
|
540
|
+
status: 200,
|
|
541
|
+
headers: { "content-type": "text/event-stream" },
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
describe("Sandbox.waitUntilReady", () => {
|
|
546
|
+
const originalFetch = globalThis.fetch;
|
|
547
|
+
afterEach(() => {
|
|
548
|
+
globalThis.fetch = originalFetch;
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("returns true when SSE emits event: ready", async () => {
|
|
552
|
+
const fetchMock = vi
|
|
553
|
+
.fn()
|
|
554
|
+
.mockResolvedValue(
|
|
555
|
+
sseResponse(
|
|
556
|
+
': keepalive\n\nevent: ready\ndata: {"ready_at":"2026-05-18T00:00:00Z"}\n\n',
|
|
557
|
+
),
|
|
558
|
+
);
|
|
559
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
560
|
+
|
|
561
|
+
const sandbox = new Sandbox(makeHttpForStream(), sandboxData());
|
|
562
|
+
const result = await sandbox.waitUntilReady({ timeout: 5 });
|
|
563
|
+
|
|
564
|
+
expect(result).toBe(true);
|
|
565
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
566
|
+
"https://api.miosa.test/api/v1/sandboxes/sbx_123/readiness/stream",
|
|
567
|
+
expect.objectContaining({
|
|
568
|
+
method: "GET",
|
|
569
|
+
headers: expect.objectContaining({
|
|
570
|
+
Authorization: "Bearer msk_u_test",
|
|
571
|
+
Accept: "text/event-stream",
|
|
572
|
+
}),
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
575
|
+
expect(mockGet).not.toHaveBeenCalled();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("returns false when SSE emits event: timeout", async () => {
|
|
579
|
+
globalThis.fetch = vi
|
|
580
|
+
.fn()
|
|
581
|
+
.mockResolvedValue(
|
|
582
|
+
sseResponse('event: timeout\ndata: {"reason":"not_ready"}\n\n'),
|
|
583
|
+
) as unknown as typeof fetch;
|
|
584
|
+
|
|
585
|
+
const sandbox = new Sandbox(makeHttpForStream(), sandboxData());
|
|
586
|
+
const result = await sandbox.waitUntilReady({ timeout: 5 });
|
|
587
|
+
|
|
588
|
+
expect(result).toBe(false);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("falls back to polling when SSE returns 404", async () => {
|
|
592
|
+
globalThis.fetch = vi.fn().mockResolvedValue(
|
|
593
|
+
new Response('{"error":"not implemented"}', {
|
|
594
|
+
status: 404,
|
|
595
|
+
headers: { "content-type": "application/json" },
|
|
596
|
+
}),
|
|
597
|
+
) as unknown as typeof fetch;
|
|
598
|
+
|
|
599
|
+
// First poll: not ready. Second poll: ready.
|
|
600
|
+
mockGet
|
|
601
|
+
.mockResolvedValueOnce({ data: { ready: false } })
|
|
602
|
+
.mockResolvedValueOnce({ data: { ready: true } });
|
|
603
|
+
|
|
604
|
+
const sandbox = new Sandbox(makeHttpForStream(), sandboxData());
|
|
605
|
+
const result = await sandbox.waitUntilReady({ timeout: 2 });
|
|
606
|
+
|
|
607
|
+
expect(result).toBe(true);
|
|
608
|
+
expect(mockGet).toHaveBeenCalledWith("/sandboxes/sbx_123/readiness");
|
|
609
|
+
expect(mockGet.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("stream:false skips SSE and only polls", async () => {
|
|
613
|
+
const fetchMock = vi.fn();
|
|
614
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
615
|
+
mockGet.mockResolvedValue({ data: { ready: true } });
|
|
616
|
+
|
|
617
|
+
const sandbox = new Sandbox(makeHttpForStream(), sandboxData());
|
|
618
|
+
const result = await sandbox.waitUntilReady({ timeout: 2, stream: false });
|
|
619
|
+
|
|
620
|
+
expect(result).toBe(true);
|
|
621
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
622
|
+
expect(mockGet).toHaveBeenCalledWith("/sandboxes/sbx_123/readiness");
|
|
623
|
+
});
|
|
624
|
+
});
|