@tpsdev-ai/agent 0.5.2 → 0.5.4

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.
@@ -0,0 +1,428 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ProviderManager } from "../src/llm/provider.js";
3
+
4
+ // We test the response mappers by calling complete() with mocked fetch.
5
+ // Each provider has a distinct response format — these tests ensure
6
+ // tool calls are normalized correctly into { id, name, input }.
7
+
8
+ function makeProvider(provider: "anthropic" | "openai" | "ollama" | "google") {
9
+ return new ProviderManager({
10
+ provider,
11
+ model: "test-model",
12
+ apiKey: "test-key",
13
+ });
14
+ }
15
+
16
+ // --- Anthropic ---
17
+
18
+ describe("Anthropic response mapping", () => {
19
+ test("parses tool_use blocks", async () => {
20
+ const pm = makeProvider("anthropic");
21
+ const raw = {
22
+ content: [
23
+ { type: "text", text: "I'll write that file." },
24
+ {
25
+ type: "tool_use",
26
+ id: "toolu_123",
27
+ name: "write",
28
+ input: { path: "hello.txt", content: "Hello!" },
29
+ },
30
+ ],
31
+ usage: { input_tokens: 100, output_tokens: 50 },
32
+ };
33
+
34
+ globalThis.fetch = async () =>
35
+ new Response(JSON.stringify(raw), { status: 200 });
36
+
37
+ const res = await pm.complete({
38
+ messages: [{ role: "user", content: "test" }],
39
+ tools: [{ name: "write", description: "Write a file", input_schema: { type: "object", properties: {} } }],
40
+ });
41
+
42
+ expect(res.content).toBe("I'll write that file.");
43
+ expect(res.toolCalls).toHaveLength(1);
44
+ expect(res.toolCalls![0].id).toBe("toolu_123");
45
+ expect(res.toolCalls![0].name).toBe("write");
46
+ expect(res.toolCalls![0].input).toEqual({ path: "hello.txt", content: "Hello!" });
47
+ expect(res.inputTokens).toBe(100);
48
+ expect(res.outputTokens).toBe(50);
49
+ });
50
+
51
+ test("handles text-only response (no tools)", async () => {
52
+ const pm = makeProvider("anthropic");
53
+ const raw = {
54
+ content: [{ type: "text", text: "Done." }],
55
+ usage: { input_tokens: 10, output_tokens: 5 },
56
+ };
57
+
58
+ globalThis.fetch = async () =>
59
+ new Response(JSON.stringify(raw), { status: 200 });
60
+
61
+ const res = await pm.complete({
62
+ messages: [{ role: "user", content: "test" }],
63
+ tools: [],
64
+ });
65
+
66
+ expect(res.content).toBe("Done.");
67
+ expect(res.toolCalls).toBeUndefined();
68
+ });
69
+
70
+ test("handles multiple tool calls", async () => {
71
+ const pm = makeProvider("anthropic");
72
+ const raw = {
73
+ content: [
74
+ { type: "tool_use", id: "t1", name: "read", input: { path: "a.txt" } },
75
+ { type: "tool_use", id: "t2", name: "write", input: { path: "b.txt", content: "hi" } },
76
+ ],
77
+ usage: { input_tokens: 0, output_tokens: 0 },
78
+ };
79
+
80
+ globalThis.fetch = async () =>
81
+ new Response(JSON.stringify(raw), { status: 200 });
82
+
83
+ const res = await pm.complete({
84
+ messages: [{ role: "user", content: "test" }],
85
+ tools: [],
86
+ });
87
+
88
+ expect(res.toolCalls).toHaveLength(2);
89
+ expect(res.toolCalls![0].name).toBe("read");
90
+ expect(res.toolCalls![1].name).toBe("write");
91
+ });
92
+ });
93
+
94
+ // --- OpenAI ---
95
+
96
+ describe("OpenAI response mapping", () => {
97
+ test("parses function tool calls", async () => {
98
+ const pm = makeProvider("openai");
99
+ const raw = {
100
+ choices: [
101
+ {
102
+ message: {
103
+ content: "",
104
+ tool_calls: [
105
+ {
106
+ id: "call_abc",
107
+ function: {
108
+ name: "exec",
109
+ arguments: '{"command":"ls -la"}',
110
+ },
111
+ },
112
+ ],
113
+ },
114
+ },
115
+ ],
116
+ usage: { prompt_tokens: 200, completion_tokens: 30 },
117
+ };
118
+
119
+ globalThis.fetch = async () =>
120
+ new Response(JSON.stringify(raw), { status: 200 });
121
+
122
+ const res = await pm.complete({
123
+ messages: [{ role: "user", content: "test" }],
124
+ tools: [{ name: "exec", description: "Run command", input_schema: { type: "object", properties: {} } }],
125
+ });
126
+
127
+ expect(res.toolCalls).toHaveLength(1);
128
+ expect(res.toolCalls![0].id).toBe("call_abc");
129
+ expect(res.toolCalls![0].name).toBe("exec");
130
+ expect(res.toolCalls![0].input).toEqual({ command: "ls -la" });
131
+ expect(res.inputTokens).toBe(200);
132
+ expect(res.outputTokens).toBe(30);
133
+ });
134
+
135
+ test("handles text-only response", async () => {
136
+ const pm = makeProvider("openai");
137
+ const raw = {
138
+ choices: [{ message: { content: "All done.", tool_calls: undefined } }],
139
+ usage: { prompt_tokens: 10, completion_tokens: 5 },
140
+ };
141
+
142
+ globalThis.fetch = async () =>
143
+ new Response(JSON.stringify(raw), { status: 200 });
144
+
145
+ const res = await pm.complete({
146
+ messages: [{ role: "user", content: "test" }],
147
+ tools: [],
148
+ });
149
+
150
+ expect(res.content).toBe("All done.");
151
+ expect(res.toolCalls).toBeUndefined();
152
+ });
153
+ });
154
+
155
+ // --- Ollama ---
156
+
157
+ describe("Ollama response mapping", () => {
158
+ test("parses function tool calls (object arguments)", async () => {
159
+ const pm = makeProvider("ollama");
160
+ const raw = {
161
+ message: {
162
+ role: "assistant",
163
+ content: "",
164
+ tool_calls: [
165
+ {
166
+ id: "call_xyz",
167
+ function: {
168
+ name: "write",
169
+ arguments: { path: "test.txt", content: "hello" },
170
+ },
171
+ },
172
+ ],
173
+ },
174
+ prompt_eval_count: 143,
175
+ eval_count: 100,
176
+ };
177
+
178
+ globalThis.fetch = async () =>
179
+ new Response(JSON.stringify(raw), { status: 200 });
180
+
181
+ const res = await pm.complete({
182
+ messages: [{ role: "user", content: "test" }],
183
+ tools: [{ name: "write", description: "Write a file", input_schema: { type: "object", properties: {} } }],
184
+ });
185
+
186
+ expect(res.toolCalls).toHaveLength(1);
187
+ expect(res.toolCalls![0].name).toBe("write");
188
+ expect(res.toolCalls![0].input).toEqual({ path: "test.txt", content: "hello" });
189
+ expect(res.inputTokens).toBe(143);
190
+ expect(res.outputTokens).toBe(100);
191
+ });
192
+
193
+ test("parses function tool calls (string arguments)", async () => {
194
+ const pm = makeProvider("ollama");
195
+ const raw = {
196
+ message: {
197
+ role: "assistant",
198
+ content: "",
199
+ tool_calls: [
200
+ {
201
+ id: "call_str",
202
+ function: {
203
+ name: "read",
204
+ arguments: '{"path":"config.yaml"}',
205
+ },
206
+ },
207
+ ],
208
+ },
209
+ prompt_eval_count: 50,
210
+ eval_count: 20,
211
+ };
212
+
213
+ globalThis.fetch = async () =>
214
+ new Response(JSON.stringify(raw), { status: 200 });
215
+
216
+ const res = await pm.complete({
217
+ messages: [{ role: "user", content: "test" }],
218
+ tools: [{ name: "read", description: "Read a file", input_schema: { type: "object", properties: {} } }],
219
+ });
220
+
221
+ expect(res.toolCalls).toHaveLength(1);
222
+ expect(res.toolCalls![0].name).toBe("read");
223
+ expect(res.toolCalls![0].input).toEqual({ path: "config.yaml" });
224
+ });
225
+
226
+ test("handles text-only response", async () => {
227
+ const pm = makeProvider("ollama");
228
+ const raw = {
229
+ message: { role: "assistant", content: "File written." },
230
+ prompt_eval_count: 10,
231
+ eval_count: 5,
232
+ };
233
+
234
+ globalThis.fetch = async () =>
235
+ new Response(JSON.stringify(raw), { status: 200 });
236
+
237
+ const res = await pm.complete({
238
+ messages: [{ role: "user", content: "test" }],
239
+ tools: [],
240
+ });
241
+
242
+ expect(res.content).toBe("File written.");
243
+ expect(res.toolCalls).toBeUndefined();
244
+ });
245
+ });
246
+
247
+ // --- Google ---
248
+
249
+ describe("Google response mapping", () => {
250
+ test("parses functionCall parts", async () => {
251
+ const pm = makeProvider("google");
252
+ const raw = {
253
+ candidates: [
254
+ {
255
+ content: {
256
+ parts: [
257
+ {
258
+ functionCall: {
259
+ name: "write",
260
+ args: { path: "out.txt", content: "data" },
261
+ },
262
+ },
263
+ ],
264
+ },
265
+ },
266
+ ],
267
+ usageMetadata: { promptTokenCount: 80, candidatesTokenCount: 40 },
268
+ };
269
+
270
+ globalThis.fetch = async () =>
271
+ new Response(JSON.stringify(raw), { status: 200 });
272
+
273
+ const res = await pm.complete({
274
+ messages: [{ role: "user", content: "test" }],
275
+ tools: [{ name: "write", description: "Write", input_schema: { type: "object", properties: {} } }],
276
+ });
277
+
278
+ expect(res.toolCalls).toHaveLength(1);
279
+ expect(res.toolCalls![0].name).toBe("write");
280
+ expect(res.toolCalls![0].input).toEqual({ path: "out.txt", content: "data" });
281
+ expect(res.inputTokens).toBe(80);
282
+ expect(res.outputTokens).toBe(40);
283
+ });
284
+
285
+ test("parses text-only response", async () => {
286
+ const pm = makeProvider("google");
287
+ const raw = {
288
+ candidates: [
289
+ {
290
+ content: {
291
+ parts: [{ text: "Here's your answer." }],
292
+ },
293
+ },
294
+ ],
295
+ usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 8 },
296
+ };
297
+
298
+ globalThis.fetch = async () =>
299
+ new Response(JSON.stringify(raw), { status: 200 });
300
+
301
+ const res = await pm.complete({
302
+ messages: [{ role: "user", content: "test" }],
303
+ tools: [],
304
+ });
305
+
306
+ expect(res.content).toBe("Here's your answer.");
307
+ expect(res.toolCalls).toBeUndefined();
308
+ });
309
+
310
+ test("handles mixed text + functionCall parts", async () => {
311
+ const pm = makeProvider("google");
312
+ const raw = {
313
+ candidates: [
314
+ {
315
+ content: {
316
+ parts: [
317
+ { text: "Let me check." },
318
+ { functionCall: { name: "read", args: { path: "data.json" } } },
319
+ ],
320
+ },
321
+ },
322
+ ],
323
+ usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 },
324
+ };
325
+
326
+ globalThis.fetch = async () =>
327
+ new Response(JSON.stringify(raw), { status: 200 });
328
+
329
+ const res = await pm.complete({
330
+ messages: [{ role: "user", content: "test" }],
331
+ tools: [],
332
+ });
333
+
334
+ expect(res.content).toBe("Let me check.");
335
+ expect(res.toolCalls).toHaveLength(1);
336
+ expect(res.toolCalls![0].name).toBe("read");
337
+ });
338
+ });
339
+
340
+ // --- Tool schema formatting ---
341
+
342
+ describe("Tool schema formatting", () => {
343
+ const spec = {
344
+ name: "write",
345
+ description: "Write a file",
346
+ input_schema: { type: "object", properties: { path: { type: "string" } } },
347
+ };
348
+
349
+ test("Anthropic format", () => {
350
+ const pm = makeProvider("anthropic");
351
+ const fn = pm.toolInputSchemaFor("anthropic");
352
+ const result = fn([spec]) as any[];
353
+ expect(result[0].name).toBe("write");
354
+ expect(result[0].input_schema).toBeDefined();
355
+ expect(result[0].function).toBeUndefined();
356
+ });
357
+
358
+ test("OpenAI format wraps in function object", () => {
359
+ const pm = makeProvider("openai");
360
+ const fn = pm.toolInputSchemaFor("openai");
361
+ const result = fn([spec]) as any[];
362
+ expect(result[0].type).toBe("function");
363
+ expect(result[0].function.name).toBe("write");
364
+ expect(result[0].function.parameters).toBeDefined();
365
+ });
366
+
367
+ test("Ollama uses OpenAI format", () => {
368
+ const pm = makeProvider("ollama");
369
+ const fn = pm.toolInputSchemaFor("ollama");
370
+ const result = fn([spec]) as any[];
371
+ expect(result[0].type).toBe("function");
372
+ expect(result[0].function.name).toBe("write");
373
+ });
374
+
375
+ test("Google format uses functionDeclarations", () => {
376
+ const pm = makeProvider("google");
377
+ const fn = pm.toolInputSchemaFor("google");
378
+ const result = fn([spec]) as any;
379
+ expect(result.functionDeclarations).toHaveLength(1);
380
+ expect(result.functionDeclarations[0].name).toBe("write");
381
+ });
382
+ });
383
+
384
+ // --- Edge cases ---
385
+
386
+ describe("Edge cases", () => {
387
+ test("safeJson handles malformed string", async () => {
388
+ const pm = makeProvider("openai");
389
+ const raw = {
390
+ choices: [
391
+ {
392
+ message: {
393
+ tool_calls: [
394
+ { id: "c1", function: { name: "exec", arguments: "not json{" } },
395
+ ],
396
+ },
397
+ },
398
+ ],
399
+ usage: { prompt_tokens: 0, completion_tokens: 0 },
400
+ };
401
+
402
+ globalThis.fetch = async () =>
403
+ new Response(JSON.stringify(raw), { status: 200 });
404
+
405
+ const res = await pm.complete({
406
+ messages: [{ role: "user", content: "test" }],
407
+ tools: [],
408
+ });
409
+
410
+ expect(res.toolCalls![0].input).toEqual({});
411
+ });
412
+
413
+ test("handles empty/missing content gracefully", async () => {
414
+ const pm = makeProvider("anthropic");
415
+ const raw = { content: [], usage: {} };
416
+
417
+ globalThis.fetch = async () =>
418
+ new Response(JSON.stringify(raw), { status: 200 });
419
+
420
+ const res = await pm.complete({
421
+ messages: [{ role: "user", content: "test" }],
422
+ tools: [],
423
+ });
424
+
425
+ expect(res.content).toBe("");
426
+ expect(res.toolCalls).toBeUndefined();
427
+ });
428
+ });
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Security regression tests for mail trust and capability scoping.
3
+ * Each test maps to a finding in SECURITY.md.
4
+ */
5
+ import { describe, test, expect, mock, beforeEach } from "bun:test";
6
+ import { EventLoop } from "../../src/runtime/event-loop.js";
7
+ import type { AgentConfig, LLMMessage, ToolSpec, CompletionResponse } from "../../src/runtime/types.js";
8
+ import type { MemoryStore } from "../../src/io/memory.js";
9
+ import type { ContextManager } from "../../src/io/context.js";
10
+ import type { ProviderManager } from "../../src/llm/provider.js";
11
+ import { ToolRegistry } from "../../src/tools/registry.js";
12
+
13
+ function makeConfig(overrides: Partial<AgentConfig> = {}): AgentConfig {
14
+ return {
15
+ name: "test-agent",
16
+ agentId: "test",
17
+ workspace: "/tmp/test-workspace",
18
+ provider: "anthropic",
19
+ model: "test",
20
+ maxToolTurns: 5,
21
+ ...overrides,
22
+ } as AgentConfig;
23
+ }
24
+
25
+ function makeMemory(): MemoryStore {
26
+ return { append: mock(() => Promise.resolve()) } as any;
27
+ }
28
+
29
+ function makeContext(): ContextManager {
30
+ return {} as any;
31
+ }
32
+
33
+ /**
34
+ * Build a provider mock that captures what tools were passed to it.
35
+ */
36
+ function makeProvider(capturedTools: ToolSpec[][]): ProviderManager {
37
+ return {
38
+ complete: mock((req: any) => {
39
+ capturedTools.push([...req.tools]);
40
+ return Promise.resolve({
41
+ content: "Done.",
42
+ toolCalls: undefined,
43
+ inputTokens: 10,
44
+ outputTokens: 5,
45
+ } as CompletionResponse);
46
+ }),
47
+ } as any;
48
+ }
49
+
50
+ function makeToolRegistry(): ToolRegistry {
51
+ const reg = new ToolRegistry();
52
+ reg.register({
53
+ name: "read",
54
+ description: "Read a file",
55
+ input_schema: { path: { type: "string" } },
56
+ execute: async () => ({ content: "file contents" }),
57
+ });
58
+ reg.register({
59
+ name: "write",
60
+ description: "Write a file",
61
+ input_schema: { path: { type: "string" }, content: { type: "string" } },
62
+ execute: async () => ({ content: "ok" }),
63
+ });
64
+ reg.register({
65
+ name: "edit",
66
+ description: "Edit a file",
67
+ input_schema: { path: { type: "string" }, old_string: { type: "string" }, new_string: { type: "string" } },
68
+ execute: async () => ({ content: "ok" }),
69
+ });
70
+ reg.register({
71
+ name: "exec",
72
+ description: "Execute a command",
73
+ input_schema: { command: { type: "string" } },
74
+ execute: async () => ({ content: "output" }),
75
+ });
76
+ reg.register({
77
+ name: "mail",
78
+ description: "Send mail",
79
+ input_schema: { to: { type: "string" }, body: { type: "string" } },
80
+ execute: async () => ({ content: "sent" }),
81
+ });
82
+ return reg;
83
+ }
84
+
85
+ describe("S43-A: internal mail drops exec", () => {
86
+ test("user trust gets exec", async () => {
87
+ const captured: ToolSpec[][] = [];
88
+ const loop = new EventLoop({
89
+ config: makeConfig(),
90
+ memory: makeMemory(),
91
+ context: makeContext(),
92
+ provider: makeProvider(captured),
93
+ tools: makeToolRegistry(),
94
+ });
95
+
96
+ await loop.runOnce("hello"); // runOnce uses trust=user
97
+ expect(captured.length).toBeGreaterThan(0);
98
+ const toolNames = captured[0].map((t) => t.name);
99
+ expect(toolNames).toContain("exec");
100
+ });
101
+
102
+ test("internal trust does NOT get exec", async () => {
103
+ const captured: ToolSpec[][] = [];
104
+ const loop = new EventLoop({
105
+ config: makeConfig(),
106
+ memory: makeMemory(),
107
+ context: makeContext(),
108
+ provider: makeProvider(captured),
109
+ tools: makeToolRegistry(),
110
+ });
111
+
112
+ // Simulate internal mail
113
+ const mail = {
114
+ body: "do something",
115
+ headers: { "X-TPS-Trust": "internal", "X-TPS-Sender": "agent-coder" },
116
+ };
117
+
118
+ // Access private processMail via run() with injected inbox
119
+ let callCount = 0;
120
+ await loop.run(async () => {
121
+ if (callCount++ === 0) return [mail as any];
122
+ await loop.stop();
123
+ return [];
124
+ });
125
+
126
+ expect(captured.length).toBeGreaterThan(0);
127
+ const toolNames = captured[0].map((t) => t.name);
128
+ expect(toolNames).not.toContain("exec");
129
+ expect(toolNames).toContain("read");
130
+ expect(toolNames).toContain("write");
131
+ expect(toolNames).toContain("mail");
132
+ });
133
+
134
+ test("external trust does NOT get exec", async () => {
135
+ const captured: ToolSpec[][] = [];
136
+ const loop = new EventLoop({
137
+ config: makeConfig(),
138
+ memory: makeMemory(),
139
+ context: makeContext(),
140
+ provider: makeProvider(captured),
141
+ tools: makeToolRegistry(),
142
+ });
143
+
144
+ const mail = {
145
+ body: "do something",
146
+ headers: { "X-TPS-Trust": "external", "X-TPS-Sender": "unknown" },
147
+ };
148
+
149
+ let callCount = 0;
150
+ await loop.run(async () => {
151
+ if (callCount++ === 0) return [mail as any];
152
+ await loop.stop();
153
+ return [];
154
+ });
155
+
156
+ expect(captured.length).toBeGreaterThan(0);
157
+ const toolNames = captured[0].map((t) => t.name);
158
+ expect(toolNames).not.toContain("exec");
159
+ });
160
+ });
161
+
162
+ describe("S43-D: scratch path traversal", () => {
163
+ test("external mail write to scratch/file.txt is allowed", async () => {
164
+ const memory = makeMemory();
165
+ const tools = makeToolRegistry();
166
+ const writeResults: string[] = [];
167
+
168
+ // Override write tool to capture calls
169
+ tools.register({
170
+ name: "write",
171
+ description: "Write a file",
172
+ input_schema: { path: { type: "string" }, content: { type: "string" } },
173
+ execute: async (input: any) => {
174
+ writeResults.push(input.path);
175
+ return { content: "ok" };
176
+ },
177
+ });
178
+
179
+ const provider = {
180
+ complete: mock(async (req: any) => {
181
+ // First call: request a write to scratch/file.txt
182
+ if ((provider.complete as any).mock.calls.length <= 1) {
183
+ return {
184
+ content: "",
185
+ toolCalls: [{ id: "1", name: "write", input: { path: "scratch/file.txt", content: "hello" } }],
186
+ inputTokens: 10,
187
+ outputTokens: 5,
188
+ };
189
+ }
190
+ return { content: "Done.", toolCalls: undefined, inputTokens: 10, outputTokens: 5 };
191
+ }),
192
+ } as any;
193
+
194
+ const loop = new EventLoop({
195
+ config: makeConfig(),
196
+ memory,
197
+ context: makeContext(),
198
+ provider,
199
+ tools,
200
+ });
201
+
202
+ const mail = {
203
+ body: "write a file",
204
+ headers: { "X-TPS-Trust": "external", "X-TPS-Sender": "outsider" },
205
+ };
206
+
207
+ let callCount = 0;
208
+ await loop.run(async () => {
209
+ if (callCount++ === 0) return [mail as any];
210
+ await loop.stop();
211
+ return [];
212
+ });
213
+
214
+ expect(writeResults).toContain("scratch/file.txt");
215
+ });
216
+
217
+ test("external mail write to scratch/../../etc/passwd is BLOCKED", async () => {
218
+ const memory = makeMemory();
219
+ const memoryAppendCalls: any[] = [];
220
+ (memory.append as any).mockImplementation((entry: any) => {
221
+ memoryAppendCalls.push(entry);
222
+ return Promise.resolve();
223
+ });
224
+
225
+ const provider = {
226
+ complete: mock(async (req: any) => {
227
+ if ((provider.complete as any).mock.calls.length <= 1) {
228
+ return {
229
+ content: "",
230
+ toolCalls: [{ id: "1", name: "write", input: { path: "scratch/../../etc/passwd", content: "pwned" } }],
231
+ inputTokens: 10,
232
+ outputTokens: 5,
233
+ };
234
+ }
235
+ return { content: "Done.", toolCalls: undefined, inputTokens: 10, outputTokens: 5 };
236
+ }),
237
+ } as any;
238
+
239
+ const loop = new EventLoop({
240
+ config: makeConfig(),
241
+ memory,
242
+ context: makeContext(),
243
+ provider,
244
+ tools: makeToolRegistry(),
245
+ });
246
+
247
+ const mail = {
248
+ body: "write a file",
249
+ headers: { "X-TPS-Trust": "external", "X-TPS-Sender": "attacker" },
250
+ };
251
+
252
+ let callCount = 0;
253
+ await loop.run(async () => {
254
+ if (callCount++ === 0) return [mail as any];
255
+ await loop.stop();
256
+ return [];
257
+ });
258
+
259
+ // Should have logged a permission denied error
260
+ const denials = memoryAppendCalls.filter(
261
+ (e: any) => e.type === "tool_result" && e.data?.result?.isError,
262
+ );
263
+ expect(denials.length).toBeGreaterThan(0);
264
+ expect(denials[0].data.result.content).toContain("Permission denied");
265
+ });
266
+ });