@gotgenes/pi-permission-system 8.1.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +1 -1
  3. package/src/config-loader.ts +53 -46
  4. package/src/handlers/gates/bash-external-directory.ts +2 -4
  5. package/src/handlers/gates/bash-path-extractor.ts +135 -169
  6. package/src/handlers/gates/bash-path.ts +2 -4
  7. package/src/handlers/gates/bash-token-classification.ts +105 -0
  8. package/src/handlers/gates/descriptor.ts +6 -6
  9. package/src/handlers/gates/external-directory.ts +2 -4
  10. package/src/handlers/gates/helpers.ts +30 -1
  11. package/src/handlers/gates/path.ts +2 -4
  12. package/src/handlers/gates/runner.ts +29 -56
  13. package/src/handlers/gates/tool.ts +5 -4
  14. package/src/handlers/permission-gate-handler.ts +4 -3
  15. package/src/permission-manager.ts +6 -49
  16. package/src/permission-session.ts +3 -2
  17. package/src/scope-merge.ts +72 -0
  18. package/src/session-approval.ts +43 -0
  19. package/src/session-rules.ts +13 -0
  20. package/test/config-loader.test.ts +82 -0
  21. package/test/handlers/before-agent-start.test.ts +2 -20
  22. package/test/handlers/external-directory-integration.test.ts +44 -82
  23. package/test/handlers/external-directory-session-dedup.test.ts +17 -41
  24. package/test/handlers/gates/bash-external-directory.test.ts +11 -9
  25. package/test/handlers/gates/bash-path.test.ts +5 -26
  26. package/test/handlers/gates/bash-token-classification.test.ts +241 -0
  27. package/test/handlers/gates/external-directory.test.ts +2 -5
  28. package/test/handlers/gates/helpers.test.ts +81 -0
  29. package/test/handlers/gates/path.test.ts +5 -14
  30. package/test/handlers/gates/runner.test.ts +95 -113
  31. package/test/handlers/gates/tool.test.ts +2 -2
  32. package/test/handlers/input-events.test.ts +42 -95
  33. package/test/handlers/input.test.ts +3 -71
  34. package/test/handlers/lifecycle.test.ts +3 -20
  35. package/test/handlers/tool-call-events.test.ts +30 -127
  36. package/test/handlers/tool-call.test.ts +21 -110
  37. package/test/helpers/gate-fixtures.ts +105 -0
  38. package/test/helpers/handler-fixtures.ts +141 -0
  39. package/test/helpers/manager-harness.ts +51 -0
  40. package/test/permission-session.test.ts +7 -22
  41. package/test/permission-system.test.ts +4 -40
  42. package/test/scope-merge.test.ts +116 -0
  43. package/test/session-approval.test.ts +75 -0
  44. package/test/session-rules.test.ts +49 -0
@@ -2,125 +2,15 @@
2
2
  * Tests that handleToolCall emits permissions:decision events at every
3
3
  * gate resolution and fast-path site.
4
4
  */
5
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
5
  import { describe, expect, it, vi } from "vitest";
7
- import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
8
- import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
9
- import type { PermissionDecisionEvent } from "#src/permission-events";
10
- import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
11
- import type { PermissionSession } from "#src/permission-session";
12
- import type { ToolRegistry } from "#src/tool-registry";
13
- import type { PermissionCheckResult } from "#src/types";
14
-
15
- // ── helpers ────────────────────────────────────────────────────────────────
16
-
17
- function makeEvents() {
18
- return {
19
- emit: vi.fn(),
20
- on: vi.fn().mockReturnValue(() => undefined),
21
- };
22
- }
23
-
24
- function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
25
- return {
26
- cwd: "/test/project",
27
- hasUI: true,
28
- ui: {
29
- setStatus: vi.fn(),
30
- notify: vi.fn(),
31
- select: vi.fn(),
32
- input: vi.fn(),
33
- },
34
- sessionManager: {
35
- getEntries: vi.fn().mockReturnValue([]),
36
- getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
37
- addEntry: vi.fn(),
38
- },
39
- ...overrides,
40
- } as unknown as ExtensionContext;
41
- }
42
-
43
- function makeToolCallEvent(
44
- toolName: string,
45
- extraFields: Record<string, unknown> = {},
46
- ) {
47
- return {
48
- type: "tool_call",
49
- toolCallId: "tc-1",
50
- name: toolName,
51
- input: {},
52
- ...extraFields,
53
- };
54
- }
55
-
56
- function makeCheckResult(
57
- state: "allow" | "deny" | "ask",
58
- overrides: Partial<PermissionCheckResult> = {},
59
- ): PermissionCheckResult {
60
- return {
61
- state,
62
- toolName: "read",
63
- source: "tool",
64
- origin: "builtin",
65
- matchedPattern: "*",
66
- ...overrides,
67
- };
68
- }
69
-
70
- function makeSession(
71
- overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
72
- ): PermissionSession {
73
- return {
74
- logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
75
- activate: vi.fn(),
76
- resolveAgentName: vi.fn().mockReturnValue(null),
77
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
78
- getToolPermission: vi.fn().mockReturnValue("allow"),
79
- getSessionRuleset: vi.fn().mockReturnValue([]),
80
- approveSessionRule: vi.fn(),
81
- getActiveSkillEntries: vi.fn().mockReturnValue([]),
82
- getInfrastructureDirs: vi
83
- .fn()
84
- .mockReturnValue(["/test/agent", "/test/agent/git"]),
85
- getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
86
- config: DEFAULT_EXTENSION_CONFIG,
87
- canPrompt: vi.fn().mockReturnValue(true),
88
- prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
89
- ...overrides,
90
- } as unknown as PermissionSession;
91
- }
92
-
93
- function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
94
- return {
95
- getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
96
- setActive: vi.fn(),
97
- ...overrides,
98
- };
99
- }
100
-
101
- function makeHandler(overrides?: {
102
- session?: Partial<Record<keyof PermissionSession, unknown>>;
103
- toolRegistry?: Partial<ToolRegistry>;
104
- }): {
105
- handler: PermissionGateHandler;
106
- events: ReturnType<typeof makeEvents>;
107
- session: PermissionSession;
108
- } {
109
- const session = makeSession(overrides?.session);
110
- const events = makeEvents();
111
- const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
112
- const handler = new PermissionGateHandler(session, events, toolRegistry);
113
- return { handler, events, session };
114
- }
115
-
116
- /** Extract all permissions:decision payloads from the events.emit mock. */
117
- function getDecisionEvents(
118
- events: ReturnType<typeof makeEvents>,
119
- ): PermissionDecisionEvent[] {
120
- return events.emit.mock.calls
121
- .filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
122
- .map(([, payload]) => payload as PermissionDecisionEvent);
123
- }
6
+
7
+ import {
8
+ getDecisionEvents,
9
+ makeCheckResult,
10
+ makeCtx,
11
+ makeHandler,
12
+ makeToolCallEvent,
13
+ } from "#test/helpers/handler-fixtures";
124
14
 
125
15
  // ── policy_allow path ──────────────────────────────────────────────────────
126
16
 
@@ -129,7 +19,8 @@ describe("handleToolCall decision events — policy_allow", () => {
129
19
  const { handler, events } = makeHandler({
130
20
  session: {
131
21
  checkPermission: vi.fn().mockReturnValue(
132
- makeCheckResult("allow", {
22
+ makeCheckResult({
23
+ state: "allow",
133
24
  origin: "global",
134
25
  matchedPattern: "*",
135
26
  }),
@@ -158,7 +49,8 @@ describe("handleToolCall decision events — policy_deny", () => {
158
49
  const { handler, events } = makeHandler({
159
50
  session: {
160
51
  checkPermission: vi.fn().mockReturnValue(
161
- makeCheckResult("deny", {
52
+ makeCheckResult({
53
+ state: "deny",
162
54
  origin: "project",
163
55
  matchedPattern: "read",
164
56
  }),
@@ -185,7 +77,8 @@ describe("handleToolCall decision events — session_approved", () => {
185
77
  const { handler, events } = makeHandler({
186
78
  session: {
187
79
  checkPermission: vi.fn().mockReturnValue(
188
- makeCheckResult("allow", {
80
+ makeCheckResult({
81
+ state: "allow",
189
82
  source: "session",
190
83
  matchedPattern: "git *",
191
84
  }),
@@ -214,7 +107,9 @@ describe("handleToolCall decision events — user_approved", () => {
214
107
  it("emits allow with user_approved when state=ask and user approves once", async () => {
215
108
  const { handler, events } = makeHandler({
216
109
  session: {
217
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
110
+ checkPermission: vi
111
+ .fn()
112
+ .mockReturnValue(makeCheckResult({ state: "ask" })),
218
113
  prompt: vi
219
114
  .fn()
220
115
  .mockResolvedValue({ approved: true, state: "approved" }),
@@ -234,7 +129,9 @@ describe("handleToolCall decision events — user_approved", () => {
234
129
  it("emits allow with user_approved_for_session when user approves for session", async () => {
235
130
  const { handler, events } = makeHandler({
236
131
  session: {
237
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
132
+ checkPermission: vi
133
+ .fn()
134
+ .mockReturnValue(makeCheckResult({ state: "ask" })),
238
135
  prompt: vi.fn().mockResolvedValue({
239
136
  approved: true,
240
137
  state: "approved_for_session",
@@ -259,7 +156,9 @@ describe("handleToolCall decision events — user_denied", () => {
259
156
  it("emits deny with user_denied when state=ask and user denies", async () => {
260
157
  const { handler, events } = makeHandler({
261
158
  session: {
262
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
159
+ checkPermission: vi
160
+ .fn()
161
+ .mockReturnValue(makeCheckResult({ state: "ask" })),
263
162
  prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
264
163
  },
265
164
  });
@@ -281,7 +180,9 @@ describe("handleToolCall decision events — confirmation_unavailable", () => {
281
180
  it("emits deny with confirmation_unavailable when state=ask but no UI", async () => {
282
181
  const { handler, events } = makeHandler({
283
182
  session: {
284
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
183
+ checkPermission: vi
184
+ .fn()
185
+ .mockReturnValue(makeCheckResult({ state: "ask" })),
285
186
  canPrompt: vi.fn().mockReturnValue(false),
286
187
  },
287
188
  });
@@ -307,7 +208,7 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
307
208
  const infraDir = "/test/agent";
308
209
  const { handler, events } = makeHandler({
309
210
  session: {
310
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
211
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult()),
311
212
  getInfrastructureDirs: vi.fn().mockReturnValue([infraDir]),
312
213
  },
313
214
  });
@@ -335,7 +236,9 @@ describe("handleToolCall decision events — auto_approved", () => {
335
236
  it("emits allow with auto_approved when prompt returns autoApproved:true", async () => {
336
237
  const { handler, events } = makeHandler({
337
238
  session: {
338
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
239
+ checkPermission: vi
240
+ .fn()
241
+ .mockReturnValue(makeCheckResult({ state: "ask" })),
339
242
  prompt: vi.fn().mockResolvedValue({
340
243
  approved: true,
341
244
  state: "approved",
@@ -1,13 +1,13 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
1
  import { describe, expect, it, vi } from "vitest";
3
- import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
2
+
3
+ import { getEventInput } from "#src/handlers/permission-gate-handler";
4
+
4
5
  import {
5
- getEventInput,
6
- PermissionGateHandler,
7
- } from "#src/handlers/permission-gate-handler";
8
- import type { PermissionSession } from "#src/permission-session";
9
- import type { ToolRegistry } from "#src/tool-registry";
10
- import type { PermissionCheckResult } from "#src/types";
6
+ makeCheckResult,
7
+ makeCtx,
8
+ makeHandler,
9
+ makeToolCallEvent,
10
+ } from "#test/helpers/handler-fixtures";
11
11
 
12
12
  // ── SDK stubs ──────────────────────────────────────────────────────────────
13
13
  vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
@@ -16,101 +16,6 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
16
16
  return { ...original };
17
17
  });
18
18
 
19
- // ── helpers ────────────────────────────────────────────────────────────────
20
-
21
- function makeCtx(
22
- overrides: Partial<ExtensionContext> & { cwd?: string } = {},
23
- ): 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
- function makeToolCallEvent(
43
- toolName: string,
44
- extraFields: Record<string, unknown> = {},
45
- ) {
46
- return {
47
- type: "tool_call",
48
- toolCallId: "tc-1",
49
- name: toolName,
50
- input: {},
51
- ...extraFields,
52
- };
53
- }
54
-
55
- function makePermissionResult(
56
- state: "allow" | "deny" | "ask",
57
- ): PermissionCheckResult {
58
- return { state, toolName: "read", source: "tool", origin: "builtin" };
59
- }
60
-
61
- function makeSession(
62
- overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
63
- ): PermissionSession {
64
- return {
65
- logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
66
- activate: vi.fn(),
67
- resolveAgentName: vi.fn().mockReturnValue(null),
68
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
69
- getToolPermission: vi.fn().mockReturnValue("allow"),
70
- getSessionRuleset: vi.fn().mockReturnValue([]),
71
- approveSessionRule: vi.fn(),
72
- getActiveSkillEntries: vi.fn().mockReturnValue([]),
73
- getInfrastructureDirs: vi
74
- .fn()
75
- .mockReturnValue(["/test/agent", "/test/agent/git"]),
76
- getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
77
- config: DEFAULT_EXTENSION_CONFIG,
78
- canPrompt: vi.fn().mockReturnValue(true),
79
- prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
80
- ...overrides,
81
- } as unknown as PermissionSession;
82
- }
83
-
84
- function makeEvents() {
85
- return {
86
- emit: vi.fn(),
87
- on: vi.fn().mockReturnValue(() => undefined),
88
- };
89
- }
90
-
91
- function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
92
- return {
93
- getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
94
- setActive: vi.fn(),
95
- ...overrides,
96
- };
97
- }
98
-
99
- function makeHandler(overrides?: {
100
- session?: Partial<Record<keyof PermissionSession, unknown>>;
101
- toolRegistry?: Partial<ToolRegistry>;
102
- }): {
103
- handler: PermissionGateHandler;
104
- session: PermissionSession;
105
- toolRegistry: ToolRegistry;
106
- } {
107
- const session = makeSession(overrides?.session);
108
- const events = makeEvents();
109
- const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
110
- const handler = new PermissionGateHandler(session, events, toolRegistry);
111
- return { handler, session, toolRegistry };
112
- }
113
-
114
19
  // ── getEventInput ──────────────────────────────────────────────────────────
115
20
 
116
21
  describe("getEventInput", () => {
@@ -184,7 +89,9 @@ describe("handleToolCall", () => {
184
89
  it("blocks when tool is denied by policy", async () => {
185
90
  const { handler } = makeHandler({
186
91
  session: {
187
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
92
+ checkPermission: vi
93
+ .fn()
94
+ .mockReturnValue(makeCheckResult({ state: "deny" })),
188
95
  },
189
96
  });
190
97
  const result = await handler.handleToolCall(
@@ -259,7 +166,9 @@ describe("handleToolCall — external-directory gate", () => {
259
166
  it("blocks a read of a path outside cwd when policy is deny", async () => {
260
167
  const { handler } = makeHandler({
261
168
  session: {
262
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
169
+ checkPermission: vi
170
+ .fn()
171
+ .mockReturnValue(makeCheckResult({ state: "deny" })),
263
172
  },
264
173
  toolRegistry: {
265
174
  getAll: vi.fn().mockReturnValue([{ name: "read" }]),
@@ -282,7 +191,9 @@ describe("handleToolCall — bash external-directory gate", () => {
282
191
  it("blocks a bash command referencing an external path when policy is deny", async () => {
283
192
  const { handler } = makeHandler({
284
193
  session: {
285
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
194
+ checkPermission: vi
195
+ .fn()
196
+ .mockReturnValue(makeCheckResult({ state: "deny" })),
286
197
  },
287
198
  toolRegistry: {
288
199
  getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
@@ -308,9 +219,9 @@ describe("handleToolCall — path gate (tools)", () => {
308
219
  .mockImplementation(
309
220
  (surface: string, _input: unknown, _agentName?: string) => {
310
221
  if (surface === "path") {
311
- return { ...makePermissionResult("deny"), matchedPattern: "*.env" };
222
+ return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
312
223
  }
313
- return makePermissionResult("allow");
224
+ return makeCheckResult();
314
225
  },
315
226
  );
316
227
  const { handler } = makeHandler({
@@ -355,9 +266,9 @@ describe("handleToolCall — bash path gate", () => {
355
266
  .mockImplementation(
356
267
  (surface: string, _input: unknown, _agentName?: string) => {
357
268
  if (surface === "path") {
358
- return { ...makePermissionResult("deny"), matchedPattern: "*.env" };
269
+ return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
359
270
  }
360
- return makePermissionResult("allow");
271
+ return makeCheckResult();
361
272
  },
362
273
  );
363
274
  const { handler } = makeHandler({
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared gate-level test fixtures for gate descriptor and runner tests.
3
+ */
4
+ import { vi } from "vitest";
5
+
6
+ import type {
7
+ GateDescriptor,
8
+ GateRunnerDeps,
9
+ } from "#src/handlers/gates/descriptor";
10
+ import type { ToolCallContext } from "#src/handlers/gates/types";
11
+ import type { PermissionCheckResult } from "#src/types";
12
+
13
+ import { makeCheckResult } from "#test/helpers/handler-fixtures";
14
+
15
+ /**
16
+ * Gate descriptor factory with runner-test defaults.
17
+ *
18
+ * Uses deny as the default `denialContext` check result so tests that
19
+ * verify block paths don't need to override the surface check.
20
+ */
21
+ export function makeDescriptor(
22
+ overrides: Partial<GateDescriptor> = {},
23
+ ): GateDescriptor {
24
+ return {
25
+ surface: "read",
26
+ input: {},
27
+ denialContext: {
28
+ kind: "tool",
29
+ check: makeCheckResult({ state: "deny", matchedPattern: "*" }),
30
+ },
31
+ promptDetails: {
32
+ source: "tool_call",
33
+ agentName: null,
34
+ message: "Allow tool 'read'?",
35
+ toolCallId: "tc-1",
36
+ toolName: "read",
37
+ },
38
+ logContext: {
39
+ source: "tool_call",
40
+ toolCallId: "tc-1",
41
+ toolName: "read",
42
+ },
43
+ decision: {
44
+ surface: "read",
45
+ value: "read",
46
+ },
47
+ ...overrides,
48
+ };
49
+ }
50
+
51
+ export function makeRunnerDeps(
52
+ overrides: Partial<GateRunnerDeps> = {},
53
+ ): GateRunnerDeps {
54
+ return {
55
+ checkPermission: vi
56
+ .fn()
57
+ .mockReturnValue(makeCheckResult({ matchedPattern: "*" })),
58
+ getSessionRuleset: vi.fn().mockReturnValue([]),
59
+ recordSessionApproval: vi.fn(),
60
+ writeReviewLog: vi.fn(),
61
+ emitDecision: vi.fn(),
62
+ canConfirm: vi.fn().mockReturnValue(true),
63
+ promptPermission: vi
64
+ .fn()
65
+ .mockResolvedValue({ approved: true, state: "approved" }),
66
+ ...overrides,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Tool-call context factory with bash defaults.
72
+ *
73
+ * path.test.ts uses different defaults (toolName "read", path input) and
74
+ * keeps a local wrapper; bash-path.test.ts uses this factory directly.
75
+ */
76
+ export function makeTcc(
77
+ overrides: Partial<ToolCallContext> = {},
78
+ ): ToolCallContext {
79
+ return {
80
+ toolName: "bash",
81
+ agentName: null,
82
+ input: { command: "cat .env" },
83
+ toolCallId: "tc-1",
84
+ cwd: "/test/project",
85
+ ...overrides,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Path-surface check result factory.
91
+ *
92
+ * Shared between bash-path.test.ts and path.test.ts; both use
93
+ * toolName "path", source "special", origin "global" as defaults.
94
+ */
95
+ export function makeGateCheckResult(
96
+ overrides: Partial<PermissionCheckResult> = {},
97
+ ): PermissionCheckResult {
98
+ return {
99
+ toolName: "path",
100
+ state: "allow",
101
+ source: "special",
102
+ origin: "global",
103
+ ...overrides,
104
+ };
105
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Shared handler-level test fixtures for PermissionGateHandler tests.
3
+ *
4
+ * All factories use override bags so callers can specialize any field
5
+ * without constructing the full object from scratch.
6
+ */
7
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
8
+ import { vi } from "vitest";
9
+
10
+ import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
11
+ import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
12
+ import type { PermissionDecisionEvent } from "#src/permission-events";
13
+ import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
14
+ import type { PermissionSession } from "#src/permission-session";
15
+ import type { ToolRegistry } from "#src/tool-registry";
16
+ import type { PermissionCheckResult } from "#src/types";
17
+
18
+ export function makeEvents() {
19
+ return {
20
+ emit: vi.fn(),
21
+ on: vi.fn().mockReturnValue(() => undefined),
22
+ };
23
+ }
24
+
25
+ export function makeCtx(
26
+ overrides: Partial<ExtensionContext> = {},
27
+ ): ExtensionContext {
28
+ return {
29
+ cwd: "/test/project",
30
+ hasUI: true,
31
+ ui: {
32
+ setStatus: vi.fn(),
33
+ notify: vi.fn(),
34
+ select: vi.fn(),
35
+ input: vi.fn(),
36
+ },
37
+ sessionManager: {
38
+ getEntries: vi.fn().mockReturnValue([]),
39
+ getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
40
+ addEntry: vi.fn(),
41
+ },
42
+ ...overrides,
43
+ } as unknown as ExtensionContext;
44
+ }
45
+
46
+ export function makeToolCallEvent(
47
+ toolName: string,
48
+ extraFields: Record<string, unknown> = {},
49
+ ) {
50
+ return {
51
+ type: "tool_call",
52
+ toolCallId: "tc-1",
53
+ name: toolName,
54
+ input: {},
55
+ ...extraFields,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Neutral-default check-result builder.
61
+ *
62
+ * Pass exactly the fields the original fixture hard-coded so divergent
63
+ * defaults across test files are preserved at their call sites.
64
+ */
65
+ export function makeCheckResult(
66
+ overrides: Partial<PermissionCheckResult> = {},
67
+ ): PermissionCheckResult {
68
+ return {
69
+ state: "allow",
70
+ toolName: "read",
71
+ source: "tool",
72
+ origin: "builtin",
73
+ ...overrides,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Full-union session stub.
79
+ *
80
+ * Includes every method mocked across handler test files so each file
81
+ * only needs to override the fields that differ from the defaults.
82
+ */
83
+ export function makeSession(
84
+ overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
85
+ ): PermissionSession {
86
+ return {
87
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
88
+ activate: vi.fn(),
89
+ resolveAgentName: vi.fn().mockReturnValue(null),
90
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult()),
91
+ getToolPermission: vi.fn().mockReturnValue("allow"),
92
+ getSessionRuleset: vi.fn().mockReturnValue([]),
93
+ recordSessionApproval: vi.fn(),
94
+ getActiveSkillEntries: vi.fn().mockReturnValue([]),
95
+ getInfrastructureDirs: vi
96
+ .fn()
97
+ .mockReturnValue(["/test/agent", "/test/agent/git"]),
98
+ getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
99
+ config: DEFAULT_EXTENSION_CONFIG,
100
+ canPrompt: vi.fn().mockReturnValue(true),
101
+ prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
102
+ createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
103
+ ...overrides,
104
+ } as unknown as PermissionSession;
105
+ }
106
+
107
+ export function makeToolRegistry(
108
+ overrides: Partial<ToolRegistry> = {},
109
+ ): ToolRegistry {
110
+ return {
111
+ getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
112
+ setActive: vi.fn(),
113
+ ...overrides,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Constructs a PermissionGateHandler with mocked collaborators.
119
+ *
120
+ * Returns all collaborators so each test file can destructure only what
121
+ * it needs — handler, events, session, and toolRegistry are all available.
122
+ */
123
+ export function makeHandler(overrides?: {
124
+ session?: Partial<Record<keyof PermissionSession, unknown>>;
125
+ toolRegistry?: Partial<ToolRegistry>;
126
+ }) {
127
+ const session = makeSession(overrides?.session);
128
+ const events = makeEvents();
129
+ const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
130
+ const handler = new PermissionGateHandler(session, events, toolRegistry);
131
+ return { handler, events, session, toolRegistry };
132
+ }
133
+
134
+ /** Extract all permissions:decision payloads from the events.emit mock. */
135
+ export function getDecisionEvents(
136
+ events: ReturnType<typeof makeEvents>,
137
+ ): PermissionDecisionEvent[] {
138
+ return events.emit.mock.calls
139
+ .filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
140
+ .map(([, payload]) => payload as PermissionDecisionEvent);
141
+ }