@gotgenes/pi-permission-system 8.2.0 → 8.2.1

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.
@@ -7,8 +7,90 @@ import {
7
7
  loadAndMergeConfigs,
8
8
  loadUnifiedConfig,
9
9
  mergeUnifiedConfigs,
10
+ stripJsonComments,
10
11
  } from "#src/config-loader";
11
12
 
13
+ describe("stripJsonComments", () => {
14
+ it("returns empty string for empty input", () => {
15
+ expect(stripJsonComments("")).toBe("");
16
+ });
17
+
18
+ it("passes through plain JSON unchanged", () => {
19
+ const input = '{"key": true}';
20
+ expect(stripJsonComments(input)).toBe(input);
21
+ });
22
+
23
+ it("drops a line comment body and preserves the trailing newline", () => {
24
+ // The space before // is emitted; the comment body is dropped; \n is kept.
25
+ expect(stripJsonComments('{ // comment\n"k": 1}')).toBe('{ \n"k": 1}');
26
+ });
27
+
28
+ it("drops a line comment that runs to EOF with no trailing newline", () => {
29
+ expect(stripJsonComments('{"k": 1} // trailing')).toBe('{"k": 1} ');
30
+ });
31
+
32
+ it("drops a block comment and nothing else", () => {
33
+ expect(stripJsonComments('{ /* block */ "k": 1}')).toBe('{ "k": 1}');
34
+ });
35
+
36
+ it("drops an unterminated block comment to EOF", () => {
37
+ expect(stripJsonComments("{ /* no close")).toBe("{ ");
38
+ });
39
+
40
+ it("preserves // inside a double-quoted string", () => {
41
+ expect(stripJsonComments('{"url": "http://example.com"}')).toBe(
42
+ '{"url": "http://example.com"}',
43
+ );
44
+ });
45
+
46
+ it("preserves block-comment markers inside a double-quoted string", () => {
47
+ expect(stripJsonComments('{"v": "a /* b */ c"}')).toBe(
48
+ '{"v": "a /* b */ c"}',
49
+ );
50
+ });
51
+
52
+ it("preserves // inside a single-quoted string", () => {
53
+ expect(stripJsonComments("{'url': 'http://x.com'}")).toBe(
54
+ "{'url': 'http://x.com'}",
55
+ );
56
+ });
57
+
58
+ it("preserves block-comment markers inside a single-quoted string", () => {
59
+ expect(stripJsonComments("{'v': 'a /* b */ c'}")).toBe(
60
+ "{'v': 'a /* b */ c'}",
61
+ );
62
+ });
63
+
64
+ it("honors a backslash-escaped quote so it does not close the string", () => {
65
+ // The string value is: a\"b (backslash-escaped double quote)
66
+ expect(stripJsonComments('{"k": "a\\"b"}')).toBe('{"k": "a\\"b"}');
67
+ });
68
+
69
+ it("emits an unterminated string to EOF verbatim", () => {
70
+ expect(stripJsonComments('{"k": "unterminated')).toBe(
71
+ '{"k": "unterminated',
72
+ );
73
+ });
74
+
75
+ it("preserves a lone slash that is not part of // or /*", () => {
76
+ expect(stripJsonComments('{"v": 1/2}')).toBe('{"v": 1/2}');
77
+ });
78
+
79
+ it("handles a combined JSONC document that round-trips to valid JSON", () => {
80
+ const jsonc = [
81
+ "{",
82
+ ' "debugLog": true, // runtime knob',
83
+ ' "permission": { /* the policy */ "*": "ask" }',
84
+ "}",
85
+ ].join("\n");
86
+ const stripped = stripJsonComments(jsonc);
87
+ // Must parse without throwing
88
+ const parsed = JSON.parse(stripped) as Record<string, unknown>;
89
+ expect(parsed.debugLog).toBe(true);
90
+ expect(parsed.permission).toEqual({ "*": "ask" });
91
+ });
92
+ });
93
+
12
94
  describe("loadUnifiedConfig", () => {
13
95
  let tempDir: string;
14
96
 
@@ -1,4 +1,3 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
1
  import { describe, expect, it, vi } from "vitest";
3
2
 
4
3
  import {
@@ -8,6 +7,8 @@ import {
8
7
  import type { PermissionSession } from "#src/permission-session";
9
8
  import type { ToolRegistry } from "#src/tool-registry";
10
9
 
10
+ import { makeCtx } from "#test/helpers/handler-fixtures";
11
+
11
12
  // ── SDK stubs ──────────────────────────────────────────────────────────────
12
13
  vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
13
14
  const original =
@@ -20,25 +21,6 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
20
21
 
21
22
  // ── helpers ────────────────────────────────────────────────────────────────
22
23
 
23
- function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
24
- return {
25
- cwd: "/test/project",
26
- hasUI: true,
27
- ui: {
28
- setStatus: vi.fn(),
29
- notify: vi.fn(),
30
- select: vi.fn(),
31
- input: vi.fn(),
32
- },
33
- sessionManager: {
34
- getEntries: vi.fn().mockReturnValue([]),
35
- getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
36
- addEntry: vi.fn(),
37
- },
38
- ...overrides,
39
- } as unknown as ExtensionContext;
40
- }
41
-
42
24
  function makeEvent(systemPrompt = "You are an assistant.") {
43
25
  return { systemPrompt };
44
26
  }
@@ -8,21 +8,23 @@
8
8
  * Regression guard: importing the four external-directory message helpers
9
9
  * ensures the test file fails to load if any helper is removed.
10
10
  */
11
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
12
11
  import { describe, expect, it, vi } from "vitest";
13
12
 
14
13
  import { EXTENSION_TAG } from "#src/denial-messages";
15
14
  import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
16
15
  import { formatExternalDirectoryAskPrompt } from "#src/handlers/gates/external-directory-messages";
17
16
  import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
18
- import {
19
- PERMISSIONS_DECISION_CHANNEL,
20
- type PermissionDecisionEvent,
21
- } from "#src/permission-events";
22
17
  import type { PermissionSession } from "#src/permission-session";
23
18
  import type { ToolRegistry } from "#src/tool-registry";
24
19
  import type { PermissionCheckResult, PermissionState } from "#src/types";
25
20
 
21
+ import {
22
+ getDecisionEvents,
23
+ makeCtx,
24
+ makeEvents,
25
+ makeToolCallEvent,
26
+ } from "#test/helpers/handler-fixtures";
27
+
26
28
  // ── SDK stubs ──────────────────────────────────────────────────────────────
27
29
  vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
28
30
  const original =
@@ -70,39 +72,6 @@ function makeCheckPermission(
70
72
  });
71
73
  }
72
74
 
73
- function makeCtx(
74
- overrides: Partial<ExtensionContext> & { cwd?: string } = {},
75
- ): ExtensionContext {
76
- return {
77
- cwd: CWD,
78
- hasUI: true,
79
- ui: {
80
- setStatus: vi.fn(),
81
- notify: vi.fn(),
82
- select: vi.fn(),
83
- input: vi.fn(),
84
- },
85
- sessionManager: {
86
- getEntries: vi.fn().mockReturnValue([]),
87
- getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
88
- addEntry: vi.fn(),
89
- },
90
- ...overrides,
91
- } as unknown as ExtensionContext;
92
- }
93
-
94
- function makeToolCallEvent(
95
- toolName: string,
96
- input: Record<string, unknown> = {},
97
- ) {
98
- return {
99
- type: "tool_call",
100
- toolCallId: "tc-ext-1",
101
- name: toolName,
102
- input,
103
- };
104
- }
105
-
106
75
  function makeSession(
107
76
  overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
108
77
  ): PermissionSession {
@@ -124,13 +93,6 @@ function makeSession(
124
93
  } as unknown as PermissionSession;
125
94
  }
126
95
 
127
- function makeEvents() {
128
- return {
129
- emit: vi.fn(),
130
- on: vi.fn().mockReturnValue(() => undefined),
131
- };
132
- }
133
-
134
96
  /** All PATH_BEARING_TOOLS members. */
135
97
  const ALL_PATH_BEARING_TOOLS = ["read", "write", "edit", "find", "grep", "ls"];
136
98
 
@@ -164,14 +126,6 @@ function makeHandler(overrides?: {
164
126
  return { handler, events, session };
165
127
  }
166
128
 
167
- function getDecisionEvents(
168
- events: ReturnType<typeof makeEvents>,
169
- ): PermissionDecisionEvent[] {
170
- return events.emit.mock.calls
171
- .filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
172
- .map(([, payload]) => payload as PermissionDecisionEvent);
173
- }
174
-
175
129
  // ── Regression guard: helper presence ──────────────────────────────────────
176
130
 
177
131
  describe("external_directory helper regression guard", () => {
@@ -199,7 +153,7 @@ describe("external_directory path scope", () => {
199
153
  session: { checkPermission: makeCheckPermission("deny") },
200
154
  });
201
155
  const event = makeToolCallEvent("read", {
202
- path: `${CWD}/src/index.ts`,
156
+ input: { path: `${CWD}/src/index.ts` },
203
157
  });
204
158
  const result = await handler.handleToolCall(event, makeCtx());
205
159
  // Should not be blocked — the external_directory gate is skipped,
@@ -211,7 +165,7 @@ describe("external_directory path scope", () => {
211
165
  const { handler } = makeHandler({
212
166
  session: { checkPermission: makeCheckPermission("deny") },
213
167
  });
214
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
168
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
215
169
  const result = await handler.handleToolCall(event, makeCtx());
216
170
  expect(result).toMatchObject({ block: true });
217
171
  });
@@ -221,7 +175,7 @@ describe("external_directory path scope", () => {
221
175
  session: { checkPermission: makeCheckPermission("deny", "allow") },
222
176
  });
223
177
  const event = makeToolCallEvent("bash", {
224
- command: `cat ${EXTERNAL_PATH}`,
178
+ input: { command: `cat ${EXTERNAL_PATH}` },
225
179
  });
226
180
  // bash is not in PATH_BEARING_TOOLS, so the external_directory gate
227
181
  // for tool path does not fire (bash-external-directory gate is separate)
@@ -239,7 +193,9 @@ describe("external_directory path scope", () => {
239
193
  const { handler } = makeHandler({
240
194
  session: { checkPermission: makeCheckPermission("deny") },
241
195
  });
242
- const event = makeToolCallEvent(toolName, { path: EXTERNAL_PATH });
196
+ const event = makeToolCallEvent(toolName, {
197
+ input: { path: EXTERNAL_PATH },
198
+ });
243
199
  const result = await handler.handleToolCall(event, makeCtx());
244
200
  expect(result).toMatchObject({ block: true });
245
201
  });
@@ -251,7 +207,7 @@ describe("external_directory path scope", () => {
251
207
  session: { checkPermission: makeCheckPermission("deny") },
252
208
  });
253
209
  // No path in input — external_directory gate should not fire
254
- const event = makeToolCallEvent(toolName, {});
210
+ const event = makeToolCallEvent(toolName);
255
211
  const result = await handler.handleToolCall(event, makeCtx());
256
212
  expect(result).toEqual({});
257
213
  });
@@ -264,7 +220,7 @@ describe("external_directory policy state — allow", () => {
264
220
  const { handler } = makeHandler({
265
221
  session: { checkPermission: makeCheckPermission("allow") },
266
222
  });
267
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
223
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
268
224
  const result = await handler.handleToolCall(event, makeCtx());
269
225
  expect(result).toEqual({});
270
226
  });
@@ -273,7 +229,7 @@ describe("external_directory policy state — allow", () => {
273
229
  const { handler, events } = makeHandler({
274
230
  session: { checkPermission: makeCheckPermission("allow") },
275
231
  });
276
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
232
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
277
233
  await handler.handleToolCall(event, makeCtx());
278
234
  const decisions = getDecisionEvents(events);
279
235
  const extDirDecision = decisions.find(
@@ -290,7 +246,7 @@ describe("external_directory policy state — allow", () => {
290
246
  const { handler, session } = makeHandler({
291
247
  session: { checkPermission: makeCheckPermission("allow") },
292
248
  });
293
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
249
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
294
250
  await handler.handleToolCall(event, makeCtx());
295
251
  const reviewCalls = (session.logger.review as ReturnType<typeof vi.fn>).mock
296
252
  .calls;
@@ -307,7 +263,7 @@ describe("external_directory — allow external reads, gate external writes (#14
307
263
  const { handler } = makeHandler({
308
264
  session: { checkPermission: makeCheckPermission("allow", "allow") },
309
265
  });
310
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
266
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
311
267
  const result = await handler.handleToolCall(event, makeCtx());
312
268
  expect(result).toEqual({});
313
269
  });
@@ -322,7 +278,9 @@ describe("external_directory — allow external reads, gate external writes (#14
322
278
  prompt,
323
279
  },
324
280
  });
325
- const event = makeToolCallEvent("write", { path: EXTERNAL_PATH });
281
+ const event = makeToolCallEvent("write", {
282
+ input: { path: EXTERNAL_PATH },
283
+ });
326
284
  const result = await handler.handleToolCall(event, makeCtx());
327
285
  // external_directory passes; write gate prompts and user approves
328
286
  expect(result).toEqual({});
@@ -333,7 +291,9 @@ describe("external_directory — allow external reads, gate external writes (#14
333
291
  const { handler } = makeHandler({
334
292
  session: { checkPermission: makeCheckPermission("allow", "deny") },
335
293
  });
336
- const event = makeToolCallEvent("write", { path: EXTERNAL_PATH });
294
+ const event = makeToolCallEvent("write", {
295
+ input: { path: EXTERNAL_PATH },
296
+ });
337
297
  const result = await handler.handleToolCall(event, makeCtx());
338
298
  expect(result.block).toBe(true);
339
299
  });
@@ -342,7 +302,9 @@ describe("external_directory — allow external reads, gate external writes (#14
342
302
  const { handler, events } = makeHandler({
343
303
  session: { checkPermission: makeCheckPermission("allow", "deny") },
344
304
  });
345
- const event = makeToolCallEvent("write", { path: EXTERNAL_PATH });
305
+ const event = makeToolCallEvent("write", {
306
+ input: { path: EXTERNAL_PATH },
307
+ });
346
308
  await handler.handleToolCall(event, makeCtx());
347
309
  const decisions = getDecisionEvents(events);
348
310
  const extDirDecision = decisions.find(
@@ -367,7 +329,7 @@ describe("external_directory policy state — deny", () => {
367
329
  const { handler } = makeHandler({
368
330
  session: { checkPermission: makeCheckPermission("deny") },
369
331
  });
370
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
332
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
371
333
  const result = await handler.handleToolCall(event, makeCtx());
372
334
  expect(result.block).toBe(true);
373
335
  expect(result.reason).toContain(EXTERNAL_PATH);
@@ -377,7 +339,7 @@ describe("external_directory policy state — deny", () => {
377
339
  const { handler } = makeHandler({
378
340
  session: { checkPermission: makeCheckPermission("deny") },
379
341
  });
380
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
342
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
381
343
  const result = await handler.handleToolCall(event, makeCtx());
382
344
  expect(result.reason).toContain("[pi-permission-system]");
383
345
  expect(result.reason).not.toContain("Hard stop");
@@ -387,7 +349,7 @@ describe("external_directory policy state — deny", () => {
387
349
  const { handler, session } = makeHandler({
388
350
  session: { checkPermission: makeCheckPermission("deny") },
389
351
  });
390
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
352
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
391
353
  await handler.handleToolCall(event, makeCtx());
392
354
  const reviewCalls = (session.logger.review as ReturnType<typeof vi.fn>).mock
393
355
  .calls;
@@ -404,7 +366,7 @@ describe("external_directory policy state — deny", () => {
404
366
  const { handler, events } = makeHandler({
405
367
  session: { checkPermission: makeCheckPermission("deny") },
406
368
  });
407
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
369
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
408
370
  await handler.handleToolCall(event, makeCtx());
409
371
  const decisions = getDecisionEvents(events);
410
372
  const extDirDecision = decisions.find(
@@ -430,7 +392,7 @@ describe("external_directory policy state — ask", () => {
430
392
  .mockResolvedValue({ approved: true, state: "approved" }),
431
393
  },
432
394
  });
433
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
395
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
434
396
  const result = await handler.handleToolCall(event, makeCtx());
435
397
  expect(result).toEqual({});
436
398
  });
@@ -444,7 +406,7 @@ describe("external_directory policy state — ask", () => {
444
406
  .mockResolvedValue({ approved: true, state: "approved" }),
445
407
  },
446
408
  });
447
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
409
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
448
410
  await handler.handleToolCall(event, makeCtx());
449
411
  const decisions = getDecisionEvents(events);
450
412
  const extDirDecision = decisions.find(
@@ -464,7 +426,7 @@ describe("external_directory policy state — ask", () => {
464
426
  prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
465
427
  },
466
428
  });
467
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
429
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
468
430
  const result = await handler.handleToolCall(event, makeCtx());
469
431
  expect(result.block).toBe(true);
470
432
  });
@@ -476,7 +438,7 @@ describe("external_directory policy state — ask", () => {
476
438
  prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
477
439
  },
478
440
  });
479
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
441
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
480
442
  await handler.handleToolCall(event, makeCtx());
481
443
  const decisions = getDecisionEvents(events);
482
444
  const extDirDecision = decisions.find(
@@ -500,7 +462,7 @@ describe("external_directory policy state — ask", () => {
500
462
  }),
501
463
  },
502
464
  });
503
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
465
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
504
466
  const result = await handler.handleToolCall(event, makeCtx());
505
467
  expect(result.block).toBe(true);
506
468
  expect(result.reason).toContain("not needed");
@@ -513,7 +475,7 @@ describe("external_directory policy state — ask", () => {
513
475
  canPrompt: vi.fn().mockReturnValue(false),
514
476
  },
515
477
  });
516
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
478
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
517
479
  const result = await handler.handleToolCall(
518
480
  event,
519
481
  makeCtx({ hasUI: false }),
@@ -529,7 +491,7 @@ describe("external_directory policy state — ask", () => {
529
491
  canPrompt: vi.fn().mockReturnValue(false),
530
492
  },
531
493
  });
532
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
494
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
533
495
  await handler.handleToolCall(event, makeCtx({ hasUI: false }));
534
496
  const reviewCalls = (session.logger.review as ReturnType<typeof vi.fn>).mock
535
497
  .calls;
@@ -549,7 +511,7 @@ describe("external_directory policy state — ask", () => {
549
511
  canPrompt: vi.fn().mockReturnValue(false),
550
512
  },
551
513
  });
552
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
514
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
553
515
  await handler.handleToolCall(event, makeCtx({ hasUI: false }));
554
516
  const decisions = getDecisionEvents(events);
555
517
  const extDirDecision = decisions.find(
@@ -602,7 +564,7 @@ describe("external_directory per-agent override", () => {
602
564
  resolveAgentName: vi.fn().mockReturnValue("special-agent"),
603
565
  },
604
566
  });
605
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
567
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
606
568
  const result1 = await handler1.handleToolCall(event, makeCtx());
607
569
  expect(result1).toEqual({});
608
570
 
@@ -633,7 +595,7 @@ describe("external_directory decision event fields", () => {
633
595
  const { handler, events } = makeHandler({
634
596
  session: { checkPermission: makeCheckPermission("deny") },
635
597
  });
636
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
598
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
637
599
  await handler.handleToolCall(event, makeCtx());
638
600
  const decisions = getDecisionEvents(events);
639
601
  const extDirDecision = decisions.find(
@@ -650,7 +612,7 @@ describe("external_directory decision event fields", () => {
650
612
  resolveAgentName: vi.fn().mockReturnValue("my-agent"),
651
613
  },
652
614
  });
653
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
615
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
654
616
  await handler.handleToolCall(event, makeCtx());
655
617
  const decisions = getDecisionEvents(events);
656
618
  const extDirDecision = decisions.find(
@@ -665,7 +627,7 @@ describe("external_directory decision event fields", () => {
665
627
  const { handler, events } = makeHandler({
666
628
  session: { checkPermission: makeCheckPermission("allow") },
667
629
  });
668
- const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
630
+ const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
669
631
  await handler.handleToolCall(event, makeCtx());
670
632
  const decisions = getDecisionEvents(events);
671
633
  const extDirDecision = decisions.find(
@@ -8,7 +8,6 @@
8
8
  * the real interaction between PermissionSession, SessionRules, and
9
9
  * PermissionManager.
10
10
  */
11
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
12
11
  import { describe, expect, it, vi } from "vitest";
13
12
 
14
13
  import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
@@ -20,6 +19,8 @@ import type { ToolRegistry } from "#src/tool-registry";
20
19
  import type { PermissionCheckResult } from "#src/types";
21
20
  import { wildcardMatch } from "#src/wildcard-matcher";
22
21
 
22
+ import { makeCtx, makeEvents } from "#test/helpers/handler-fixtures";
23
+
23
24
  // ── SDK stub ───────────────────────────────────────────────────────────────
24
25
  vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
25
26
  const original =
@@ -29,27 +30,6 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
29
30
 
30
31
  // ── helpers ────────────────────────────────────────────────────────────────
31
32
 
32
- const CWD = "/test/project";
33
-
34
- function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
35
- return {
36
- cwd: CWD,
37
- hasUI: true,
38
- ui: {
39
- setStatus: vi.fn(),
40
- notify: vi.fn(),
41
- select: vi.fn(),
42
- input: vi.fn(),
43
- },
44
- sessionManager: {
45
- getEntries: vi.fn().mockReturnValue([]),
46
- getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
47
- addEntry: vi.fn(),
48
- },
49
- ...overrides,
50
- } as unknown as ExtensionContext;
51
- }
52
-
53
33
  /**
54
34
  * Build a PermissionSession mock with stateful session-rule tracking.
55
35
  *
@@ -152,13 +132,6 @@ function makeStatefulSession(
152
132
  } as unknown as PermissionSession;
153
133
  }
154
134
 
155
- function makeEvents() {
156
- return {
157
- emit: vi.fn(),
158
- on: vi.fn().mockReturnValue(() => undefined),
159
- };
160
- }
161
-
162
135
  function makeToolRegistry(): ToolRegistry {
163
136
  return {
164
137
  getAll: vi
@@ -15,39 +15,18 @@ import type {
15
15
  GateDescriptor,
16
16
  } from "#src/handlers/gates/descriptor";
17
17
  import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
18
- import type { ToolCallContext } from "#src/handlers/gates/types";
19
18
  import type { Rule } from "#src/rule";
20
19
  import type { PermissionCheckResult } from "#src/types";
21
20
 
21
+ import {
22
+ makeGateCheckResult as makeCheckResult,
23
+ makeTcc,
24
+ } from "#test/helpers/gate-fixtures";
25
+
22
26
  afterEach(() => {
23
27
  vi.restoreAllMocks();
24
28
  });
25
29
 
26
- // ── helpers ────────────────────────────────────────────────────────────────
27
-
28
- function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
29
- return {
30
- toolName: "bash",
31
- agentName: null,
32
- input: { command: "cat .env" },
33
- toolCallId: "tc-1",
34
- cwd: "/test/project",
35
- ...overrides,
36
- };
37
- }
38
-
39
- function makeCheckResult(
40
- overrides: Partial<PermissionCheckResult> = {},
41
- ): PermissionCheckResult {
42
- return {
43
- toolName: "path",
44
- state: "allow",
45
- source: "special",
46
- origin: "global",
47
- ...overrides,
48
- };
49
- }
50
-
51
30
  type CheckPermissionFn = (
52
31
  surface: string,
53
32
  input: unknown,