@capybara-hq/testing 0.1.0-rc.4 → 0.1.0-rc.6
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/dist/fake-e2b.d.ts +34 -0
- package/dist/fake-e2b.js +95 -1
- package/dist/fake-git-server.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +5 -5
package/dist/fake-e2b.d.ts
CHANGED
|
@@ -19,6 +19,21 @@
|
|
|
19
19
|
* 本机只有一个用户,权限隔离测不了,测试改为核对请求里的用户)。进程以新会话启动(与 setsid 等价),
|
|
20
20
|
* 客户端断开**不会**结束进程(与 envd 一致),只有 SendSignal、杀进程脚本或删沙箱才会。
|
|
21
21
|
*
|
|
22
|
+
* - **Template build** (the routes `Template.build(Template().fromImage(image), alias)` calls, e2b 2.58.0
|
|
23
|
+
* `buildApi`; all need `X-API-KEY`): `POST /v3/templates` (body `{name, tags?, cpuCount?, memoryMB?}`,
|
|
24
|
+
* `name` may carry `:tag`; 202 `{templateID, buildID, names, aliases, tags, public}`),
|
|
25
|
+
* `GET /templates/{templateID}/files/{hash}` (upload link of a COPY step; `{present: true}`),
|
|
26
|
+
* `POST /v2/templates/{templateID}/builds/{buildID}` (body `{fromImage, steps, force, startCmd?,
|
|
27
|
+
* readyCmd?}`; 202, and {@link FakeE2BOptions.templateBuildMs} starts counting), `GET
|
|
28
|
+
* /templates/{templateID}/builds/{buildID}/status?logsOffset=` (`{buildID, templateID, status:
|
|
29
|
+
* waiting | building | ready, logEntries, logs}`), `GET /templates/aliases/{alias}` (200
|
|
30
|
+
* `{templateID, public}`, else 404). A template (including the seeded `templates`) creates
|
|
31
|
+
* sandboxes by alias or templateID; one still building answers 404; template names never
|
|
32
|
+
* registered are accepted as before.
|
|
33
|
+
* - **Pause**: `POST /sandboxes/{id}/pause` with `{memory: false}` (the SDK's `mode: "filesystem"`)
|
|
34
|
+
* ends all processes and keeps the files; otherwise (also an empty body) processes stay.
|
|
35
|
+
* {@link FakeE2BServer.removeSandbox} simulates a lost paused sandbox.
|
|
36
|
+
*
|
|
22
37
|
* `connect` 带 `memory: false`(SDK 的 `onResume: "reboot"`)时结束该沙箱的全部进程(冷启动)。
|
|
23
38
|
* SendSignal 只杀指定进程(不是进程组)。
|
|
24
39
|
*
|
|
@@ -32,6 +47,19 @@ export interface FakeE2BOptions {
|
|
|
32
47
|
pageSize?: number;
|
|
33
48
|
/** create / connect 返回的沙箱 `domain`(默认 null,即用客户端配置的域名)。 */
|
|
34
49
|
domain?: string | null;
|
|
50
|
+
/** Existing template aliases (treated as built). */
|
|
51
|
+
templates?: string[];
|
|
52
|
+
/** Milliseconds from triggering a template build to `ready` (default 0). */
|
|
53
|
+
templateBuildMs?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface FakeE2BTemplate {
|
|
56
|
+
templateID: string;
|
|
57
|
+
alias: string;
|
|
58
|
+
/** The image it was built from (undefined for a seeded template). */
|
|
59
|
+
image?: string | undefined;
|
|
60
|
+
buildID?: string | undefined;
|
|
61
|
+
/** When it becomes `ready` (ms timestamp); undefined until the build is triggered (waiting). */
|
|
62
|
+
readyAt?: number | undefined;
|
|
35
63
|
}
|
|
36
64
|
export interface FakeE2BSandbox {
|
|
37
65
|
id: string;
|
|
@@ -41,6 +69,8 @@ export interface FakeE2BSandbox {
|
|
|
41
69
|
/** 当前的超时(秒):建沙箱时给的,connect 只会把它调大。 */
|
|
42
70
|
timeout: number;
|
|
43
71
|
autoPause: boolean;
|
|
72
|
+
/** Whether the last pause kept memory (`pause` body `memory` is not false). */
|
|
73
|
+
pausedWithMemory?: boolean;
|
|
44
74
|
/** 建沙箱请求体里的出站设置(原样记录)。 */
|
|
45
75
|
allowInternetAccess?: boolean;
|
|
46
76
|
network?: unknown;
|
|
@@ -71,6 +101,8 @@ export interface FakeE2BServer {
|
|
|
71
101
|
url: string;
|
|
72
102
|
apiKey: string;
|
|
73
103
|
sandboxes: Map<string, FakeE2BSandbox>;
|
|
104
|
+
/** Templates by alias. */
|
|
105
|
+
templates: Map<string, FakeE2BTemplate>;
|
|
74
106
|
calls: FakeE2BCall[];
|
|
75
107
|
/**
|
|
76
108
|
* 下一个匹配的请求回 `status`:默认 E2B 的错误格式 JSON `{code: status, message}`;`raw` 时回纯文本
|
|
@@ -90,6 +122,8 @@ export interface FakeE2BServer {
|
|
|
90
122
|
withholdAccessToken: boolean;
|
|
91
123
|
/** 沙箱直接消失(杀掉它的进程、删掉根目录)。 */
|
|
92
124
|
expire(id: string): Promise<void>;
|
|
125
|
+
/** Same as {@link FakeE2BServer.expire}: simulates a lost paused sandbox (connect then answers 404). */
|
|
126
|
+
removeSandbox(id: string): Promise<void>;
|
|
93
127
|
/** 沙箱内路径在本机上的位置。 */
|
|
94
128
|
hostPath(id: string, sandboxPath: string): string;
|
|
95
129
|
/** 沙箱里还在跑的进程(由 Start 启动、尚未退出的)。 */
|
package/dist/fake-e2b.js
CHANGED
|
@@ -84,6 +84,12 @@ export async function startFakeE2B(opts = {}) {
|
|
|
84
84
|
await chmod(join(shims, "setsid"), 0o755);
|
|
85
85
|
const sandboxes = new Map();
|
|
86
86
|
const procs = new Map();
|
|
87
|
+
const templates = new Map();
|
|
88
|
+
for (const alias of opts.templates ?? [])
|
|
89
|
+
templates.set(alias, { templateID: `t${randomBytes(6).toString("hex")}`, alias });
|
|
90
|
+
const buildMs = opts.templateBuildMs ?? 0;
|
|
91
|
+
const templateByRef = (ref) => templates.get(ref) ?? [...templates.values()].find((t) => t.templateID === ref);
|
|
92
|
+
const ready = (t) => t.readyAt !== undefined && Date.now() >= t.readyAt;
|
|
87
93
|
const calls = [];
|
|
88
94
|
const fails = [];
|
|
89
95
|
const reboots = new Map();
|
|
@@ -122,12 +128,13 @@ export async function startFakeE2B(opts = {}) {
|
|
|
122
128
|
};
|
|
123
129
|
const newSandbox = async (body) => {
|
|
124
130
|
const id = `i${randomBytes(9).toString("hex")}`;
|
|
131
|
+
const tpl = templateByRef(String(body.templateID));
|
|
125
132
|
const root = join(base, id);
|
|
126
133
|
for (const d of ROOT_DIRS)
|
|
127
134
|
await mkdir(join(root, d), { recursive: true });
|
|
128
135
|
const s = {
|
|
129
136
|
id,
|
|
130
|
-
templateID: String(body.templateID),
|
|
137
|
+
templateID: tpl?.templateID ?? String(body.templateID),
|
|
131
138
|
metadata: body.metadata ?? {},
|
|
132
139
|
state: "running",
|
|
133
140
|
timeout: typeof body.timeout === "number" ? body.timeout : 300,
|
|
@@ -160,6 +167,11 @@ export async function startFakeE2B(opts = {}) {
|
|
|
160
167
|
sendJson(res, 400, { code: 400, message: "templateID is required" });
|
|
161
168
|
return;
|
|
162
169
|
}
|
|
170
|
+
const tpl = templateByRef(body.templateID);
|
|
171
|
+
if (tpl?.buildID !== undefined && !ready(tpl)) {
|
|
172
|
+
sendJson(res, 404, { code: 404, message: `template '${body.templateID}' is not ready` });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
163
175
|
const s = await newSandbox(body);
|
|
164
176
|
sendJson(res, 201, {
|
|
165
177
|
sandboxID: s.id,
|
|
@@ -235,9 +247,89 @@ export async function startFakeE2B(opts = {}) {
|
|
|
235
247
|
return;
|
|
236
248
|
}
|
|
237
249
|
s.state = "paused";
|
|
250
|
+
s.pausedWithMemory = body.memory !== false;
|
|
251
|
+
if (!s.pausedWithMemory) {
|
|
252
|
+
// filesystem-only snapshot: the processes are gone, the files stay
|
|
253
|
+
for (const p of procs.get(s.id) ?? [])
|
|
254
|
+
killGroup(p);
|
|
255
|
+
procs.delete(s.id);
|
|
256
|
+
}
|
|
238
257
|
sendJson(res, 204);
|
|
239
258
|
return;
|
|
240
259
|
}
|
|
260
|
+
if (method === "POST" && path === "/v3/templates") {
|
|
261
|
+
const name = typeof body.name === "string" ? body.name : "";
|
|
262
|
+
if (name === "") {
|
|
263
|
+
sendJson(res, 400, { code: 400, message: "name is required" });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const alias = name.split(":")[0];
|
|
267
|
+
const tags = Array.isArray(body.tags) ? body.tags : [];
|
|
268
|
+
const t = templates.get(alias) ?? {
|
|
269
|
+
templateID: `t${randomBytes(6).toString("hex")}`,
|
|
270
|
+
alias,
|
|
271
|
+
};
|
|
272
|
+
t.buildID = randomBytes(8).toString("hex");
|
|
273
|
+
t.readyAt = undefined;
|
|
274
|
+
t.image = undefined;
|
|
275
|
+
templates.set(alias, t);
|
|
276
|
+
sendJson(res, 202, {
|
|
277
|
+
templateID: t.templateID,
|
|
278
|
+
buildID: t.buildID,
|
|
279
|
+
names: [name],
|
|
280
|
+
aliases: [alias],
|
|
281
|
+
tags,
|
|
282
|
+
public: false,
|
|
283
|
+
});
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
m = /^\/templates\/([^/]+)\/files\/([^/]+)$/.exec(path);
|
|
287
|
+
if (m && method === "GET") {
|
|
288
|
+
sendJson(res, 200, { present: true });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
m = /^\/v2\/templates\/([^/]+)\/builds\/([^/]+)$/.exec(path);
|
|
292
|
+
if (m && method === "POST") {
|
|
293
|
+
const t = templateByRef(m[1]);
|
|
294
|
+
if (!t || t.buildID !== m[2]) {
|
|
295
|
+
sendJson(res, 404, { code: 404, message: "build not found" });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (typeof body.fromImage !== "string") {
|
|
299
|
+
sendJson(res, 400, { code: 400, message: "fromImage is required" });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
t.image = body.fromImage;
|
|
303
|
+
t.readyAt = Date.now() + buildMs;
|
|
304
|
+
sendJson(res, 202);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
m = /^\/templates\/([^/]+)\/builds\/([^/]+)\/status$/.exec(path);
|
|
308
|
+
if (m && method === "GET") {
|
|
309
|
+
const t = templateByRef(m[1]);
|
|
310
|
+
if (!t || t.buildID !== m[2]) {
|
|
311
|
+
sendJson(res, 404, { code: 404, message: "build not found" });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
sendJson(res, 200, {
|
|
315
|
+
buildID: t.buildID,
|
|
316
|
+
templateID: t.templateID,
|
|
317
|
+
status: t.readyAt === undefined ? "waiting" : ready(t) ? "ready" : "building",
|
|
318
|
+
logEntries: [],
|
|
319
|
+
logs: [],
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
m = /^\/templates\/aliases\/([^/]+)$/.exec(path);
|
|
324
|
+
if (m && method === "GET") {
|
|
325
|
+
const t = templates.get(decodeURIComponent(m[1]));
|
|
326
|
+
if (!t) {
|
|
327
|
+
sendJson(res, 404, { code: 404, message: `template '${m[1]}' not found` });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
sendJson(res, 200, { templateID: t.templateID, public: false });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
241
333
|
m = /^\/sandboxes\/([^/]+)$/.exec(path);
|
|
242
334
|
if (m && method === "DELETE") {
|
|
243
335
|
if (!sandboxes.has(m[1]))
|
|
@@ -454,6 +546,7 @@ export async function startFakeE2B(opts = {}) {
|
|
|
454
546
|
url,
|
|
455
547
|
apiKey,
|
|
456
548
|
sandboxes,
|
|
549
|
+
templates,
|
|
457
550
|
calls,
|
|
458
551
|
failNext(match, status, message = `fake failure ${status}`, opts = {}) {
|
|
459
552
|
fails.push({ match, status, message, raw: opts.raw === true });
|
|
@@ -469,6 +562,7 @@ export async function startFakeE2B(opts = {}) {
|
|
|
469
562
|
hangs.push(match);
|
|
470
563
|
},
|
|
471
564
|
expire: destroy,
|
|
565
|
+
removeSandbox: destroy,
|
|
472
566
|
hostPath(id, sandboxPath) {
|
|
473
567
|
const s = sandboxes.get(id);
|
|
474
568
|
if (!s)
|
package/dist/fake-git-server.js
CHANGED
|
@@ -129,8 +129,10 @@ function serveCgi(root, req, res) {
|
|
|
129
129
|
res.writeHead(h.status, h.headers);
|
|
130
130
|
res.write(head.subarray(h.bodyStart));
|
|
131
131
|
});
|
|
132
|
+
// A child that could not be spawned emits 'error' and then 'close': only the first writes a head
|
|
133
|
+
// (a second writeHead throws ERR_HTTP_HEADERS_SENT, an uncaught exception).
|
|
132
134
|
p.on("close", () => {
|
|
133
|
-
if (!headDone)
|
|
135
|
+
if (!headDone && !res.headersSent)
|
|
134
136
|
res.writeHead(502);
|
|
135
137
|
res.end();
|
|
136
138
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* package and the SDK: the database helpers and types, and model-visible texts, queue names and
|
|
4
4
|
* keys the kernel owns (assert on them instead of copying their values).
|
|
5
5
|
*/
|
|
6
|
-
export { type ConversationEvent, createDb, gatewayReplicasKey, type KernelConfig, type KernelSchemaDb, kernelDb, MAINTENANCE_QUEUE_NAME, runJobId, SANDBOX_LOCK_TTL_MS,
|
|
6
|
+
export { type ConversationEvent, createDb, gatewayReplicasKey, type KernelConfig, type KernelSchemaDb, kernelDb, MAINTENANCE_QUEUE_NAME, runJobId, SANDBOX_LOCK_TTL_MS, SANDBOX_RESTARTED_NOTICE, SANDBOX_STARTED_NOTICE, SANDBOX_SUSPEND_JOB, UNKNOWN_RESULT_TEXT, } from "@capybara-hq/kernel";
|
|
7
7
|
export { createFreshDatabase, startTestPostgres, startTestRedis, startTestS3, TEST_S3_IMAGE, type TestContainer, type TestS3, } from "./containers.js";
|
|
8
8
|
export { createDualReplicaHarness, type DualReplicaOptions, type ExtraReplicaHandle, type Harness, REPLICA_HEADER, type ReplicaHandle, type ReplicaResponse, replicaOf, } from "./dual-replica.js";
|
|
9
9
|
export { assertExtensionSecretsReachable, extensionDeploymentSecretValues, } from "./extension-config.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* package and the SDK: the database helpers and types, and model-visible texts, queue names and
|
|
4
4
|
* keys the kernel owns (assert on them instead of copying their values).
|
|
5
5
|
*/
|
|
6
|
-
export { createDb, gatewayReplicasKey, kernelDb, MAINTENANCE_QUEUE_NAME, runJobId, SANDBOX_LOCK_TTL_MS,
|
|
6
|
+
export { createDb, gatewayReplicasKey, kernelDb, MAINTENANCE_QUEUE_NAME, runJobId, SANDBOX_LOCK_TTL_MS, SANDBOX_RESTARTED_NOTICE, SANDBOX_STARTED_NOTICE, SANDBOX_SUSPEND_JOB, UNKNOWN_RESULT_TEXT, } from "@capybara-hq/kernel";
|
|
7
7
|
export { createFreshDatabase, startTestPostgres, startTestRedis, startTestS3, TEST_S3_IMAGE, } from "./containers.js";
|
|
8
8
|
export { createDualReplicaHarness, REPLICA_HEADER, replicaOf, } from "./dual-replica.js";
|
|
9
9
|
export { assertExtensionSecretsReachable, extensionDeploymentSecretValues, } from "./extension-config.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capybara-hq/testing",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.6",
|
|
4
4
|
"description": "Test helpers for Capybara: test containers, dual-replica harness and fake third-party servers",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -47,17 +47,17 @@
|
|
|
47
47
|
"tsx": "^4.23.15",
|
|
48
48
|
"ws": "^8.22.0",
|
|
49
49
|
"zod": "^4.6.7",
|
|
50
|
-
"@capybara-hq/kernel": "^0.1.0-rc.
|
|
50
|
+
"@capybara-hq/kernel": "^0.1.0-rc.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/pg": "^8.23.1",
|
|
54
54
|
"@types/ws": "^8.18.2",
|
|
55
|
-
"@capybara-hq/plugin-sdk": "^0.1.0-rc.
|
|
55
|
+
"@capybara-hq/plugin-sdk": "^0.1.0-rc.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"vitest": "^5.0.5",
|
|
59
|
-
"@capybara-hq/plugin-sdk": "^0.1.0-rc.
|
|
60
|
-
"@capybara-hq/plugin-tools": "^0.1.0-rc.
|
|
59
|
+
"@capybara-hq/plugin-sdk": "^0.1.0-rc.3",
|
|
60
|
+
"@capybara-hq/plugin-tools": "^0.1.0-rc.5"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"@capybara-hq/plugin-tools": {
|