@downcity/agent 1.1.258 → 1.1.260

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.
@@ -1,309 +1,172 @@
1
1
  /**
2
- * @file 验证 CityModel 会优先走 OpenAI-compatible LanguageModel 并完成 tool loop。
2
+ * @file 验证 Agent 直接调用实现 LanguageModelV3 CityModel 并完成 tool loop。
3
3
  *
4
4
  * 关键点(中文)
5
- * - 这里直接走编译后的 Agent 产物,避免测试只覆盖源码级辅助函数。
6
- * - CityModel 使用 @downcity/type 的共享协议构造,避免反向依赖 City SDK 实现。
7
- * - 重点锁住 CityModel -> LanguageModel -> tool-call -> 本地执行 -> tool-result 回传链路。
8
- * - 新路径不应再调用 `/v1/ai/stream`,避免 UIMessage stream 反向适配丢失 finish 语义。
5
+ * - 测试模型自身已经实现 LanguageModelV3,Agent 不再创建第二个 Provider 模型。
6
+ * - 第一次调用返回 tool-call,Agent 本地执行后把 tool-result 放进第二次调用。
7
+ * - CityModel 的目录信息继续用于上下文窗口和日志,不参与网络连接转换。
9
8
  */
10
9
 
11
10
  import test from "node:test";
12
11
  import assert from "node:assert/strict";
13
- import http from "node:http";
14
12
  import os from "node:os";
15
13
  import path from "node:path";
16
14
  import fs from "node:fs/promises";
17
- import { Agent } from "../bin/index.js";
18
- import { createAction, createPlugin } from "../bin/plugin/core/PluginActionFactory.js";
19
- import { CITY_MODEL_INVOKER, CITY_MODEL_KIND } from "@downcity/type";
15
+ import { MockLanguageModelV3 } from "ai/test";
20
16
  import { tool } from "ai";
21
17
  import { z } from "zod";
22
18
 
23
- function write_openai_sse(res, chunks) {
24
- res.writeHead(200, {
25
- "content-type": "text/event-stream; charset=utf-8",
26
- "cache-control": "no-cache",
27
- connection: "keep-alive",
28
- });
29
- for (const chunk of chunks) {
30
- const payload = typeof chunk === "string" ? chunk : JSON.stringify(chunk);
31
- res.write(`data: ${payload}\n\n`);
32
- }
33
- res.end();
19
+ import { Agent } from "../bin/index.js";
20
+ import { createAction, createPlugin } from "../bin/plugin/core/PluginActionFactory.js";
21
+ import { CITY_MODEL_KIND } from "@downcity/type";
22
+
23
+ /** 构造 AI SDK V3 usage。 */
24
+ function create_usage() {
25
+ return {
26
+ inputTokens: { total: 5, noCache: 5, cacheRead: 0, cacheWrite: 0 },
27
+ outputTokens: { total: 3, text: 3, reasoning: 0 },
28
+ };
34
29
  }
35
30
 
36
- async function read_json_body(req) {
37
- const raw = await new Promise((resolve, reject) => {
38
- let data = "";
39
- req.on("data", (chunk) => {
40
- data += chunk;
41
- });
42
- req.on("end", () => resolve(data));
43
- req.on("error", reject);
44
- });
45
- return JSON.parse(String(raw || "{}"));
31
+ /** 构造一次 ping tool-call。 */
32
+ function create_tool_call_stream() {
33
+ const input = JSON.stringify({ value: "hello" });
34
+ return {
35
+ stream: new ReadableStream({
36
+ start(controller) {
37
+ controller.enqueue({ type: "stream-start", warnings: [] });
38
+ controller.enqueue({ type: "tool-input-start", id: "call_1", toolName: "ping" });
39
+ controller.enqueue({ type: "tool-input-delta", id: "call_1", delta: input });
40
+ controller.enqueue({ type: "tool-input-end", id: "call_1" });
41
+ controller.enqueue({
42
+ type: "tool-call",
43
+ toolCallId: "call_1",
44
+ toolName: "ping",
45
+ input,
46
+ });
47
+ controller.enqueue({
48
+ type: "finish",
49
+ finishReason: { unified: "tool-calls", raw: "tool_calls" },
50
+ usage: create_usage(),
51
+ });
52
+ controller.close();
53
+ },
54
+ }),
55
+ };
46
56
  }
47
57
 
48
- test("CityModel uses direct LanguageModel path and sends tool result back", async () => {
49
- const agent_requests = [];
50
- let stream_requests = 0;
51
- let tool_executed = false;
52
-
53
- const server = http.createServer(async (req, res) => {
54
- const url = new URL(String(req.url || "/"), "http://127.0.0.1");
55
-
56
- if (req.method === "POST" && url.pathname === "/v1/ai/stream") {
57
- stream_requests += 1;
58
- res.writeHead(500, { "content-type": "application/json" });
59
- res.end(JSON.stringify({ error: "legacy stream endpoint should not be called" }));
60
- return;
61
- }
62
-
63
- if (req.method === "POST" && url.pathname === "/v1/ai/chat/completions") {
64
- const body = await read_json_body(req);
65
-
66
- if (!Array.isArray(body.tools)) {
67
- write_openai_sse(res, [
68
- {
69
- id: "chatcmpl_title",
70
- object: "chat.completion.chunk",
71
- created: 1,
72
- model: "mock-model",
73
- choices: [
74
- {
75
- index: 0,
76
- delta: { role: "assistant", content: "Tool loop" },
77
- finish_reason: null,
78
- },
79
- ],
80
- },
81
- {
82
- id: "chatcmpl_title",
83
- object: "chat.completion.chunk",
84
- created: 1,
85
- model: "mock-model",
86
- choices: [
87
- {
88
- index: 0,
89
- delta: {},
90
- finish_reason: "stop",
91
- },
92
- ],
93
- },
94
- "[DONE]",
95
- ]);
96
- return;
97
- }
58
+ /** 构造普通文本完成流。 */
59
+ function create_text_stream(text) {
60
+ return {
61
+ stream: new ReadableStream({
62
+ start(controller) {
63
+ controller.enqueue({ type: "stream-start", warnings: [] });
64
+ controller.enqueue({ type: "text-start", id: "text_1" });
65
+ controller.enqueue({ type: "text-delta", id: "text_1", delta: text });
66
+ controller.enqueue({ type: "text-end", id: "text_1" });
67
+ controller.enqueue({
68
+ type: "finish",
69
+ finishReason: { unified: "stop", raw: "stop" },
70
+ usage: create_usage(),
71
+ });
72
+ controller.close();
73
+ },
74
+ }),
75
+ };
76
+ }
98
77
 
99
- agent_requests.push(body);
100
- if (agent_requests.length === 1) {
101
- write_openai_sse(res, [
102
- {
103
- id: "chatcmpl_1",
104
- object: "chat.completion.chunk",
105
- created: 1,
106
- model: "mock-model",
107
- choices: [
108
- {
109
- index: 0,
110
- delta: { role: "assistant" },
111
- finish_reason: null,
112
- },
113
- ],
114
- },
115
- {
116
- id: "chatcmpl_1",
117
- object: "chat.completion.chunk",
118
- created: 1,
119
- model: "mock-model",
120
- choices: [
121
- {
122
- index: 0,
123
- delta: {
124
- tool_calls: [
125
- {
126
- index: 0,
127
- id: "call_1",
128
- type: "function",
129
- function: {
130
- name: "ping",
131
- arguments: "{\"value\":\"hello\"}",
132
- },
133
- },
134
- ],
135
- },
136
- finish_reason: null,
137
- },
138
- ],
139
- },
140
- {
141
- id: "chatcmpl_1",
142
- object: "chat.completion.chunk",
143
- created: 1,
144
- model: "mock-model",
145
- choices: [
146
- {
147
- index: 0,
148
- delta: {},
149
- finish_reason: "tool_calls",
150
- },
151
- ],
152
- },
153
- "[DONE]",
154
- ]);
155
- return;
78
+ /** 给标准 LanguageModelV3 附加 CityModel 目录协议。 */
79
+ function create_city_model(model_requests) {
80
+ let request_count = 0;
81
+ const model = new MockLanguageModelV3({
82
+ modelId: "mock-model",
83
+ doStream: async (options) => {
84
+ if (!Array.isArray(options.tools) || options.tools.length === 0) {
85
+ return create_text_stream("Tool loop");
156
86
  }
87
+ request_count += 1;
88
+ model_requests.push(options);
89
+ return request_count === 1
90
+ ? create_tool_call_stream()
91
+ : create_text_stream("done");
92
+ },
93
+ });
94
+ return Object.assign(model, {
95
+ id: "mock-model",
96
+ name: "Mock Model",
97
+ description: "Native CityModel tool loop test",
98
+ modalities: ["text", "stream"],
99
+ tags: [],
100
+ meta: {},
101
+ kind: CITY_MODEL_KIND,
102
+ });
103
+ }
157
104
 
158
- write_openai_sse(res, [
159
- {
160
- id: "chatcmpl_2",
161
- object: "chat.completion.chunk",
162
- created: 1,
163
- model: "mock-model",
164
- choices: [
165
- {
166
- index: 0,
167
- delta: { role: "assistant" },
168
- finish_reason: null,
169
- },
170
- ],
171
- },
172
- {
173
- id: "chatcmpl_2",
174
- object: "chat.completion.chunk",
175
- created: 1,
176
- model: "mock-model",
177
- choices: [
178
- {
179
- index: 0,
180
- delta: { content: "done" },
181
- finish_reason: null,
182
- },
183
- ],
184
- },
185
- {
186
- id: "chatcmpl_2",
187
- object: "chat.completion.chunk",
188
- created: 1,
189
- model: "mock-model",
190
- choices: [
191
- {
192
- index: 0,
193
- delta: {},
194
- finish_reason: "stop",
195
- },
196
- ],
105
+ test("CityModel uses direct LanguageModel path and sends tool result back", async () => {
106
+ const model_requests = [];
107
+ let tool_executed = false;
108
+ const agent_path = await fs.mkdtemp(
109
+ path.join(os.tmpdir(), "downcity-agent-city-model-tool-loop-"),
110
+ );
111
+ const skill_plugin = createPlugin({
112
+ name: "skill",
113
+ title: "Skill",
114
+ description: "Test skill plugin",
115
+ actions: {
116
+ lookup: createAction({
117
+ description: "Lookup a skill",
118
+ execute: async ({ input }) => ({
119
+ success: true,
120
+ data: { name: input.name },
121
+ message: "loaded",
122
+ }),
123
+ }),
124
+ },
125
+ });
126
+ const agent = new Agent({
127
+ id: "tool_loop_agent",
128
+ path: agent_path,
129
+ plugins: [skill_plugin],
130
+ tools: {
131
+ ping: tool({
132
+ description: "ping tool",
133
+ inputSchema: z.object({ value: z.string() }),
134
+ execute: async ({ value }, options) => {
135
+ tool_executed = true;
136
+ options.experimental_context.session_run_context.pendingAssistantFileParts.push({
137
+ type: "file",
138
+ mediaType: "image/png",
139
+ url: ".downcity/resources/tool-output.png",
140
+ filename: "tool-output.png",
141
+ });
142
+ return { echoed: value };
197
143
  },
198
- "[DONE]",
199
- ]);
200
- return;
201
- }
202
-
203
- res.writeHead(404);
204
- res.end("not found");
144
+ }),
145
+ },
205
146
  });
206
147
 
207
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
208
-
209
- let agent;
210
148
  try {
211
- const address = server.address();
212
- assert.ok(address && typeof address === "object");
213
- const model = Object.freeze({
214
- id: "mock-model",
215
- name: "Mock Model",
216
- description: "mock",
217
- modalities: ["text", "stream"],
218
- tags: [],
219
- meta: {},
220
- kind: CITY_MODEL_KIND,
221
- [CITY_MODEL_INVOKER]: {
222
- connection: () => ({
223
- base_url: `http://127.0.0.1:${String(address.port)}/v1/ai`,
224
- api_key: "ub_test",
225
- model_id: "mock-model",
226
- }),
227
- },
228
- });
229
-
230
- const agent_path = await fs.mkdtemp(
231
- path.join(os.tmpdir(), "downcity-agent-city-model-tool-loop-"),
232
- );
233
- const skill_plugin = createPlugin({
234
- name: "skill",
235
- title: "Skill",
236
- description: "Test skill plugin",
237
- actions: {
238
- lookup: createAction({
239
- description: "Lookup a skill",
240
- execute: async ({ input }) => ({
241
- success: true,
242
- data: { name: input.name },
243
- message: "loaded",
244
- }),
245
- }),
246
- },
247
- });
248
- agent = new Agent({
249
- id: "tool_loop_agent",
250
- path: agent_path,
251
- plugins: [skill_plugin],
252
- tools: {
253
- ping: tool({
254
- description: "ping tool",
255
- inputSchema: z.object({
256
- value: z.string(),
257
- }),
258
- execute: async ({ value }, options) => {
259
- tool_executed = true;
260
- options.experimental_context.session_run_context.pendingAssistantFileParts.push({
261
- type: "file",
262
- mediaType: "image/png",
263
- url: ".downcity/resources/tool-output.png",
264
- filename: "tool-output.png",
265
- });
266
- return { echoed: value };
267
- },
268
- }),
269
- },
270
- });
271
-
272
149
  const session = await agent.sessions.create();
273
- await session.set({ model });
150
+ await session.set({ model: create_city_model(model_requests) });
274
151
  const turn = await session.prompt({ query: "please use the ping tool" });
275
152
  const result = await turn.finished;
276
153
 
277
154
  assert.equal(result.success, true);
278
155
  assert.equal(tool_executed, true);
279
- assert.equal(stream_requests, 0);
280
- assert.equal(agent_requests.length, 2);
281
- assert.equal(agent_requests[0]?.model, "mock-model");
282
- const plugin_call_tool = agent_requests[0]?.tools?.find(
283
- (item) => item?.type === "function" && item?.function?.name === "plugin_call",
284
- );
156
+ assert.equal(model_requests.length, 2);
157
+ const plugin_call_tool = model_requests[0].tools.find((item) => item.name === "plugin_call");
285
158
  assert.ok(plugin_call_tool);
286
- const plugin_call_parameters = plugin_call_tool.function.parameters;
287
- assert.equal(plugin_call_parameters.type, "object");
288
- assert.deepEqual(plugin_call_parameters.required, ["plugin", "action"]);
289
- assert.equal(plugin_call_parameters.additionalProperties, false);
290
- assert.equal(
291
- plugin_call_parameters.properties.payload.additionalProperties,
292
- true,
293
- );
159
+ assert.equal(plugin_call_tool.inputSchema.type, "object");
160
+ assert.deepEqual(plugin_call_tool.inputSchema.required, ["plugin", "action"]);
161
+ assert.equal(plugin_call_tool.inputSchema.additionalProperties, false);
294
162
 
295
- const second_request_messages = Array.isArray(agent_requests[1]?.messages)
296
- ? agent_requests[1].messages
297
- : [];
298
- const serialized_second_messages = JSON.stringify(second_request_messages);
299
- assert.match(serialized_second_messages, /"role":"tool"/);
300
- assert.match(serialized_second_messages, /call_1/);
301
- assert.match(serialized_second_messages, /echoed/);
302
- assert.match(serialized_second_messages, /hello/);
163
+ const serialized_second_prompt = JSON.stringify(model_requests[1].prompt);
164
+ assert.match(serialized_second_prompt, /"role":"tool"/);
165
+ assert.match(serialized_second_prompt, /call_1/);
166
+ assert.match(serialized_second_prompt, /echoed/);
167
+ assert.match(serialized_second_prompt, /hello/);
303
168
 
304
- const result_file = result.assistantMessage.parts.find(
305
- (part) => part.type === "file",
306
- );
169
+ const result_file = result.assistantMessage.parts.find((part) => part.type === "file");
307
170
  assert.deepEqual(result_file, {
308
171
  type: "file",
309
172
  mediaType: "image/png",
@@ -311,32 +174,12 @@ test("CityModel uses direct LanguageModel path and sends tool result back", asyn
311
174
  filename: "tool-output.png",
312
175
  });
313
176
  const session_messages = await session.messages({ include_internal: true });
314
- const persisted_assistant = session_messages.items.find(
315
- (message) => message.type === "assistant",
316
- );
317
- const persisted_file = persisted_assistant.parts.find(
318
- (part) => part.type === "file",
319
- );
320
- assert.deepEqual(persisted_file, {
321
- part_id: persisted_file.part_id,
322
- sequence: persisted_file.sequence,
323
- type: "file",
324
- media_type: "image/png",
325
- url: ".downcity/resources/tool-output.png",
326
- filename: "tool-output.png",
327
- });
177
+ const persisted_assistant = session_messages.items.find((message) => message.type === "assistant");
178
+ const persisted_file = persisted_assistant.parts.find((part) => part.type === "file");
179
+ assert.equal(persisted_file.media_type, "image/png");
180
+ assert.equal(persisted_file.url, ".downcity/resources/tool-output.png");
181
+ assert.equal(persisted_file.filename, "tool-output.png");
328
182
  } finally {
329
- if (agent) {
330
- await agent.dispose();
331
- }
332
- await new Promise((resolve, reject) => {
333
- server.close((error) => {
334
- if (error) {
335
- reject(error);
336
- return;
337
- }
338
- resolve();
339
- });
340
- });
183
+ await agent.dispose();
341
184
  }
342
185
  });
@@ -1,9 +1,5 @@
1
1
  /**
2
- * @file 验证 Linux bubblewrap sandbox 参数生成。
3
- *
4
- * 关键点(中文)
5
- * - 测试编译后的 bin 输出,避免测试文件进入 package 源码导出面。
6
- * - 不启动真实 `bwrap`,只锁住路径挂载、网络开关与 shell 调用参数。
2
+ * @file 验证 Linux Bubblewrap 参数对统一 Sandbox 策略的映射。
7
3
  */
8
4
 
9
5
  import test from "node:test";
@@ -12,140 +8,61 @@ import fs from "node:fs/promises";
12
8
  import os from "node:os";
13
9
  import path from "node:path";
14
10
 
15
- import { buildLinuxBubblewrapArgs } from "@downcity/shell/sandbox/LinuxBubblewrapSandbox.js";
11
+ import { build_linux_bubblewrap_args } from "@downcity/shell/sandbox/LinuxBubblewrap.js";
16
12
 
17
- async function createSandboxFixture() {
18
- const root = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-bwrap-"));
19
- const projectRoot = path.join(root, "project");
20
- const writablePath = path.join(projectRoot, ".downcity");
21
- const shellDir = path.join(writablePath, "shell", "sh_test");
22
- const sandboxDir = path.join(projectRoot, ".downcity", "sandbox");
23
- const tmpDir = path.join(sandboxDir, "tmp");
24
- const cacheDir = path.join(sandboxDir, ".cache");
25
-
26
- await fs.mkdir(shellDir, { recursive: true });
27
- await fs.mkdir(tmpDir, { recursive: true });
28
- await fs.mkdir(cacheDir, { recursive: true });
29
-
30
- return {
31
- root,
32
- projectRoot,
33
- writablePath,
34
- shellDir,
35
- sandboxDir,
36
- tmpDir,
37
- cacheDir,
38
- };
39
- }
40
-
41
- function createParams(fixture, overrides = {}) {
42
- return {
43
- executionId: "sh_test",
44
- executionDir: fixture.shellDir,
45
- cmd: "printf hello",
46
- cwd: fixture.projectRoot,
47
- actualCwd: fixture.projectRoot,
48
- shellPath: "/bin/sh",
49
- login: true,
50
- baseEnv: {
51
- PATH: "/usr/bin:/bin",
52
- LANG: "C.UTF-8",
53
- DC_SESSION_ID: "session_test",
54
- },
55
- config: {
56
- backend: "linux-bubblewrap",
57
- rootPath: fixture.projectRoot,
58
- sandboxDir: fixture.sandboxDir,
59
- homeDir: fixture.sandboxDir,
60
- tmpDir: fixture.tmpDir,
61
- cacheDir: fixture.cacheDir,
62
- envAllowlist: ["PATH", "LANG"],
63
- writablePaths: [fixture.projectRoot, fixture.sandboxDir],
64
- networkMode: "off",
65
- },
66
- ...overrides,
67
- };
68
- }
69
-
70
- function hasArg(args, value) {
71
- return args.includes(value);
72
- }
73
-
74
- function hasOptionPair(args, option, sourcePath, targetPath = sourcePath) {
13
+ function has_option_pair(args, option, source_path, target_path = source_path) {
75
14
  for (let index = 0; index < args.length - 2; index += 1) {
76
15
  if (
77
16
  args[index] === option &&
78
- args[index + 1] === sourcePath &&
79
- args[index + 2] === targetPath
80
- ) {
81
- return true;
82
- }
83
- }
84
- return false;
85
- }
86
-
87
- function hasOptionValue(args, option, value) {
88
- for (let index = 0; index < args.length - 1; index += 1) {
89
- if (args[index] === option && args[index + 1] === value) {
90
- return true;
91
- }
17
+ args[index + 1] === source_path &&
18
+ args[index + 2] === target_path
19
+ ) return true;
92
20
  }
93
21
  return false;
94
22
  }
95
23
 
96
- test("Linux bubblewrap args isolate network and overlay writable project paths", async () => {
97
- const fixture = await createSandboxFixture();
98
- try {
99
- const args = buildLinuxBubblewrapArgs(createParams(fixture));
100
-
101
- assert.equal(hasArg(args, "--die-with-parent"), true);
102
- assert.equal(hasArg(args, "--unshare-pid"), true);
103
- assert.equal(hasArg(args, "--unshare-net"), true);
104
- assert.equal(hasOptionPair(args, "--bind", fixture.projectRoot), true);
105
- assert.equal(hasOptionPair(args, "--bind", fixture.sandboxDir), false);
106
- assert.equal(hasOptionPair(args, "--ro-bind", fixture.projectRoot), false);
107
- assert.equal(hasOptionValue(args, "--dir", fixture.writablePath), false);
108
- assert.deepEqual(args.slice(-5), [
109
- "--chdir",
110
- fixture.projectRoot,
111
- "/bin/sh",
112
- "-lc",
113
- "printf hello",
114
- ]);
115
- } finally {
116
- await fs.rm(fixture.root, { recursive: true, force: true });
117
- }
118
- });
119
-
120
- test("Linux bubblewrap args keep root writable when sandbox writablePaths includes root", async () => {
121
- const fixture = await createSandboxFixture();
24
+ test("Linux maps read paths to ro-bind and workspace to bind", async () => {
25
+ const fixture_root = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-bwrap-"));
122
26
  try {
123
- const args = buildLinuxBubblewrapArgs(createParams(fixture, {
124
- config: {
27
+ const project_root = path.join(fixture_root, "project");
28
+ const tool_root = path.join(fixture_root, "tool");
29
+ await fs.mkdir(project_root, { recursive: true });
30
+ await fs.mkdir(tool_root, { recursive: true });
31
+ const request = {
32
+ execution_id: "sh_test",
33
+ execution_dir: path.join(project_root, ".downcity", "shell", "sh_test"),
34
+ cmd: "printf hello",
35
+ cwd: project_root,
36
+ shell_path: "/bin/sh",
37
+ login: true,
38
+ base_env: { PATH: "/usr/bin:/bin" },
39
+ policy: {
125
40
  backend: "linux-bubblewrap",
126
- rootPath: fixture.projectRoot,
127
- sandboxDir: fixture.sandboxDir,
128
- homeDir: fixture.sandboxDir,
129
- tmpDir: fixture.tmpDir,
130
- cacheDir: fixture.cacheDir,
131
- envAllowlist: ["PATH"],
132
- writablePaths: [fixture.projectRoot],
133
- networkMode: "full",
41
+ root_path: project_root,
42
+ sandbox_dir: path.join(project_root, ".downcity", "sandbox"),
43
+ home_dir: path.join(project_root, ".downcity", "sandbox"),
44
+ tmp_dir: path.join(project_root, ".downcity", "sandbox", "tmp"),
45
+ cache_dir: path.join(project_root, ".downcity", "sandbox", ".cache"),
46
+ env_allowlist: ["PATH"],
47
+ read_only_paths: [tool_root],
48
+ read_write_paths: [project_root],
49
+ network_mode: "off",
50
+ fingerprint: "policy_test",
134
51
  },
135
- login: false,
136
- }));
137
-
138
- assert.equal(hasArg(args, "--unshare-net"), false);
139
- assert.equal(hasOptionPair(args, "--bind", fixture.projectRoot), true);
140
- assert.equal(hasOptionPair(args, "--ro-bind", fixture.projectRoot), false);
52
+ };
53
+ const args = build_linux_bubblewrap_args(request);
54
+ assert.equal(args.includes("--unshare-net"), true);
55
+ assert.equal(has_option_pair(args, "--ro-bind", tool_root), true);
56
+ assert.equal(has_option_pair(args, "--bind", tool_root), false);
57
+ assert.equal(has_option_pair(args, "--bind", project_root), true);
141
58
  assert.deepEqual(args.slice(-5), [
142
59
  "--chdir",
143
- fixture.projectRoot,
60
+ project_root,
144
61
  "/bin/sh",
145
- "-c",
62
+ "-lc",
146
63
  "printf hello",
147
64
  ]);
148
65
  } finally {
149
- await fs.rm(fixture.root, { recursive: true, force: true });
66
+ await fs.rm(fixture_root, { recursive: true, force: true });
150
67
  }
151
68
  });