@poncho-ai/harness 0.34.1 → 0.36.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/.turbo/turbo-build.log +12 -11
- package/.turbo/turbo-lint.log +6 -0
- package/.turbo/turbo-test.log +27100 -0
- package/CHANGELOG.md +37 -0
- package/dist/chunk-MCKGQKYU.js +15 -0
- package/dist/dist-3KMQR4IO.js +27092 -0
- package/dist/index.d.ts +553 -29
- package/dist/index.js +3132 -1902
- package/dist/isolate-5MISBSUK.js +733 -0
- package/dist/isolate-5R6762YA.js +605 -0
- package/dist/isolate-KUZ5NOPG.js +727 -0
- package/dist/isolate-LOL3T7RA.js +729 -0
- package/dist/isolate-N22X4TCE.js +740 -0
- package/dist/isolate-T7WXM7IL.js +1490 -0
- package/dist/isolate-TCWTUVG4.js +1532 -0
- package/dist/isolate-WFOLANOB.js +768 -0
- package/package.json +24 -4
- package/scripts/migrate-to-engine.mjs +556 -0
- package/src/config.ts +112 -1
- package/src/harness.ts +282 -91
- package/src/index.ts +7 -0
- package/src/isolate/bindings.ts +206 -0
- package/src/isolate/bundler.ts +179 -0
- package/src/isolate/index.ts +10 -0
- package/src/isolate/polyfills.ts +796 -0
- package/src/isolate/run-code-tool.ts +220 -0
- package/src/isolate/runtime.ts +286 -0
- package/src/isolate/type-stubs.ts +196 -0
- package/src/mcp.ts +140 -9
- package/src/memory.ts +142 -191
- package/src/reminder-store.ts +7 -235
- package/src/reminder-tools.ts +15 -2
- package/src/secrets-store.ts +163 -0
- package/src/state.ts +22 -1291
- package/src/storage/engine.ts +106 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/memory-engine.ts +588 -0
- package/src/storage/postgres-engine.ts +139 -0
- package/src/storage/schema.ts +145 -0
- package/src/storage/sql-dialect.ts +963 -0
- package/src/storage/sqlite-engine.ts +99 -0
- package/src/storage/store-adapters.ts +100 -0
- package/src/subagent-manager.ts +1 -0
- package/src/subagent-tools.ts +1 -0
- package/src/telemetry.ts +5 -1
- package/src/tenant-token.ts +42 -0
- package/src/todo-tools.ts +1 -136
- package/src/upload-store.ts +1 -0
- package/src/vfs/bash-manager.ts +120 -0
- package/src/vfs/bash-tool.ts +59 -0
- package/src/vfs/create-bash-fs.ts +32 -0
- package/src/vfs/edit-file-tool.ts +72 -0
- package/src/vfs/index.ts +5 -0
- package/src/vfs/poncho-fs-adapter.ts +267 -0
- package/src/vfs/protected-fs.ts +177 -0
- package/src/vfs/read-file-tool.ts +103 -0
- package/src/vfs/write-file-tool.ts +49 -0
- package/test/harness.test.ts +30 -36
- package/test/isolate-vfs.test.ts +453 -0
- package/test/isolate.test.ts +252 -0
- package/test/state.test.ts +4 -27
- package/test/storage-engine.test.ts +250 -0
- package/test/vfs.test.ts +242 -0
- package/src/kv-store.ts +0 -216
package/test/harness.test.ts
CHANGED
|
@@ -37,10 +37,9 @@ model:
|
|
|
37
37
|
await harness.initialize();
|
|
38
38
|
const names = harness.listTools().map((tool) => tool.name);
|
|
39
39
|
|
|
40
|
-
expect(names).toContain("
|
|
41
|
-
expect(names).toContain("
|
|
42
|
-
expect(names).toContain("
|
|
43
|
-
expect(names).toContain("edit_file");
|
|
40
|
+
expect(names).toContain("bash");
|
|
41
|
+
expect(names).toContain("web_search");
|
|
42
|
+
expect(names).toContain("todo_list");
|
|
44
43
|
});
|
|
45
44
|
|
|
46
45
|
it("disables write_file by default in production environment", async () => {
|
|
@@ -63,10 +62,9 @@ model:
|
|
|
63
62
|
await harness.initialize();
|
|
64
63
|
const names = harness.listTools().map((tool) => tool.name);
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
expect(names).toContain("
|
|
68
|
-
expect(names).not.toContain("
|
|
69
|
-
expect(names).not.toContain("edit_file");
|
|
65
|
+
// In production, bash is registered but poncho_docs is not
|
|
66
|
+
expect(names).toContain("bash");
|
|
67
|
+
expect(names).not.toContain("poncho_docs");
|
|
70
68
|
});
|
|
71
69
|
|
|
72
70
|
it("allows disabling built-in tools via poncho.config.js", async () => {
|
|
@@ -88,9 +86,7 @@ model:
|
|
|
88
86
|
join(dir, "poncho.config.js"),
|
|
89
87
|
`export default {
|
|
90
88
|
tools: {
|
|
91
|
-
|
|
92
|
-
read_file: false
|
|
93
|
-
}
|
|
89
|
+
web_search: false
|
|
94
90
|
}
|
|
95
91
|
};
|
|
96
92
|
`,
|
|
@@ -100,8 +96,8 @@ model:
|
|
|
100
96
|
const harness = new AgentHarness({ workingDir: dir, environment: "production" });
|
|
101
97
|
await harness.initialize();
|
|
102
98
|
const names = harness.listTools().map((tool) => tool.name);
|
|
103
|
-
expect(names).toContain("
|
|
104
|
-
expect(names).not.toContain("
|
|
99
|
+
expect(names).toContain("bash");
|
|
100
|
+
expect(names).not.toContain("web_search");
|
|
105
101
|
});
|
|
106
102
|
|
|
107
103
|
it("supports per-environment tool overrides", async () => {
|
|
@@ -123,15 +119,13 @@ model:
|
|
|
123
119
|
join(dir, "poncho.config.js"),
|
|
124
120
|
`export default {
|
|
125
121
|
tools: {
|
|
126
|
-
|
|
127
|
-
read_file: false
|
|
128
|
-
},
|
|
122
|
+
web_search: false,
|
|
129
123
|
byEnvironment: {
|
|
130
124
|
development: {
|
|
131
|
-
|
|
125
|
+
web_search: true
|
|
132
126
|
},
|
|
133
127
|
production: {
|
|
134
|
-
|
|
128
|
+
web_fetch: false
|
|
135
129
|
}
|
|
136
130
|
}
|
|
137
131
|
}
|
|
@@ -143,14 +137,14 @@ model:
|
|
|
143
137
|
const developmentHarness = new AgentHarness({ workingDir: dir, environment: "development" });
|
|
144
138
|
await developmentHarness.initialize();
|
|
145
139
|
const developmentTools = developmentHarness.listTools().map((tool) => tool.name);
|
|
146
|
-
expect(developmentTools).toContain("
|
|
147
|
-
expect(developmentTools).toContain("
|
|
140
|
+
expect(developmentTools).toContain("web_search");
|
|
141
|
+
expect(developmentTools).toContain("bash");
|
|
148
142
|
|
|
149
143
|
const productionHarness = new AgentHarness({ workingDir: dir, environment: "production" });
|
|
150
144
|
await productionHarness.initialize();
|
|
151
145
|
const productionTools = productionHarness.listTools().map((tool) => tool.name);
|
|
152
|
-
expect(productionTools).not.toContain("
|
|
153
|
-
expect(productionTools).not.toContain("
|
|
146
|
+
expect(productionTools).not.toContain("web_search");
|
|
147
|
+
expect(productionTools).not.toContain("web_fetch");
|
|
154
148
|
});
|
|
155
149
|
|
|
156
150
|
it("does not auto-register exported tool objects from skill scripts", async () => {
|
|
@@ -1410,8 +1404,8 @@ model:
|
|
|
1410
1404
|
join(dir, "poncho.config.js"),
|
|
1411
1405
|
`export default {
|
|
1412
1406
|
tools: {
|
|
1413
|
-
|
|
1414
|
-
|
|
1407
|
+
web_search: false,
|
|
1408
|
+
web_fetch: true,
|
|
1415
1409
|
}
|
|
1416
1410
|
};
|
|
1417
1411
|
`,
|
|
@@ -1421,8 +1415,8 @@ model:
|
|
|
1421
1415
|
const harness = new AgentHarness({ workingDir: dir });
|
|
1422
1416
|
await harness.initialize();
|
|
1423
1417
|
const names = harness.listTools().map((tool) => tool.name);
|
|
1424
|
-
expect(names).toContain("
|
|
1425
|
-
expect(names).not.toContain("
|
|
1418
|
+
expect(names).toContain("web_fetch");
|
|
1419
|
+
expect(names).not.toContain("web_search");
|
|
1426
1420
|
});
|
|
1427
1421
|
|
|
1428
1422
|
it("flat tool access takes priority over legacy defaults", async () => {
|
|
@@ -1444,9 +1438,9 @@ model:
|
|
|
1444
1438
|
join(dir, "poncho.config.js"),
|
|
1445
1439
|
`export default {
|
|
1446
1440
|
tools: {
|
|
1447
|
-
|
|
1441
|
+
web_search: true,
|
|
1448
1442
|
defaults: {
|
|
1449
|
-
|
|
1443
|
+
web_search: false,
|
|
1450
1444
|
},
|
|
1451
1445
|
}
|
|
1452
1446
|
};
|
|
@@ -1457,7 +1451,7 @@ model:
|
|
|
1457
1451
|
const harness = new AgentHarness({ workingDir: dir });
|
|
1458
1452
|
await harness.initialize();
|
|
1459
1453
|
const names = harness.listTools().map((tool) => tool.name);
|
|
1460
|
-
expect(names).toContain("
|
|
1454
|
+
expect(names).toContain("web_search");
|
|
1461
1455
|
});
|
|
1462
1456
|
|
|
1463
1457
|
it("byEnvironment overrides flat tool access", async () => {
|
|
@@ -1479,10 +1473,10 @@ model:
|
|
|
1479
1473
|
join(dir, "poncho.config.js"),
|
|
1480
1474
|
`export default {
|
|
1481
1475
|
tools: {
|
|
1482
|
-
|
|
1476
|
+
web_search: false,
|
|
1483
1477
|
byEnvironment: {
|
|
1484
1478
|
development: {
|
|
1485
|
-
|
|
1479
|
+
web_search: true,
|
|
1486
1480
|
},
|
|
1487
1481
|
},
|
|
1488
1482
|
}
|
|
@@ -1494,7 +1488,7 @@ model:
|
|
|
1494
1488
|
const harness = new AgentHarness({ workingDir: dir, environment: "development" });
|
|
1495
1489
|
await harness.initialize();
|
|
1496
1490
|
const names = harness.listTools().map((tool) => tool.name);
|
|
1497
|
-
expect(names).toContain("
|
|
1491
|
+
expect(names).toContain("web_search");
|
|
1498
1492
|
});
|
|
1499
1493
|
|
|
1500
1494
|
it("registerTools skips tools disabled via config", async () => {
|
|
@@ -1563,7 +1557,7 @@ model:
|
|
|
1563
1557
|
join(dir, "poncho.config.js"),
|
|
1564
1558
|
`export default {
|
|
1565
1559
|
tools: {
|
|
1566
|
-
|
|
1560
|
+
web_search: 'approval',
|
|
1567
1561
|
}
|
|
1568
1562
|
};
|
|
1569
1563
|
`,
|
|
@@ -1573,9 +1567,9 @@ model:
|
|
|
1573
1567
|
const harness = new AgentHarness({ workingDir: dir });
|
|
1574
1568
|
await harness.initialize();
|
|
1575
1569
|
const names = harness.listTools().map((tool) => tool.name);
|
|
1576
|
-
expect(names).toContain("
|
|
1570
|
+
expect(names).toContain("web_search");
|
|
1577
1571
|
|
|
1578
|
-
const requiresApproval = (harness as any).requiresApprovalForToolCall("
|
|
1572
|
+
const requiresApproval = (harness as any).requiresApprovalForToolCall("web_search", {});
|
|
1579
1573
|
expect(requiresApproval).toBe(true);
|
|
1580
1574
|
});
|
|
1581
1575
|
|
|
@@ -1597,7 +1591,7 @@ model:
|
|
|
1597
1591
|
|
|
1598
1592
|
const harness = new AgentHarness({ workingDir: dir });
|
|
1599
1593
|
await harness.initialize();
|
|
1600
|
-
const requiresApproval = (harness as any).requiresApprovalForToolCall("
|
|
1594
|
+
const requiresApproval = (harness as any).requiresApprovalForToolCall("web_search", {});
|
|
1601
1595
|
expect(requiresApproval).toBe(false);
|
|
1602
1596
|
});
|
|
1603
1597
|
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createIsolateRuntime } from "../src/isolate/runtime.js";
|
|
3
|
+
import { createVfsBindings, createFetchBinding } from "../src/isolate/bindings.js";
|
|
4
|
+
import { buildPolyfillPreamble } from "../src/isolate/polyfills.js";
|
|
5
|
+
import { InMemoryEngine } from "../src/storage/memory-engine.js";
|
|
6
|
+
import { PonchoFsAdapter } from "../src/vfs/poncho-fs-adapter.js";
|
|
7
|
+
import type { IsolateBinding } from "../src/config.js";
|
|
8
|
+
|
|
9
|
+
const MB = 1024 * 1024;
|
|
10
|
+
const LIMITS = { maxFileSize: 10 * MB, maxTotalStorage: 100 * MB };
|
|
11
|
+
const DEFAULT_CONFIG = { memoryLimit: 64, timeout: 5000, outputLimit: 65536 };
|
|
12
|
+
|
|
13
|
+
async function createTestAdapter(tenantId = "t1"): Promise<{
|
|
14
|
+
adapter: PonchoFsAdapter;
|
|
15
|
+
engine: InMemoryEngine;
|
|
16
|
+
}> {
|
|
17
|
+
const engine = new InMemoryEngine("test");
|
|
18
|
+
await engine.initialize();
|
|
19
|
+
const adapter = new PonchoFsAdapter(engine, tenantId, LIMITS);
|
|
20
|
+
return { adapter, engine };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const polyfills = buildPolyfillPreamble(false);
|
|
24
|
+
const polyfillsWithFetch = buildPolyfillPreamble(true);
|
|
25
|
+
|
|
26
|
+
describe("Standard fs API in isolate", () => {
|
|
27
|
+
it("reads and writes text files via fs.readFile/writeFile", async () => {
|
|
28
|
+
const { adapter, engine } = await createTestAdapter();
|
|
29
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
30
|
+
const bindings = createVfsBindings(adapter);
|
|
31
|
+
|
|
32
|
+
const res = await runtime.execute(
|
|
33
|
+
`
|
|
34
|
+
await fs.writeFile("/test.txt", "hello world");
|
|
35
|
+
const content = await fs.readFile("/test.txt", "utf-8");
|
|
36
|
+
return content;
|
|
37
|
+
`,
|
|
38
|
+
bindings, null, undefined, polyfills,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(res.error).toBeUndefined();
|
|
42
|
+
expect(res.result).toBe("hello world");
|
|
43
|
+
|
|
44
|
+
const persisted = await adapter.readFile("/test.txt");
|
|
45
|
+
expect(persisted).toBe("hello world");
|
|
46
|
+
|
|
47
|
+
await engine.close();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("reads binary files as Buffer", async () => {
|
|
51
|
+
const { adapter, engine } = await createTestAdapter();
|
|
52
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
53
|
+
const bindings = createVfsBindings(adapter);
|
|
54
|
+
|
|
55
|
+
// Write binary via adapter
|
|
56
|
+
await adapter.writeFile("/data.bin", Buffer.from([0x41, 0x42, 0x43]));
|
|
57
|
+
|
|
58
|
+
const res = await runtime.execute(
|
|
59
|
+
`
|
|
60
|
+
const buf = await fs.readFile("/data.bin");
|
|
61
|
+
return buf.toString("utf-8");
|
|
62
|
+
`,
|
|
63
|
+
bindings, null, undefined, polyfills,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(res.error).toBeUndefined();
|
|
67
|
+
expect(res.result).toBe("ABC");
|
|
68
|
+
|
|
69
|
+
await engine.close();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("writes Buffer/Uint8Array binary data", async () => {
|
|
73
|
+
const { adapter, engine } = await createTestAdapter();
|
|
74
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
75
|
+
const bindings = createVfsBindings(adapter);
|
|
76
|
+
|
|
77
|
+
const res = await runtime.execute(
|
|
78
|
+
`
|
|
79
|
+
const buf = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
|
|
80
|
+
await fs.writeFile("/hello.bin", buf);
|
|
81
|
+
return true;
|
|
82
|
+
`,
|
|
83
|
+
bindings, null, undefined, polyfills,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(res.error).toBeUndefined();
|
|
87
|
+
const content = await adapter.readFileBuffer("/hello.bin");
|
|
88
|
+
expect(Buffer.from(content).toString("utf-8")).toBe("Hello");
|
|
89
|
+
|
|
90
|
+
await engine.close();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("lists directories via fs.readdir", async () => {
|
|
94
|
+
const { adapter, engine } = await createTestAdapter();
|
|
95
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
96
|
+
const bindings = createVfsBindings(adapter);
|
|
97
|
+
|
|
98
|
+
await adapter.mkdir("/mydir", { recursive: true });
|
|
99
|
+
await adapter.writeFile("/mydir/a.txt", "a");
|
|
100
|
+
await adapter.writeFile("/mydir/b.txt", "b");
|
|
101
|
+
|
|
102
|
+
const res = await runtime.execute(
|
|
103
|
+
`return await fs.readdir("/mydir");`,
|
|
104
|
+
bindings, null, undefined, polyfills,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(res.error).toBeUndefined();
|
|
108
|
+
expect((res.result as string[]).sort()).toEqual(["a.txt", "b.txt"]);
|
|
109
|
+
|
|
110
|
+
await engine.close();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("checks file existence via fs.exists", async () => {
|
|
114
|
+
const { adapter, engine } = await createTestAdapter();
|
|
115
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
116
|
+
const bindings = createVfsBindings(adapter);
|
|
117
|
+
|
|
118
|
+
await adapter.writeFile("/exists.txt", "yes");
|
|
119
|
+
|
|
120
|
+
const res = await runtime.execute(
|
|
121
|
+
`
|
|
122
|
+
const a = await fs.exists("/exists.txt");
|
|
123
|
+
const b = await fs.exists("/nope.txt");
|
|
124
|
+
return { a, b };
|
|
125
|
+
`,
|
|
126
|
+
bindings, null, undefined, polyfills,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(res.error).toBeUndefined();
|
|
130
|
+
expect(res.result).toEqual({ a: true, b: false });
|
|
131
|
+
|
|
132
|
+
await engine.close();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("gets file stats via fs.stat", async () => {
|
|
136
|
+
const { adapter, engine } = await createTestAdapter();
|
|
137
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
138
|
+
const bindings = createVfsBindings(adapter);
|
|
139
|
+
|
|
140
|
+
await adapter.writeFile("/info.txt", "12345");
|
|
141
|
+
|
|
142
|
+
const res = await runtime.execute(
|
|
143
|
+
`
|
|
144
|
+
const s = await fs.stat("/info.txt");
|
|
145
|
+
return { isFile: s.isFile(), isDir: s.isDirectory(), size: s.size };
|
|
146
|
+
`,
|
|
147
|
+
bindings, null, undefined, polyfills,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(res.error).toBeUndefined();
|
|
151
|
+
expect(res.result).toEqual({ isFile: true, isDir: false, size: 5 });
|
|
152
|
+
|
|
153
|
+
await engine.close();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("deletes files via fs.unlink", async () => {
|
|
157
|
+
const { adapter, engine } = await createTestAdapter();
|
|
158
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
159
|
+
const bindings = createVfsBindings(adapter);
|
|
160
|
+
|
|
161
|
+
await adapter.writeFile("/doomed.txt", "bye");
|
|
162
|
+
|
|
163
|
+
const res = await runtime.execute(
|
|
164
|
+
`
|
|
165
|
+
await fs.unlink("/doomed.txt");
|
|
166
|
+
return await fs.exists("/doomed.txt");
|
|
167
|
+
`,
|
|
168
|
+
bindings, null, undefined, polyfills,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(res.error).toBeUndefined();
|
|
172
|
+
expect(res.result).toBe(false);
|
|
173
|
+
|
|
174
|
+
await engine.close();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("creates directories via fs.mkdir", async () => {
|
|
178
|
+
const { adapter, engine } = await createTestAdapter();
|
|
179
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
180
|
+
const bindings = createVfsBindings(adapter);
|
|
181
|
+
|
|
182
|
+
const res = await runtime.execute(
|
|
183
|
+
`
|
|
184
|
+
await fs.mkdir("/deep/nested/dir");
|
|
185
|
+
await fs.writeFile("/deep/nested/dir/file.txt", "hi");
|
|
186
|
+
return await fs.readFile("/deep/nested/dir/file.txt", "utf-8");
|
|
187
|
+
`,
|
|
188
|
+
bindings, null, undefined, polyfills,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(res.error).toBeUndefined();
|
|
192
|
+
expect(res.result).toBe("hi");
|
|
193
|
+
|
|
194
|
+
await engine.close();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("Buffer polyfill in isolate", () => {
|
|
199
|
+
it("supports from/toString with encodings", async () => {
|
|
200
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
201
|
+
|
|
202
|
+
const res = await runtime.execute(
|
|
203
|
+
`
|
|
204
|
+
const b = Buffer.from("hello");
|
|
205
|
+
const hex = b.toString("hex");
|
|
206
|
+
const b64 = b.toString("base64");
|
|
207
|
+
const back = Buffer.from(b64, "base64").toString("utf-8");
|
|
208
|
+
return { hex, b64, back };
|
|
209
|
+
`,
|
|
210
|
+
{}, null, undefined, polyfills,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(res.error).toBeUndefined();
|
|
214
|
+
expect(res.result).toEqual({
|
|
215
|
+
hex: "68656c6c6f",
|
|
216
|
+
b64: "aGVsbG8=",
|
|
217
|
+
back: "hello",
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("supports concat and alloc", async () => {
|
|
222
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
223
|
+
|
|
224
|
+
const res = await runtime.execute(
|
|
225
|
+
`
|
|
226
|
+
const a = Buffer.from("hel");
|
|
227
|
+
const b = Buffer.from("lo");
|
|
228
|
+
const c = Buffer.concat([a, b]);
|
|
229
|
+
return c.toString("utf-8");
|
|
230
|
+
`,
|
|
231
|
+
{}, null, undefined, polyfills,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(res.error).toBeUndefined();
|
|
235
|
+
expect(res.result).toBe("hello");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("path polyfill in isolate", () => {
|
|
240
|
+
it("join, basename, dirname, extname work", async () => {
|
|
241
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
242
|
+
|
|
243
|
+
const res = await runtime.execute(
|
|
244
|
+
`
|
|
245
|
+
return {
|
|
246
|
+
joined: path.join("/foo", "bar", "baz.txt"),
|
|
247
|
+
base: path.basename("/foo/bar/baz.txt"),
|
|
248
|
+
baseNoExt: path.basename("/foo/bar/baz.txt", ".txt"),
|
|
249
|
+
dir: path.dirname("/foo/bar/baz.txt"),
|
|
250
|
+
ext: path.extname("/foo/bar/baz.txt"),
|
|
251
|
+
};
|
|
252
|
+
`,
|
|
253
|
+
{}, null, undefined, polyfills,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(res.error).toBeUndefined();
|
|
257
|
+
expect(res.result).toEqual({
|
|
258
|
+
joined: "/foo/bar/baz.txt",
|
|
259
|
+
base: "baz.txt",
|
|
260
|
+
baseNoExt: "baz",
|
|
261
|
+
dir: "/foo/bar",
|
|
262
|
+
ext: ".txt",
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("Polyfill basics in isolate", () => {
|
|
268
|
+
it("atob/btoa work", async () => {
|
|
269
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
270
|
+
|
|
271
|
+
const res = await runtime.execute(
|
|
272
|
+
`return { encoded: btoa("hello"), decoded: atob("aGVsbG8=") };`,
|
|
273
|
+
{}, null, undefined, polyfills,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(res.error).toBeUndefined();
|
|
277
|
+
expect(res.result).toEqual({ encoded: "aGVsbG8=", decoded: "hello" });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("setTimeout with 0 delay works", async () => {
|
|
281
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
282
|
+
|
|
283
|
+
const res = await runtime.execute(
|
|
284
|
+
`
|
|
285
|
+
let called = false;
|
|
286
|
+
setTimeout(() => { called = true; }, 0);
|
|
287
|
+
await new Promise(r => setTimeout(r, 0));
|
|
288
|
+
// Give microtasks a chance to flush
|
|
289
|
+
await new Promise(r => setTimeout(r, 0));
|
|
290
|
+
return called;
|
|
291
|
+
`,
|
|
292
|
+
{}, null, undefined, polyfills,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(res.error).toBeUndefined();
|
|
296
|
+
expect(res.result).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("crypto.randomUUID returns valid format", async () => {
|
|
300
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
301
|
+
|
|
302
|
+
const res = await runtime.execute(
|
|
303
|
+
`return crypto.randomUUID();`,
|
|
304
|
+
{}, null, undefined, polyfills,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
expect(res.error).toBeUndefined();
|
|
308
|
+
expect(res.result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("Blob works", async () => {
|
|
312
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
313
|
+
|
|
314
|
+
const res = await runtime.execute(
|
|
315
|
+
`
|
|
316
|
+
const blob = new Blob(["hello ", "world"], { type: "text/plain" });
|
|
317
|
+
const text = await blob.text();
|
|
318
|
+
return { size: blob.size, type: blob.type, text };
|
|
319
|
+
`,
|
|
320
|
+
{}, null, undefined, polyfills,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(res.error).toBeUndefined();
|
|
324
|
+
expect(res.result).toEqual({ size: 11, type: "text/plain", text: "hello world" });
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("structuredClone works", async () => {
|
|
328
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
329
|
+
|
|
330
|
+
const res = await runtime.execute(
|
|
331
|
+
`
|
|
332
|
+
const obj = { a: 1, b: [2, 3] };
|
|
333
|
+
const clone = structuredClone(obj);
|
|
334
|
+
clone.b.push(4);
|
|
335
|
+
return { original: obj.b.length, cloned: clone.b.length };
|
|
336
|
+
`,
|
|
337
|
+
{}, null, undefined, polyfills,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(res.error).toBeUndefined();
|
|
341
|
+
expect(res.result).toEqual({ original: 2, cloned: 3 });
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("console.table works", async () => {
|
|
345
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
346
|
+
|
|
347
|
+
const res = await runtime.execute(
|
|
348
|
+
`console.table([{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }]);`,
|
|
349
|
+
{}, null, undefined, polyfills,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
expect(res.error).toBeUndefined();
|
|
353
|
+
expect(res.stdout).toContain("Alice");
|
|
354
|
+
expect(res.stdout).toContain("Bob");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("fetch() gives helpful error when network not configured", async () => {
|
|
358
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
359
|
+
|
|
360
|
+
const res = await runtime.execute(
|
|
361
|
+
`
|
|
362
|
+
try {
|
|
363
|
+
await fetch("https://example.com");
|
|
364
|
+
return "should not reach here";
|
|
365
|
+
} catch (e) {
|
|
366
|
+
return e.message;
|
|
367
|
+
}
|
|
368
|
+
`,
|
|
369
|
+
{}, null, undefined, polyfills,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expect(res.error).toBeUndefined();
|
|
373
|
+
expect(res.result).toMatch(/not available|network/i);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("Standard fetch API in isolate", () => {
|
|
378
|
+
it("fetches text content with standard API", async () => {
|
|
379
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
380
|
+
const fetchBinding = createFetchBinding(["example.com"], { dangerouslyAllowAll: true });
|
|
381
|
+
const bindings: Record<string, IsolateBinding> = {
|
|
382
|
+
__poncho_fetch: fetchBinding,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const res = await runtime.execute(
|
|
386
|
+
`
|
|
387
|
+
const resp = await fetch("https://example.com");
|
|
388
|
+
const text = await resp.text();
|
|
389
|
+
return { ok: resp.ok, status: resp.status, hasContent: text.length > 0 };
|
|
390
|
+
`,
|
|
391
|
+
bindings, null, undefined, polyfillsWithFetch,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(res.error).toBeUndefined();
|
|
395
|
+
const result = res.result as { ok: boolean; status: number; hasContent: boolean };
|
|
396
|
+
expect(result.ok).toBe(true);
|
|
397
|
+
expect(result.status).toBe(200);
|
|
398
|
+
expect(result.hasContent).toBe(true);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("rejects requests to non-allowed domains", async () => {
|
|
402
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
403
|
+
const fetchBinding = createFetchBinding(["api.example.com"]);
|
|
404
|
+
const bindings: Record<string, IsolateBinding> = {
|
|
405
|
+
__poncho_fetch: fetchBinding,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const res = await runtime.execute(
|
|
409
|
+
`
|
|
410
|
+
try {
|
|
411
|
+
await fetch("https://evil.com/data");
|
|
412
|
+
return "should not reach here";
|
|
413
|
+
} catch (e) {
|
|
414
|
+
return e.message;
|
|
415
|
+
}
|
|
416
|
+
`,
|
|
417
|
+
bindings, null, undefined, polyfillsWithFetch,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
expect(res.error).toBeUndefined();
|
|
421
|
+
expect(res.result).toMatch(/blocked.*evil\.com/i);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe("VFS tenant isolation", () => {
|
|
426
|
+
it("isolates file systems between tenants", async () => {
|
|
427
|
+
const engine = new InMemoryEngine("test");
|
|
428
|
+
await engine.initialize();
|
|
429
|
+
const runtime = createIsolateRuntime(DEFAULT_CONFIG);
|
|
430
|
+
|
|
431
|
+
const adapter1 = new PonchoFsAdapter(engine, "tenant-a", LIMITS);
|
|
432
|
+
const adapter2 = new PonchoFsAdapter(engine, "tenant-b", LIMITS);
|
|
433
|
+
|
|
434
|
+
const bindings1 = createVfsBindings(adapter1);
|
|
435
|
+
const bindings2 = createVfsBindings(adapter2);
|
|
436
|
+
|
|
437
|
+
// Write in tenant A
|
|
438
|
+
await runtime.execute(
|
|
439
|
+
`await fs.writeFile("/secret.txt", "tenant-a-data");`,
|
|
440
|
+
bindings1, null, undefined, polyfills,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// Tenant B should not see it
|
|
444
|
+
const res = await runtime.execute(
|
|
445
|
+
`return await fs.exists("/secret.txt");`,
|
|
446
|
+
bindings2, null, undefined, polyfills,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
expect(res.result).toBe(false);
|
|
450
|
+
|
|
451
|
+
await engine.close();
|
|
452
|
+
});
|
|
453
|
+
});
|