@lobu/worker 6.0.1 → 7.0.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.
Files changed (104) hide show
  1. package/dist/embedded/exec-sandbox.d.ts +2 -2
  2. package/dist/embedded/exec-sandbox.js +7 -7
  3. package/dist/embedded/exec-sandbox.js.map +1 -1
  4. package/dist/embedded/just-bash-bootstrap.d.ts +2 -2
  5. package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
  6. package/dist/embedded/just-bash-bootstrap.js +30 -6
  7. package/dist/embedded/just-bash-bootstrap.js.map +1 -1
  8. package/dist/embedded/mcp-cli-commands.d.ts +5 -5
  9. package/dist/gateway/gateway-integration.js +4 -4
  10. package/dist/gateway/gateway-integration.js.map +1 -1
  11. package/dist/gateway/message-batcher.d.ts.map +1 -1
  12. package/dist/gateway/message-batcher.js +3 -5
  13. package/dist/gateway/message-batcher.js.map +1 -1
  14. package/dist/gateway/sse-client.d.ts +1 -0
  15. package/dist/gateway/sse-client.d.ts.map +1 -1
  16. package/dist/gateway/sse-client.js +13 -8
  17. package/dist/gateway/sse-client.js.map +1 -1
  18. package/dist/gateway/types.d.ts +1 -1
  19. package/dist/gateway/types.d.ts.map +1 -1
  20. package/dist/instructions/builder.d.ts +4 -0
  21. package/dist/instructions/builder.d.ts.map +1 -1
  22. package/dist/instructions/builder.js +8 -11
  23. package/dist/instructions/builder.js.map +1 -1
  24. package/dist/instructions/providers.d.ts +5 -5
  25. package/dist/instructions/providers.d.ts.map +1 -1
  26. package/dist/instructions/providers.js +3 -2
  27. package/dist/instructions/providers.js.map +1 -1
  28. package/dist/openclaw/custom-tools.d.ts +1 -1
  29. package/dist/openclaw/custom-tools.js +1 -1
  30. package/dist/openclaw/instructions.d.ts +9 -9
  31. package/dist/openclaw/instructions.d.ts.map +1 -1
  32. package/dist/openclaw/instructions.js +4 -4
  33. package/dist/openclaw/instructions.js.map +1 -1
  34. package/dist/openclaw/tools.js.map +1 -1
  35. package/dist/openclaw/worker.d.ts +0 -1
  36. package/dist/openclaw/worker.d.ts.map +1 -1
  37. package/dist/openclaw/worker.js +18 -75
  38. package/dist/openclaw/worker.js.map +1 -1
  39. package/dist/shared/tool-implementations.d.ts.map +1 -1
  40. package/dist/shared/tool-implementations.js +37 -13
  41. package/dist/shared/tool-implementations.js.map +1 -1
  42. package/package.json +14 -4
  43. package/src/__tests__/audio-provider-suggestions.test.ts +199 -0
  44. package/src/__tests__/custom-tools.test.ts +92 -0
  45. package/src/__tests__/embedded-just-bash-bootstrap.test.ts +128 -0
  46. package/src/__tests__/embedded-mcp-cli-bash.test.ts +179 -0
  47. package/src/__tests__/embedded-tools.test.ts +744 -0
  48. package/src/__tests__/exec-sandbox-extra.test.ts +0 -0
  49. package/src/__tests__/exec-sandbox.test.ts +550 -0
  50. package/src/__tests__/generated-media.test.ts +142 -0
  51. package/src/__tests__/instructions.test.ts +60 -0
  52. package/src/__tests__/mcp-cli-commands-extra.test.ts +478 -0
  53. package/src/__tests__/mcp-cli-commands.test.ts +383 -0
  54. package/src/__tests__/mcp-tool-call.test.ts +423 -0
  55. package/src/__tests__/memory-flush-harden.test.ts +367 -0
  56. package/src/__tests__/memory-flush-runtime.test.ts +138 -0
  57. package/src/__tests__/memory-flush.test.ts +64 -0
  58. package/src/__tests__/message-batcher.test.ts +247 -0
  59. package/src/__tests__/model-resolver-harden.test.ts +197 -0
  60. package/src/__tests__/model-resolver.test.ts +156 -0
  61. package/src/__tests__/processor-harden.test.ts +269 -0
  62. package/src/__tests__/processor.test.ts +225 -0
  63. package/src/__tests__/replace-base-prompt-identity.test.ts +41 -0
  64. package/src/__tests__/sandbox-leak-harden.test.ts +200 -0
  65. package/src/__tests__/sandbox-leak.test.ts +167 -0
  66. package/src/__tests__/setup.ts +102 -0
  67. package/src/__tests__/sse-client-harden.test.ts +588 -0
  68. package/src/__tests__/sse-client.test.ts +90 -0
  69. package/src/__tests__/tool-implementations.test.ts +196 -0
  70. package/src/__tests__/tool-policy-edge-cases.test.ts +263 -0
  71. package/src/__tests__/tool-policy.test.ts +269 -0
  72. package/src/__tests__/worker.test.ts +89 -0
  73. package/src/core/error-handler.ts +62 -0
  74. package/src/core/project-scanner.ts +65 -0
  75. package/src/core/types.ts +128 -0
  76. package/src/core/workspace.ts +89 -0
  77. package/src/embedded/exec-sandbox.ts +372 -0
  78. package/src/embedded/just-bash-bootstrap.ts +543 -0
  79. package/src/embedded/mcp-cli-commands.ts +402 -0
  80. package/src/gateway/gateway-integration.ts +298 -0
  81. package/src/gateway/message-batcher.ts +123 -0
  82. package/src/gateway/sse-client.ts +951 -0
  83. package/src/gateway/types.ts +68 -0
  84. package/src/index.ts +141 -0
  85. package/src/instructions/builder.ts +45 -0
  86. package/src/instructions/providers.ts +27 -0
  87. package/src/modules/lifecycle.ts +92 -0
  88. package/src/openclaw/custom-tools.ts +315 -0
  89. package/src/openclaw/instructions.ts +36 -0
  90. package/src/openclaw/model-resolver.ts +150 -0
  91. package/src/openclaw/plugin-loader.ts +427 -0
  92. package/src/openclaw/processor.ts +198 -0
  93. package/src/openclaw/sandbox-leak.ts +105 -0
  94. package/src/openclaw/session-context.ts +320 -0
  95. package/src/openclaw/tool-policy.ts +248 -0
  96. package/src/openclaw/tools.ts +277 -0
  97. package/src/openclaw/worker.ts +1847 -0
  98. package/src/server.ts +334 -0
  99. package/src/shared/audio-provider-suggestions.ts +132 -0
  100. package/src/shared/processor-utils.ts +33 -0
  101. package/src/shared/provider-auth-hints.ts +68 -0
  102. package/src/shared/tool-display-config.ts +75 -0
  103. package/src/shared/tool-implementations.ts +940 -0
  104. package/src/shared/worker-env-keys.ts +8 -0
@@ -0,0 +1,196 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+ import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import { rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ askUserQuestion,
8
+ getChannelHistory,
9
+ uploadUserFile,
10
+ } from "../shared/tool-implementations";
11
+
12
+ const originalFetch = globalThis.fetch;
13
+
14
+ const gw = {
15
+ gatewayUrl: "http://gateway",
16
+ workerToken: "worker-token",
17
+ channelId: "channel-1",
18
+ conversationId: "conversation-1",
19
+ platform: "telegram",
20
+ };
21
+
22
+ function extractText(result: {
23
+ content: Array<{ type: "text"; text: string }>;
24
+ }): string {
25
+ return result.content[0]?.text || "";
26
+ }
27
+
28
+ describe("tool implementations", () => {
29
+ afterEach(() => {
30
+ globalThis.fetch = originalFetch;
31
+ mock.restore();
32
+ });
33
+
34
+ test("uploadUserFile uploads an existing file and emits onUploaded metadata", async () => {
35
+ const tempDir = mkdtempSync(join(tmpdir(), "lobu-upload-"));
36
+ const filePath = join(tempDir, "e2e.txt");
37
+ writeFileSync(filePath, "lobu e2e");
38
+
39
+ const uploaded: Array<Record<string, unknown>> = [];
40
+ const fetchMock = mock(
41
+ async (input: RequestInfo | URL, init?: RequestInit) => {
42
+ const url = String(input);
43
+ if (url.endsWith("/internal/files/upload")) {
44
+ const headers = new Headers(init?.headers);
45
+ expect(init?.method).toBe("POST");
46
+ expect(headers.get("Authorization")).toBe("Bearer worker-token");
47
+ expect(headers.get("X-Channel-Id")).toBe("channel-1");
48
+ expect(headers.get("X-Conversation-Id")).toBe("conversation-1");
49
+ return Response.json({
50
+ fileId: "file-123",
51
+ name: "e2e.txt",
52
+ permalink: "https://files.example/e2e.txt",
53
+ });
54
+ }
55
+ throw new Error(`Unexpected fetch: ${url}`);
56
+ }
57
+ );
58
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
59
+
60
+ try {
61
+ const result = await uploadUserFile(
62
+ gw,
63
+ { file_path: filePath, description: "Test file" },
64
+ {
65
+ onUploaded: (payload) => {
66
+ uploaded.push(payload);
67
+ },
68
+ }
69
+ );
70
+
71
+ expect(extractText(result as any)).toContain(
72
+ "Successfully showed e2e.txt to the user"
73
+ );
74
+ expect(uploaded).toEqual([
75
+ {
76
+ tool: "UploadUserFile",
77
+ platform: "telegram",
78
+ fileId: "file-123",
79
+ name: "e2e.txt",
80
+ permalink: "https://files.example/e2e.txt",
81
+ size: 8,
82
+ },
83
+ ]);
84
+ } finally {
85
+ await rm(tempDir, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ test("uploadUserFile forwards artifact fallback metadata", async () => {
90
+ const tempDir = mkdtempSync(join(tmpdir(), "lobu-upload-artifact-"));
91
+ const filePath = join(tempDir, "fallback.txt");
92
+ writeFileSync(filePath, "artifact");
93
+
94
+ const uploaded: Array<Record<string, unknown>> = [];
95
+ globalThis.fetch = mock(async () =>
96
+ Response.json({
97
+ fileId: "artifact-123",
98
+ artifactId: "artifact-123",
99
+ name: "fallback.txt",
100
+ permalink:
101
+ "https://gateway.example.com/api/v1/files/artifact-123?token=abc",
102
+ delivery: "artifact-url",
103
+ })
104
+ ) as unknown as typeof fetch;
105
+
106
+ try {
107
+ const result = await uploadUserFile(
108
+ gw,
109
+ { file_path: filePath },
110
+ {
111
+ onUploaded: (payload) => uploaded.push(payload),
112
+ }
113
+ );
114
+
115
+ expect(extractText(result as any)).toContain(
116
+ "Successfully showed fallback.txt to the user"
117
+ );
118
+ expect(uploaded).toEqual([
119
+ {
120
+ tool: "UploadUserFile",
121
+ platform: "telegram",
122
+ fileId: "artifact-123",
123
+ artifactId: "artifact-123",
124
+ name: "fallback.txt",
125
+ permalink:
126
+ "https://gateway.example.com/api/v1/files/artifact-123?token=abc",
127
+ size: 8,
128
+ delivery: "artifact-url",
129
+ },
130
+ ]);
131
+ } finally {
132
+ await rm(tempDir, { recursive: true, force: true });
133
+ }
134
+ });
135
+
136
+ test("uploadUserFile returns a clear error for missing files", async () => {
137
+ const result = await uploadUserFile(gw, {
138
+ file_path: "/tmp/does-not-exist",
139
+ });
140
+ expect(extractText(result as any)).toContain("not found or is not a file");
141
+ });
142
+
143
+ test("askUserQuestion posts a question interaction", async () => {
144
+ let capturedBody: Record<string, unknown> | null = null;
145
+ globalThis.fetch = mock(
146
+ async (_input: RequestInfo | URL, init?: RequestInit) => {
147
+ capturedBody = JSON.parse(String(init?.body));
148
+ return Response.json({ id: "question-1" });
149
+ }
150
+ ) as unknown as typeof fetch;
151
+
152
+ const result = await askUserQuestion(gw, {
153
+ question: "Pick one",
154
+ options: ["A", "B"],
155
+ });
156
+
157
+ expect(capturedBody).toEqual({
158
+ interactionType: "question",
159
+ question: "Pick one",
160
+ options: ["A", "B"],
161
+ });
162
+ expect(extractText(result as any)).toContain(
163
+ "Question posted with buttons"
164
+ );
165
+ });
166
+
167
+ test("getChannelHistory returns note responses and formatted history", async () => {
168
+ globalThis.fetch = mock(async () =>
169
+ Response.json({ note: "History unavailable" })
170
+ ) as unknown as typeof fetch;
171
+
172
+ const note = await getChannelHistory(gw, { limit: 5 });
173
+ expect(extractText(note as any)).toBe("History unavailable");
174
+
175
+ globalThis.fetch = mock(async () =>
176
+ Response.json({
177
+ messages: [
178
+ {
179
+ timestamp: "2026-04-11T18:30:00.000Z",
180
+ user: "Burak",
181
+ text: "Hello",
182
+ isBot: false,
183
+ },
184
+ ],
185
+ nextCursor: "2026-04-11T18:00:00.000Z",
186
+ hasMore: true,
187
+ })
188
+ ) as unknown as typeof fetch;
189
+
190
+ const history = await getChannelHistory(gw, { limit: 5 });
191
+ const text = extractText(history as any);
192
+ expect(text).toContain("Found 1 messages");
193
+ expect(text).toContain("Burak: Hello");
194
+ expect(text).toContain('before="2026-04-11T18:00:00.000Z"');
195
+ });
196
+ });
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Tool-Policy Edge-Case Tests
3
+ *
4
+ * Supplements the main tool-policy.test.ts with cases that were missing:
5
+ *
6
+ * - isDirectPackageInstallCommand: compound commands, piped package installs,
7
+ * edge cases that should NOT be caught (false positives)
8
+ * - enforceBashCommandPolicy: empty allow-prefixes with allowAll=false
9
+ * (no filter = pass-through) versus explicit empty allow-prefixes
10
+ * - buildToolPolicy: wildcard prefix matching (e.g. "Read*")
11
+ * - normalizeToolList: mixed array with numbers coerced to strings
12
+ * - isToolAllowedByPolicy: tool name with trailing/leading whitespace in policy
13
+ * - Bash deny entries do NOT block other tool names that happen to start with "Bash"
14
+ */
15
+
16
+ import { describe, expect, test } from "bun:test";
17
+ import {
18
+ buildToolPolicy,
19
+ enforceBashCommandPolicy,
20
+ isDirectPackageInstallCommand,
21
+ isToolAllowedByPolicy,
22
+ normalizeToolList,
23
+ type BashCommandPolicy,
24
+ } from "../openclaw/tool-policy";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // isDirectPackageInstallCommand
28
+ // ---------------------------------------------------------------------------
29
+
30
+ describe("isDirectPackageInstallCommand", () => {
31
+ // Should detect
32
+ const detected = [
33
+ "npm install lodash",
34
+ "npm i lodash",
35
+ "npm install",
36
+ "pnpm add react",
37
+ "pnpm install",
38
+ "yarn add typescript",
39
+ "yarn install",
40
+ "bun install",
41
+ "bun add express",
42
+ "pip install requests",
43
+ "pip3 install requests",
44
+ "uv pip install pandas",
45
+ "cargo install ripgrep",
46
+ "go install golang.org/x/tools/gopls@latest",
47
+ "gem install bundler",
48
+ "poetry add numpy",
49
+ "composer require monolog/monolog",
50
+ "apt install curl",
51
+ "apt-get install -y ffmpeg",
52
+ "sudo apt install curl",
53
+ "sudo apt-get install curl",
54
+ "brew install wget",
55
+ "apk add bash",
56
+ // piped / chained
57
+ "echo hi | npm install",
58
+ "true && npm install foo",
59
+ "npm install; echo done",
60
+ // quoted inside
61
+ "bash -c 'npm install foo'",
62
+ ];
63
+
64
+ for (const cmd of detected) {
65
+ test(`detects package install: ${cmd}`, () => {
66
+ expect(isDirectPackageInstallCommand(cmd)).toBe(true);
67
+ });
68
+ }
69
+
70
+ // Should NOT detect (false positive guard).
71
+ // Note: "brew list" IS detected (brew prefix matches) — intentionally conservative.
72
+ // Note: "apt-get update" IS detected (apt-get prefix matches) — intentionally conservative.
73
+ // Note: "echo npm install" IS detected via regex (embedded npm install) — intentionally conservative.
74
+ const allowed = [
75
+ "",
76
+ " ",
77
+ "git status",
78
+ "npm run build", // npm run ≠ npm install
79
+ "npm test",
80
+ "npm start",
81
+ "npx create-react-app my-app", // npx not npm install
82
+ "pip list", // pip list ≠ pip install
83
+ "pip show requests",
84
+ "pnpm run dev",
85
+ "bun run dev",
86
+ "bun test",
87
+ "yarn run test",
88
+ "cargo build",
89
+ "go build ./...",
90
+ "gem list",
91
+ "cat npm-install.log",
92
+ ];
93
+
94
+ for (const cmd of allowed) {
95
+ test(`does not falsely detect: ${cmd || "(empty)"}`, () => {
96
+ expect(isDirectPackageInstallCommand(cmd)).toBe(false);
97
+ });
98
+ }
99
+
100
+ // Conservative over-detection: document actual behavior to catch regressions
101
+ test("brew list IS detected (brew prefix is in deny list — conservative)", () => {
102
+ expect(isDirectPackageInstallCommand("brew list")).toBe(true);
103
+ });
104
+
105
+ test("apt-get update IS detected (apt-get prefix is in deny list — conservative)", () => {
106
+ expect(isDirectPackageInstallCommand("apt-get update")).toBe(true);
107
+ });
108
+
109
+ test("echo npm install IS detected (regex matches embedded npm install)", () => {
110
+ // The DIRECT_PACKAGE_INSTALL_PATTERNS match npm install anywhere in the command
111
+ expect(isDirectPackageInstallCommand("echo npm install")).toBe(true);
112
+ });
113
+ });
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // normalizeToolList edge cases
117
+ // ---------------------------------------------------------------------------
118
+
119
+ describe("normalizeToolList edge cases", () => {
120
+ test("numbers in array are coerced to strings", () => {
121
+ // @ts-expect-error: intentional wrong type to test coercion
122
+ expect(normalizeToolList([1, 2, 3])).toEqual(["1", "2", "3"]);
123
+ });
124
+
125
+ test("mixed newline + comma separation", () => {
126
+ expect(normalizeToolList("Read,Write\nEdit")).toEqual([
127
+ "Read",
128
+ "Write",
129
+ "Edit",
130
+ ]);
131
+ });
132
+
133
+ test("single entry with no delimiter", () => {
134
+ expect(normalizeToolList("Read")).toEqual(["Read"]);
135
+ });
136
+
137
+ test("only whitespace entries are all filtered out", () => {
138
+ expect(normalizeToolList(" , , \n ")).toEqual([]);
139
+ });
140
+ });
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // buildToolPolicy: wildcard prefix pattern
144
+ // ---------------------------------------------------------------------------
145
+
146
+ describe("buildToolPolicy wildcard prefix", () => {
147
+ test("Bash(git:*) in allowed extracts 'git' prefix", () => {
148
+ const policy = buildToolPolicy({ allowedTools: ["Bash(git:*)"] });
149
+ expect(policy.bashPolicy.allowPrefixes).toContain("git");
150
+ });
151
+
152
+ test("wildcard prefix 'Read*' in allowedPatterns matches ReadFile and ReadDir", () => {
153
+ const policy = buildToolPolicy({
154
+ toolsConfig: { strictMode: true },
155
+ allowedTools: ["Read*"],
156
+ });
157
+ expect(isToolAllowedByPolicy("ReadFile", policy)).toBe(true);
158
+ expect(isToolAllowedByPolicy("ReadDir", policy)).toBe(true);
159
+ expect(isToolAllowedByPolicy("WriteFile", policy)).toBe(false);
160
+ });
161
+
162
+ test("wildcard '*' in deniedPatterns blocks everything", () => {
163
+ const policy = buildToolPolicy({ disallowedTools: ["*"] });
164
+ expect(isToolAllowedByPolicy("Read", policy)).toBe(false);
165
+ expect(isToolAllowedByPolicy("Write", policy)).toBe(false);
166
+ });
167
+ });
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // isToolAllowedByPolicy edge cases
171
+ // ---------------------------------------------------------------------------
172
+
173
+ describe("isToolAllowedByPolicy edge cases", () => {
174
+ test("tool name with leading/trailing whitespace in policy entry is trimmed and matched", () => {
175
+ const policy = buildToolPolicy({ disallowedTools: [" Write "] });
176
+ // The denied pattern is stored trimmed → "Write"
177
+ expect(isToolAllowedByPolicy("Write", policy)).toBe(false);
178
+ });
179
+
180
+ test("Bash deny filter (Bash(rm:*)) does NOT block unrelated tool 'BashHelper'", () => {
181
+ const policy = buildToolPolicy({ disallowedTools: ["Bash(rm:*)"] });
182
+ // BashHelper is not the Bash tool itself
183
+ expect(isToolAllowedByPolicy("BashHelper", policy)).toBe(true);
184
+ });
185
+
186
+ test("strict mode blocks unlisted tool even if allowedPatterns is non-empty", () => {
187
+ const policy = buildToolPolicy({
188
+ toolsConfig: { strictMode: true, allowedTools: ["Read"] },
189
+ });
190
+ expect(isToolAllowedByPolicy("Write", policy)).toBe(false);
191
+ expect(isToolAllowedByPolicy("Read", policy)).toBe(true);
192
+ });
193
+
194
+ test("deny list takes priority over wildcard allow", () => {
195
+ const policy = buildToolPolicy({
196
+ allowedTools: ["*"],
197
+ disallowedTools: ["Write"],
198
+ });
199
+ expect(isToolAllowedByPolicy("Write", policy)).toBe(false);
200
+ expect(isToolAllowedByPolicy("Read", policy)).toBe(true);
201
+ });
202
+ });
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // enforceBashCommandPolicy edge cases
206
+ // ---------------------------------------------------------------------------
207
+
208
+ describe("enforceBashCommandPolicy edge cases", () => {
209
+ test("deny prefix matched case-insensitively on uppercase command", () => {
210
+ const policy: BashCommandPolicy = {
211
+ allowAll: true,
212
+ allowPrefixes: [],
213
+ denyPrefixes: ["rm"],
214
+ };
215
+ expect(() => enforceBashCommandPolicy("RM file.txt", policy)).toThrow(
216
+ "Bash command denied by policy"
217
+ );
218
+ });
219
+
220
+ test("allow prefix matched case-insensitively", () => {
221
+ const policy: BashCommandPolicy = {
222
+ allowAll: false,
223
+ allowPrefixes: ["git"],
224
+ denyPrefixes: [],
225
+ };
226
+ // "GIT status" matches allowPrefix "git" (case-insensitive)
227
+ expect(() => enforceBashCommandPolicy("GIT status", policy)).not.toThrow();
228
+ });
229
+
230
+ test("command that is a prefix of a deny rule but does not match is allowed", () => {
231
+ const policy: BashCommandPolicy = {
232
+ allowAll: true,
233
+ allowPrefixes: [],
234
+ // "rm " (with space) — "rmdir" does NOT start with "rm "
235
+ denyPrefixes: ["rm "],
236
+ };
237
+ expect(() =>
238
+ enforceBashCommandPolicy("rmdir /tmp/safe", policy)
239
+ ).not.toThrow();
240
+ });
241
+
242
+ test("pip install caught by default policy", () => {
243
+ const policy = buildToolPolicy({});
244
+ expect(() =>
245
+ enforceBashCommandPolicy("pip install requests", policy.bashPolicy)
246
+ ).toThrow("Bash command denied by policy");
247
+ });
248
+
249
+ test("npm install caught by default policy", () => {
250
+ const policy = buildToolPolicy({});
251
+ expect(() =>
252
+ enforceBashCommandPolicy("npm install lodash", policy.bashPolicy)
253
+ ).toThrow("Bash command denied by policy");
254
+ });
255
+
256
+ test("npm run build NOT caught by default policy", () => {
257
+ const policy = buildToolPolicy({});
258
+ // "npm install " and "npm i " are in the deny list — "npm run" is not
259
+ expect(() =>
260
+ enforceBashCommandPolicy("npm run build", policy.bashPolicy)
261
+ ).not.toThrow();
262
+ });
263
+ });