@gotgenes/pi-permission-system 10.0.0 → 10.1.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 (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/agent-prep-session.ts +28 -0
  5. package/src/decision-reporter.ts +41 -0
  6. package/src/denial-messages.ts +11 -0
  7. package/src/forwarded-permissions/permission-forwarder.ts +549 -0
  8. package/src/forwarding-manager.ts +3 -7
  9. package/src/gate-handler-session.ts +13 -0
  10. package/src/gate-prompter.ts +14 -0
  11. package/src/handlers/before-agent-start.ts +2 -3
  12. package/src/handlers/gates/bash-command.ts +4 -18
  13. package/src/handlers/gates/bash-external-directory.ts +3 -15
  14. package/src/handlers/gates/bash-path.ts +3 -16
  15. package/src/handlers/gates/descriptor.ts +0 -28
  16. package/src/handlers/gates/path.ts +3 -15
  17. package/src/handlers/gates/runner.ts +142 -105
  18. package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
  19. package/src/handlers/gates/skill-input.ts +44 -0
  20. package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
  21. package/src/handlers/lifecycle.ts +9 -9
  22. package/src/handlers/permission-gate-handler.ts +34 -238
  23. package/src/index.ts +49 -69
  24. package/src/mcp-targets.ts +56 -46
  25. package/src/permission-prompter.ts +7 -58
  26. package/src/permission-resolver.ts +17 -0
  27. package/src/permission-session.ts +77 -9
  28. package/src/permissions-service.ts +53 -0
  29. package/src/service-lifecycle.ts +49 -0
  30. package/src/session-approval-recorder.ts +6 -0
  31. package/src/session-lifecycle-session.ts +24 -0
  32. package/src/tool-input-preview.ts +0 -62
  33. package/src/tool-input-prompt-formatters.ts +63 -0
  34. package/src/tool-preview-formatter.ts +6 -4
  35. package/test/decision-reporter.test.ts +112 -0
  36. package/test/denial-messages.test.ts +62 -0
  37. package/test/forwarding-manager.test.ts +26 -44
  38. package/test/handlers/before-agent-start.test.ts +45 -21
  39. package/test/handlers/external-directory-integration.test.ts +86 -22
  40. package/test/handlers/external-directory-session-dedup.test.ts +102 -55
  41. package/test/handlers/gates/bash-command.test.ts +49 -90
  42. package/test/handlers/gates/bash-external-directory.test.ts +54 -95
  43. package/test/handlers/gates/bash-path.test.ts +63 -148
  44. package/test/handlers/gates/path.test.ts +38 -105
  45. package/test/handlers/gates/runner.test.ts +150 -93
  46. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
  47. package/test/handlers/gates/skill-input.test.ts +128 -0
  48. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
  49. package/test/handlers/input.test.ts +1 -2
  50. package/test/handlers/lifecycle.test.ts +49 -33
  51. package/test/handlers/tool-call-events.test.ts +1 -1
  52. package/test/helpers/gate-fixtures.ts +147 -16
  53. package/test/helpers/handler-fixtures.ts +143 -27
  54. package/test/mcp-targets.test.ts +55 -0
  55. package/test/permission-forwarder.test.ts +295 -0
  56. package/test/permission-forwarding.test.ts +0 -282
  57. package/test/permission-prompter.test.ts +33 -44
  58. package/test/permission-session.test.ts +160 -27
  59. package/test/permissions-service.test.ts +151 -0
  60. package/test/runtime.test.ts +0 -4
  61. package/test/service-lifecycle.test.ts +162 -0
  62. package/test/tool-input-preview.test.ts +0 -111
  63. package/test/tool-input-prompt-formatters.test.ts +115 -0
  64. package/src/forwarded-permissions/polling.ts +0 -411
@@ -0,0 +1,295 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import { afterEach, describe, expect, test, vi } from "vitest";
6
+
7
+ import {
8
+ PermissionForwarder,
9
+ type PermissionForwarderDeps,
10
+ } from "#src/forwarded-permissions/permission-forwarder";
11
+ import { createPermissionForwardingLocation } from "#src/permission-forwarding";
12
+
13
+ // ── Helpers ───────────────────────────────────────────────────────────────
14
+
15
+ function makeDeps(
16
+ overrides: Partial<PermissionForwarderDeps> = {},
17
+ ): PermissionForwarderDeps {
18
+ return {
19
+ forwardingDir: "/tmp/forwarding",
20
+ subagentSessionsDir: "/tmp/subagents",
21
+ logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
22
+ writeReviewLog: vi.fn(),
23
+ requestPermissionDecisionFromUi: vi
24
+ .fn()
25
+ .mockResolvedValue({ approved: true, state: "approved" as const }),
26
+ shouldAutoApprove: () => false,
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ afterEach(() => {
32
+ vi.unstubAllEnvs();
33
+ });
34
+
35
+ // ── requestApproval ───────────────────────────────────────────────────────
36
+
37
+ describe("requestApproval — UI fast path", () => {
38
+ test("calls requestPermissionDecisionFromUi but does not emit a UI prompt event (the prompter does)", async () => {
39
+ const events = {
40
+ emit: vi.fn(),
41
+ on: vi.fn().mockReturnValue(() => undefined),
42
+ };
43
+ const requestPermissionDecisionFromUi = vi
44
+ .fn()
45
+ .mockResolvedValue({ approved: true, state: "approved" as const });
46
+
47
+ const forwarder = new PermissionForwarder(
48
+ makeDeps({ events, requestPermissionDecisionFromUi }),
49
+ );
50
+
51
+ await forwarder.requestApproval(
52
+ {
53
+ hasUI: true,
54
+ ui: { select: vi.fn(), input: vi.fn() },
55
+ } as unknown as ExtensionContext,
56
+ "Allow git push?",
57
+ );
58
+
59
+ expect(requestPermissionDecisionFromUi).toHaveBeenCalled();
60
+ expect(events.emit).not.toHaveBeenCalledWith(
61
+ "permissions:ui_prompt",
62
+ expect.anything(),
63
+ );
64
+ });
65
+ });
66
+
67
+ describe("requestApproval — non-UI, non-subagent path", () => {
68
+ test("returns denied without showing a dialog or emitting when there is no active UI", async () => {
69
+ const events = {
70
+ emit: vi.fn(),
71
+ on: vi.fn().mockReturnValue(() => undefined),
72
+ };
73
+ const requestPermissionDecisionFromUi = vi.fn();
74
+
75
+ const forwarder = new PermissionForwarder(
76
+ makeDeps({ events, requestPermissionDecisionFromUi }),
77
+ );
78
+
79
+ const result = await forwarder.requestApproval(
80
+ {
81
+ hasUI: false,
82
+ sessionManager: {
83
+ getSessionDir: vi.fn().mockReturnValue(null),
84
+ },
85
+ } as unknown as ExtensionContext,
86
+ "Allow git push?",
87
+ );
88
+
89
+ expect(result).toEqual({ approved: false, state: "denied" });
90
+ expect(events.emit).not.toHaveBeenCalledWith(
91
+ "permissions:ui_prompt",
92
+ expect.anything(),
93
+ );
94
+ expect(requestPermissionDecisionFromUi).not.toHaveBeenCalled();
95
+ });
96
+ });
97
+
98
+ // ── processInbox ──────────────────────────────────────────────────────────
99
+
100
+ describe("processInbox", () => {
101
+ test("emits a UI prompt event before showing a forwarded permission dialog", async () => {
102
+ const root = mkdtempSync(join(tmpdir(), "permission-forwarding-"));
103
+ try {
104
+ const forwardingDir = join(root, "forwarding");
105
+ const location = createPermissionForwardingLocation(
106
+ forwardingDir,
107
+ "parent-session",
108
+ );
109
+ mkdirSync(location.requestsDir, { recursive: true });
110
+ mkdirSync(location.responsesDir, { recursive: true });
111
+ writeFileSync(
112
+ join(location.requestsDir, "req-forwarded.json"),
113
+ JSON.stringify({
114
+ id: "req-forwarded",
115
+ createdAt: Date.now(),
116
+ requesterSessionId: "child-session",
117
+ targetSessionId: "parent-session",
118
+ requesterAgentName: "Explore",
119
+ message: "Allow git push?",
120
+ }),
121
+ "utf-8",
122
+ );
123
+
124
+ const events = {
125
+ emit: vi.fn(),
126
+ on: vi.fn().mockReturnValue(() => undefined),
127
+ };
128
+ const requestPermissionDecisionFromUi = vi
129
+ .fn()
130
+ .mockResolvedValue({ approved: true, state: "approved" as const });
131
+
132
+ const forwarder = new PermissionForwarder(
133
+ makeDeps({
134
+ forwardingDir,
135
+ events,
136
+ requestPermissionDecisionFromUi,
137
+ }),
138
+ );
139
+
140
+ await forwarder.processInbox({
141
+ hasUI: true,
142
+ ui: { select: vi.fn(), input: vi.fn() },
143
+ sessionManager: {
144
+ getSessionId: vi.fn().mockReturnValue("parent-session"),
145
+ },
146
+ } as unknown as ExtensionContext);
147
+
148
+ expect(events.emit).toHaveBeenCalledWith(
149
+ "permissions:ui_prompt",
150
+ expect.objectContaining({
151
+ requestId: "req-forwarded",
152
+ source: "tool_call",
153
+ surface: null,
154
+ value: null,
155
+ agentName: "Explore",
156
+ message: expect.stringContaining("Allow git push?"),
157
+ forwarding: {
158
+ requesterAgentName: "Explore",
159
+ requesterSessionId: "child-session",
160
+ },
161
+ }),
162
+ );
163
+ expect(requestPermissionDecisionFromUi).toHaveBeenCalled();
164
+ } finally {
165
+ rmSync(root, { recursive: true, force: true });
166
+ }
167
+ });
168
+
169
+ test("emits a non-degraded UI prompt event when the request carries display fields", async () => {
170
+ const root = mkdtempSync(join(tmpdir(), "permission-forwarding-"));
171
+ try {
172
+ const forwardingDir = join(root, "forwarding");
173
+ const location = createPermissionForwardingLocation(
174
+ forwardingDir,
175
+ "parent-session",
176
+ );
177
+ mkdirSync(location.requestsDir, { recursive: true });
178
+ mkdirSync(location.responsesDir, { recursive: true });
179
+ writeFileSync(
180
+ join(location.requestsDir, "req-forwarded-rich.json"),
181
+ JSON.stringify({
182
+ id: "req-forwarded-rich",
183
+ createdAt: Date.now(),
184
+ requesterSessionId: "child-session",
185
+ targetSessionId: "parent-session",
186
+ requesterAgentName: "Explore",
187
+ message: "Allow git push?",
188
+ source: "tool_call",
189
+ surface: "bash",
190
+ value: "git push",
191
+ }),
192
+ "utf-8",
193
+ );
194
+
195
+ const events = {
196
+ emit: vi.fn(),
197
+ on: vi.fn().mockReturnValue(() => undefined),
198
+ };
199
+ const requestPermissionDecisionFromUi = vi
200
+ .fn()
201
+ .mockResolvedValue({ approved: true, state: "approved" as const });
202
+
203
+ const forwarder = new PermissionForwarder(
204
+ makeDeps({
205
+ forwardingDir,
206
+ events,
207
+ requestPermissionDecisionFromUi,
208
+ }),
209
+ );
210
+
211
+ await forwarder.processInbox({
212
+ hasUI: true,
213
+ ui: { select: vi.fn(), input: vi.fn() },
214
+ sessionManager: {
215
+ getSessionId: vi.fn().mockReturnValue("parent-session"),
216
+ },
217
+ } as unknown as ExtensionContext);
218
+
219
+ expect(events.emit).toHaveBeenCalledWith(
220
+ "permissions:ui_prompt",
221
+ expect.objectContaining({
222
+ requestId: "req-forwarded-rich",
223
+ source: "tool_call",
224
+ surface: "bash",
225
+ value: "git push",
226
+ agentName: "Explore",
227
+ message: expect.stringContaining("Allow git push?"),
228
+ forwarding: {
229
+ requesterAgentName: "Explore",
230
+ requesterSessionId: "child-session",
231
+ },
232
+ }),
233
+ );
234
+ expect(requestPermissionDecisionFromUi).toHaveBeenCalled();
235
+ } finally {
236
+ rmSync(root, { recursive: true, force: true });
237
+ }
238
+ });
239
+
240
+ test("does not emit a UI prompt event when forwarded permission auto-approves", async () => {
241
+ const root = mkdtempSync(join(tmpdir(), "permission-forwarding-"));
242
+ try {
243
+ const forwardingDir = join(root, "forwarding");
244
+ const location = createPermissionForwardingLocation(
245
+ forwardingDir,
246
+ "parent-session",
247
+ );
248
+ mkdirSync(location.requestsDir, { recursive: true });
249
+ mkdirSync(location.responsesDir, { recursive: true });
250
+ writeFileSync(
251
+ join(location.requestsDir, "req-forwarded-auto.json"),
252
+ JSON.stringify({
253
+ id: "req-forwarded-auto",
254
+ createdAt: Date.now(),
255
+ requesterSessionId: "child-session",
256
+ targetSessionId: "parent-session",
257
+ requesterAgentName: "Explore",
258
+ message: "Allow git push?",
259
+ }),
260
+ "utf-8",
261
+ );
262
+
263
+ const events = {
264
+ emit: vi.fn(),
265
+ on: vi.fn().mockReturnValue(() => undefined),
266
+ };
267
+ const requestPermissionDecisionFromUi = vi.fn();
268
+
269
+ const forwarder = new PermissionForwarder(
270
+ makeDeps({
271
+ forwardingDir,
272
+ events,
273
+ requestPermissionDecisionFromUi,
274
+ shouldAutoApprove: () => true,
275
+ }),
276
+ );
277
+
278
+ await forwarder.processInbox({
279
+ hasUI: true,
280
+ ui: { select: vi.fn(), input: vi.fn() },
281
+ sessionManager: {
282
+ getSessionId: vi.fn().mockReturnValue("parent-session"),
283
+ },
284
+ } as unknown as ExtensionContext);
285
+
286
+ expect(events.emit).not.toHaveBeenCalledWith(
287
+ "permissions:ui_prompt",
288
+ expect.anything(),
289
+ );
290
+ expect(requestPermissionDecisionFromUi).not.toHaveBeenCalled();
291
+ } finally {
292
+ rmSync(root, { recursive: true, force: true });
293
+ }
294
+ });
295
+ });
@@ -1,14 +1,5 @@
1
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
1
  import { afterEach, describe, expect, test, vi } from "vitest";
6
2
  import {
7
- confirmPermission,
8
- processForwardedPermissionRequests,
9
- } from "#src/forwarded-permissions/polling";
10
- import {
11
- createPermissionForwardingLocation,
12
3
  resolvePermissionForwardingTargetSessionId,
13
4
  SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
14
5
  SUBAGENT_PARENT_SESSION_ENV_KEY,
@@ -249,276 +240,3 @@ describe("resolvePermissionForwardingTargetSessionId — registry resolution", (
249
240
  ).toBe("parent-from-env");
250
241
  });
251
242
  });
252
-
253
- describe("processForwardedPermissionRequests", () => {
254
- test("emits a UI prompt event before showing a forwarded permission dialog", async () => {
255
- const root = mkdtempSync(join(tmpdir(), "permission-forwarding-"));
256
- try {
257
- const forwardingDir = join(root, "forwarding");
258
- const location = createPermissionForwardingLocation(
259
- forwardingDir,
260
- "parent-session",
261
- );
262
- mkdirSync(location.requestsDir, { recursive: true });
263
- mkdirSync(location.responsesDir, { recursive: true });
264
- writeFileSync(
265
- join(location.requestsDir, "req-forwarded.json"),
266
- JSON.stringify({
267
- id: "req-forwarded",
268
- createdAt: Date.now(),
269
- requesterSessionId: "child-session",
270
- targetSessionId: "parent-session",
271
- requesterAgentName: "Explore",
272
- message: "Allow git push?",
273
- }),
274
- "utf-8",
275
- );
276
-
277
- const events = {
278
- emit: vi.fn(),
279
- on: vi.fn().mockReturnValue(() => undefined),
280
- };
281
- const requestPermissionDecisionFromUi = vi
282
- .fn()
283
- .mockResolvedValue({ approved: true, state: "approved" as const });
284
-
285
- await processForwardedPermissionRequests(
286
- {
287
- hasUI: true,
288
- ui: { select: vi.fn(), input: vi.fn() },
289
- sessionManager: {
290
- getSessionId: vi.fn().mockReturnValue("parent-session"),
291
- },
292
- } as unknown as ExtensionContext,
293
- {
294
- forwardingDir,
295
- subagentSessionsDir: join(root, "subagents"),
296
- events,
297
- logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
298
- writeReviewLog: vi.fn(),
299
- requestPermissionDecisionFromUi,
300
- shouldAutoApprove: () => false,
301
- },
302
- );
303
-
304
- expect(events.emit).toHaveBeenCalledWith(
305
- "permissions:ui_prompt",
306
- expect.objectContaining({
307
- requestId: "req-forwarded",
308
- source: "tool_call",
309
- surface: null,
310
- value: null,
311
- agentName: "Explore",
312
- message: expect.stringContaining("Allow git push?"),
313
- forwarding: {
314
- requesterAgentName: "Explore",
315
- requesterSessionId: "child-session",
316
- },
317
- }),
318
- );
319
- expect(requestPermissionDecisionFromUi).toHaveBeenCalled();
320
- } finally {
321
- rmSync(root, { recursive: true, force: true });
322
- }
323
- });
324
-
325
- test("emits a non-degraded UI prompt event when the request carries display fields", async () => {
326
- const root = mkdtempSync(join(tmpdir(), "permission-forwarding-"));
327
- try {
328
- const forwardingDir = join(root, "forwarding");
329
- const location = createPermissionForwardingLocation(
330
- forwardingDir,
331
- "parent-session",
332
- );
333
- mkdirSync(location.requestsDir, { recursive: true });
334
- mkdirSync(location.responsesDir, { recursive: true });
335
- writeFileSync(
336
- join(location.requestsDir, "req-forwarded-rich.json"),
337
- JSON.stringify({
338
- id: "req-forwarded-rich",
339
- createdAt: Date.now(),
340
- requesterSessionId: "child-session",
341
- targetSessionId: "parent-session",
342
- requesterAgentName: "Explore",
343
- message: "Allow git push?",
344
- source: "tool_call",
345
- surface: "bash",
346
- value: "git push",
347
- }),
348
- "utf-8",
349
- );
350
-
351
- const events = {
352
- emit: vi.fn(),
353
- on: vi.fn().mockReturnValue(() => undefined),
354
- };
355
- const requestPermissionDecisionFromUi = vi
356
- .fn()
357
- .mockResolvedValue({ approved: true, state: "approved" as const });
358
-
359
- await processForwardedPermissionRequests(
360
- {
361
- hasUI: true,
362
- ui: { select: vi.fn(), input: vi.fn() },
363
- sessionManager: {
364
- getSessionId: vi.fn().mockReturnValue("parent-session"),
365
- },
366
- } as unknown as ExtensionContext,
367
- {
368
- forwardingDir,
369
- subagentSessionsDir: join(root, "subagents"),
370
- events,
371
- logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
372
- writeReviewLog: vi.fn(),
373
- requestPermissionDecisionFromUi,
374
- shouldAutoApprove: () => false,
375
- },
376
- );
377
-
378
- expect(events.emit).toHaveBeenCalledWith(
379
- "permissions:ui_prompt",
380
- expect.objectContaining({
381
- requestId: "req-forwarded-rich",
382
- source: "tool_call",
383
- surface: "bash",
384
- value: "git push",
385
- agentName: "Explore",
386
- message: expect.stringContaining("Allow git push?"),
387
- forwarding: {
388
- requesterAgentName: "Explore",
389
- requesterSessionId: "child-session",
390
- },
391
- }),
392
- );
393
- expect(requestPermissionDecisionFromUi).toHaveBeenCalled();
394
- } finally {
395
- rmSync(root, { recursive: true, force: true });
396
- }
397
- });
398
-
399
- test("does not emit a UI prompt event when forwarded permission auto-approves", async () => {
400
- const root = mkdtempSync(join(tmpdir(), "permission-forwarding-"));
401
- try {
402
- const forwardingDir = join(root, "forwarding");
403
- const location = createPermissionForwardingLocation(
404
- forwardingDir,
405
- "parent-session",
406
- );
407
- mkdirSync(location.requestsDir, { recursive: true });
408
- mkdirSync(location.responsesDir, { recursive: true });
409
- writeFileSync(
410
- join(location.requestsDir, "req-forwarded-auto.json"),
411
- JSON.stringify({
412
- id: "req-forwarded-auto",
413
- createdAt: Date.now(),
414
- requesterSessionId: "child-session",
415
- targetSessionId: "parent-session",
416
- requesterAgentName: "Explore",
417
- message: "Allow git push?",
418
- }),
419
- "utf-8",
420
- );
421
-
422
- const events = {
423
- emit: vi.fn(),
424
- on: vi.fn().mockReturnValue(() => undefined),
425
- };
426
- const requestPermissionDecisionFromUi = vi.fn();
427
-
428
- await processForwardedPermissionRequests(
429
- {
430
- hasUI: true,
431
- ui: { select: vi.fn(), input: vi.fn() },
432
- sessionManager: {
433
- getSessionId: vi.fn().mockReturnValue("parent-session"),
434
- },
435
- } as unknown as ExtensionContext,
436
- {
437
- forwardingDir,
438
- subagentSessionsDir: join(root, "subagents"),
439
- events,
440
- logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
441
- writeReviewLog: vi.fn(),
442
- requestPermissionDecisionFromUi,
443
- shouldAutoApprove: () => true,
444
- },
445
- );
446
-
447
- expect(events.emit).not.toHaveBeenCalledWith(
448
- "permissions:ui_prompt",
449
- expect.anything(),
450
- );
451
- expect(requestPermissionDecisionFromUi).not.toHaveBeenCalled();
452
- } finally {
453
- rmSync(root, { recursive: true, force: true });
454
- }
455
- });
456
- });
457
-
458
- describe("confirmPermission", () => {
459
- test("shows the UI dialog but does not emit a UI prompt event (the prompter does)", async () => {
460
- const events = {
461
- emit: vi.fn(),
462
- on: vi.fn().mockReturnValue(() => undefined),
463
- };
464
- const requestPermissionDecisionFromUi = vi
465
- .fn()
466
- .mockResolvedValue({ approved: true, state: "approved" as const });
467
-
468
- await confirmPermission(
469
- {
470
- hasUI: true,
471
- ui: { select: vi.fn(), input: vi.fn() },
472
- } as unknown as ExtensionContext,
473
- "Allow git push?",
474
- {
475
- forwardingDir: "/tmp/forwarding",
476
- subagentSessionsDir: "/tmp/subagents",
477
- events,
478
- logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
479
- writeReviewLog: vi.fn(),
480
- requestPermissionDecisionFromUi,
481
- shouldAutoApprove: () => false,
482
- },
483
- );
484
-
485
- expect(requestPermissionDecisionFromUi).toHaveBeenCalled();
486
- expect(events.emit).not.toHaveBeenCalledWith(
487
- "permissions:ui_prompt",
488
- expect.anything(),
489
- );
490
- });
491
-
492
- test("does not show a dialog or emit when there is no active UI", async () => {
493
- const events = {
494
- emit: vi.fn(),
495
- on: vi.fn().mockReturnValue(() => undefined),
496
- };
497
- const requestPermissionDecisionFromUi = vi.fn();
498
-
499
- await confirmPermission(
500
- {
501
- hasUI: false,
502
- sessionManager: {
503
- getSessionDir: vi.fn().mockReturnValue(null),
504
- },
505
- } as unknown as ExtensionContext,
506
- "Allow git push?",
507
- {
508
- forwardingDir: "/tmp/forwarding",
509
- subagentSessionsDir: "/tmp/subagents",
510
- events,
511
- logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
512
- writeReviewLog: vi.fn(),
513
- requestPermissionDecisionFromUi,
514
- shouldAutoApprove: () => false,
515
- },
516
- );
517
-
518
- expect(events.emit).not.toHaveBeenCalledWith(
519
- "permissions:ui_prompt",
520
- expect.anything(),
521
- );
522
- expect(requestPermissionDecisionFromUi).not.toHaveBeenCalled();
523
- });
524
- });