@gotgenes/pi-permission-system 5.10.0 → 5.11.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.
@@ -5,11 +5,11 @@
5
5
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
6
  import { describe, expect, it, vi } from "vitest";
7
7
 
8
- import { handleToolCall } from "../../src/handlers/tool-call";
9
- import type { HandlerDeps } from "../../src/handlers/types";
8
+ import { PermissionGateHandler } from "../../src/handlers/permission-gate-handler";
10
9
  import type { PermissionDecisionEvent } from "../../src/permission-events";
11
10
  import { PERMISSIONS_DECISION_CHANNEL } from "../../src/permission-events";
12
11
  import type { PermissionSession } from "../../src/permission-session";
12
+ import type { ToolRegistry } from "../../src/tool-registry";
13
13
  import type { PermissionCheckResult, PermissionState } from "../../src/types";
14
14
 
15
15
  // ── helpers ────────────────────────────────────────────────────────────────
@@ -83,30 +83,40 @@ function makeSession(
83
83
  .fn()
84
84
  .mockReturnValue(["/test/agent", "/test/agent/git"]),
85
85
  getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
86
+ canPrompt: vi.fn().mockReturnValue(true),
87
+ prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
86
88
  ...overrides,
87
89
  } as unknown as PermissionSession;
88
90
  }
89
91
 
90
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
92
+ function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
91
93
  return {
92
- session: makeSession(),
93
- events: makeEvents(),
94
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
95
- promptPermission: vi
96
- .fn()
97
- .mockResolvedValue({ approved: true, state: "approved" }),
98
- createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
99
- stopPermissionRpcHandlers: vi.fn(),
100
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
101
- setActiveTools: vi.fn(),
94
+ getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
95
+ setActive: vi.fn(),
102
96
  ...overrides,
103
97
  };
104
98
  }
105
99
 
100
+ function makeHandler(overrides?: {
101
+ session?: Partial<Record<keyof PermissionSession, unknown>>;
102
+ toolRegistry?: Partial<ToolRegistry>;
103
+ }): {
104
+ handler: PermissionGateHandler;
105
+ events: ReturnType<typeof makeEvents>;
106
+ session: PermissionSession;
107
+ } {
108
+ const session = makeSession(overrides?.session);
109
+ const events = makeEvents();
110
+ const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
111
+ const handler = new PermissionGateHandler(session, events, toolRegistry);
112
+ return { handler, events, session };
113
+ }
114
+
106
115
  /** Extract all permissions:decision payloads from the events.emit mock. */
107
- function getDecisionEvents(deps: HandlerDeps): PermissionDecisionEvent[] {
108
- const emitMock = (deps.events as ReturnType<typeof makeEvents>).emit;
109
- return emitMock.mock.calls
116
+ function getDecisionEvents(
117
+ events: ReturnType<typeof makeEvents>,
118
+ ): PermissionDecisionEvent[] {
119
+ return events.emit.mock.calls
110
120
  .filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
111
121
  .map(([, payload]) => payload as PermissionDecisionEvent);
112
122
  }
@@ -115,21 +125,22 @@ function getDecisionEvents(deps: HandlerDeps): PermissionDecisionEvent[] {
115
125
 
116
126
  describe("handleToolCall decision events — policy_allow", () => {
117
127
  it("emits allow with policy_allow when checkPermission returns allow", async () => {
118
- const session = makeSession({
119
- checkPermission: vi.fn().mockReturnValue(
120
- makeCheckResult("allow", {
121
- origin: "global",
122
- matchedPattern: "*",
123
- }),
124
- ),
128
+ const { handler, events } = makeHandler({
129
+ session: {
130
+ checkPermission: vi.fn().mockReturnValue(
131
+ makeCheckResult("allow", {
132
+ origin: "global",
133
+ matchedPattern: "*",
134
+ }),
135
+ ),
136
+ },
125
137
  });
126
- const deps = makeDeps({ session });
127
138
 
128
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
139
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
129
140
 
130
- const events = getDecisionEvents(deps);
131
- expect(events).toHaveLength(1);
132
- expect(events[0]).toMatchObject({
141
+ const decisions = getDecisionEvents(events);
142
+ expect(decisions).toHaveLength(1);
143
+ expect(decisions[0]).toMatchObject({
133
144
  surface: "read",
134
145
  result: "allow",
135
146
  resolution: "policy_allow",
@@ -143,21 +154,22 @@ describe("handleToolCall decision events — policy_allow", () => {
143
154
 
144
155
  describe("handleToolCall decision events — policy_deny", () => {
145
156
  it("emits deny with policy_deny when checkPermission returns deny", async () => {
146
- const session = makeSession({
147
- checkPermission: vi.fn().mockReturnValue(
148
- makeCheckResult("deny", {
149
- origin: "project",
150
- matchedPattern: "read",
151
- }),
152
- ),
157
+ const { handler, events } = makeHandler({
158
+ session: {
159
+ checkPermission: vi.fn().mockReturnValue(
160
+ makeCheckResult("deny", {
161
+ origin: "project",
162
+ matchedPattern: "read",
163
+ }),
164
+ ),
165
+ },
153
166
  });
154
- const deps = makeDeps({ session });
155
167
 
156
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
168
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
157
169
 
158
- const events = getDecisionEvents(deps);
159
- expect(events).toHaveLength(1);
160
- expect(events[0]).toMatchObject({
170
+ const decisions = getDecisionEvents(events);
171
+ expect(decisions).toHaveLength(1);
172
+ expect(decisions[0]).toMatchObject({
161
173
  surface: "read",
162
174
  result: "deny",
163
175
  resolution: "policy_deny",
@@ -169,25 +181,25 @@ describe("handleToolCall decision events — policy_deny", () => {
169
181
 
170
182
  describe("handleToolCall decision events — session_approved", () => {
171
183
  it("emits allow with session_approved when checkPermission returns source:session", async () => {
172
- const session = makeSession({
173
- checkPermission: vi.fn().mockReturnValue(
174
- makeCheckResult("allow", {
175
- source: "session",
176
- matchedPattern: "git *",
177
- }),
178
- ),
184
+ const { handler, events } = makeHandler({
185
+ session: {
186
+ checkPermission: vi.fn().mockReturnValue(
187
+ makeCheckResult("allow", {
188
+ source: "session",
189
+ matchedPattern: "git *",
190
+ }),
191
+ ),
192
+ },
179
193
  });
180
- const deps = makeDeps({ session });
181
194
 
182
- await handleToolCall(
183
- deps,
195
+ await handler.handleToolCall(
184
196
  makeToolCallEvent("bash", { input: { command: "git status" } }),
185
197
  makeCtx(),
186
198
  );
187
199
 
188
- const events = getDecisionEvents(deps);
189
- expect(events).toHaveLength(1);
190
- expect(events[0]).toMatchObject({
200
+ const decisions = getDecisionEvents(events);
201
+ expect(decisions).toHaveLength(1);
202
+ expect(decisions[0]).toMatchObject({
191
203
  surface: "bash",
192
204
  result: "allow",
193
205
  resolution: "session_approved",
@@ -199,42 +211,41 @@ describe("handleToolCall decision events — session_approved", () => {
199
211
 
200
212
  describe("handleToolCall decision events — user_approved", () => {
201
213
  it("emits allow with user_approved when state=ask and user approves once", async () => {
202
- const session = makeSession({
203
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
204
- });
205
- const deps = makeDeps({
206
- session,
207
- promptPermission: vi
208
- .fn()
209
- .mockResolvedValue({ approved: true, state: "approved" }),
214
+ const { handler, events } = makeHandler({
215
+ session: {
216
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
217
+ prompt: vi
218
+ .fn()
219
+ .mockResolvedValue({ approved: true, state: "approved" }),
220
+ },
210
221
  });
211
222
 
212
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
223
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
213
224
 
214
- const events = getDecisionEvents(deps);
215
- expect(events).toHaveLength(1);
216
- expect(events[0]).toMatchObject({
225
+ const decisions = getDecisionEvents(events);
226
+ expect(decisions).toHaveLength(1);
227
+ expect(decisions[0]).toMatchObject({
217
228
  result: "allow",
218
229
  resolution: "user_approved",
219
230
  });
220
231
  });
221
232
 
222
233
  it("emits allow with user_approved_for_session when user approves for session", async () => {
223
- const session = makeSession({
224
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
225
- });
226
- const deps = makeDeps({
227
- session,
228
- promptPermission: vi
229
- .fn()
230
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
234
+ const { handler, events } = makeHandler({
235
+ session: {
236
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
237
+ prompt: vi.fn().mockResolvedValue({
238
+ approved: true,
239
+ state: "approved_for_session",
240
+ }),
241
+ },
231
242
  });
232
243
 
233
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
244
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
234
245
 
235
- const events = getDecisionEvents(deps);
236
- expect(events).toHaveLength(1);
237
- expect(events[0]).toMatchObject({
246
+ const decisions = getDecisionEvents(events);
247
+ expect(decisions).toHaveLength(1);
248
+ expect(decisions[0]).toMatchObject({
238
249
  result: "allow",
239
250
  resolution: "user_approved_for_session",
240
251
  });
@@ -245,21 +256,18 @@ describe("handleToolCall decision events — user_approved", () => {
245
256
 
246
257
  describe("handleToolCall decision events — user_denied", () => {
247
258
  it("emits deny with user_denied when state=ask and user denies", async () => {
248
- const session = makeSession({
249
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
250
- });
251
- const deps = makeDeps({
252
- session,
253
- promptPermission: vi
254
- .fn()
255
- .mockResolvedValue({ approved: false, state: "denied" }),
259
+ const { handler, events } = makeHandler({
260
+ session: {
261
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
262
+ prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
263
+ },
256
264
  });
257
265
 
258
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
266
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
259
267
 
260
- const events = getDecisionEvents(deps);
261
- expect(events).toHaveLength(1);
262
- expect(events[0]).toMatchObject({
268
+ const decisions = getDecisionEvents(events);
269
+ expect(decisions).toHaveLength(1);
270
+ expect(decisions[0]).toMatchObject({
263
271
  result: "deny",
264
272
  resolution: "user_denied",
265
273
  });
@@ -270,23 +278,21 @@ describe("handleToolCall decision events — user_denied", () => {
270
278
 
271
279
  describe("handleToolCall decision events — confirmation_unavailable", () => {
272
280
  it("emits deny with confirmation_unavailable when state=ask but no UI", async () => {
273
- const session = makeSession({
274
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
275
- });
276
- const deps = makeDeps({
277
- session,
278
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
281
+ const { handler, events } = makeHandler({
282
+ session: {
283
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
284
+ canPrompt: vi.fn().mockReturnValue(false),
285
+ },
279
286
  });
280
287
 
281
- await handleToolCall(
282
- deps,
288
+ await handler.handleToolCall(
283
289
  makeToolCallEvent("read"),
284
290
  makeCtx({ hasUI: false }),
285
291
  );
286
292
 
287
- const events = getDecisionEvents(deps);
288
- expect(events).toHaveLength(1);
289
- expect(events[0]).toMatchObject({
293
+ const decisions = getDecisionEvents(events);
294
+ expect(decisions).toHaveLength(1);
295
+ expect(decisions[0]).toMatchObject({
290
296
  result: "deny",
291
297
  resolution: "confirmation_unavailable",
292
298
  });
@@ -298,19 +304,20 @@ describe("handleToolCall decision events — confirmation_unavailable", () => {
298
304
  describe("handleToolCall decision events — infrastructure_auto_allowed", () => {
299
305
  it("emits allow with infrastructure_auto_allowed for Pi infra reads", async () => {
300
306
  const infraDir = "/test/agent";
301
- const session = makeSession({
302
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
303
- getInfrastructureDirs: vi.fn().mockReturnValue([infraDir]),
307
+ const { handler, events } = makeHandler({
308
+ session: {
309
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
310
+ getInfrastructureDirs: vi.fn().mockReturnValue([infraDir]),
311
+ },
304
312
  });
305
- const deps = makeDeps({ session });
306
313
 
307
314
  const event = makeToolCallEvent("read", {
308
315
  input: { path: `${infraDir}/some-file.json` },
309
316
  });
310
- await handleToolCall(deps, event, makeCtx());
317
+ await handler.handleToolCall(event, makeCtx());
311
318
 
312
- const events = getDecisionEvents(deps);
313
- const infraEvents = events.filter(
319
+ const decisions = getDecisionEvents(events);
320
+ const infraEvents = decisions.filter(
314
321
  (e) => e.resolution === "infrastructure_auto_allowed",
315
322
  );
316
323
  expect(infraEvents).toHaveLength(1);
@@ -324,24 +331,23 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
324
331
  // ── auto_approved path (yolo mode) ───────────────────────────────────
325
332
 
326
333
  describe("handleToolCall decision events — auto_approved", () => {
327
- it("emits allow with auto_approved when promptPermission returns autoApproved:true", async () => {
328
- const session = makeSession({
329
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
330
- });
331
- const deps = makeDeps({
332
- session,
333
- promptPermission: vi.fn().mockResolvedValue({
334
- approved: true,
335
- state: "approved",
336
- autoApproved: true,
337
- }),
334
+ it("emits allow with auto_approved when prompt returns autoApproved:true", async () => {
335
+ const { handler, events } = makeHandler({
336
+ session: {
337
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
338
+ prompt: vi.fn().mockResolvedValue({
339
+ approved: true,
340
+ state: "approved",
341
+ autoApproved: true,
342
+ }),
343
+ },
338
344
  });
339
345
 
340
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
346
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
341
347
 
342
- const events = getDecisionEvents(deps);
343
- expect(events).toHaveLength(1);
344
- expect(events[0]).toMatchObject({
348
+ const decisions = getDecisionEvents(events);
349
+ expect(decisions).toHaveLength(1);
350
+ expect(decisions[0]).toMatchObject({
345
351
  result: "allow",
346
352
  resolution: "auto_approved",
347
353
  });
@@ -1,9 +1,12 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
 
4
- import { getEventInput, handleToolCall } from "../../src/handlers/tool-call";
5
- import type { HandlerDeps } from "../../src/handlers/types";
4
+ import {
5
+ getEventInput,
6
+ PermissionGateHandler,
7
+ } from "../../src/handlers/permission-gate-handler";
6
8
  import type { PermissionSession } from "../../src/permission-session";
9
+ import type { ToolRegistry } from "../../src/tool-registry";
7
10
  import type { PermissionCheckResult, PermissionState } from "../../src/types";
8
11
 
9
12
  // ── SDK stubs ──────────────────────────────────────────────────────────────
@@ -71,26 +74,42 @@ function makeSession(
71
74
  .fn()
72
75
  .mockReturnValue(["/test/agent", "/test/agent/git"]),
73
76
  getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
77
+ canPrompt: vi.fn().mockReturnValue(true),
78
+ prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
74
79
  ...overrides,
75
80
  } as unknown as PermissionSession;
76
81
  }
77
82
 
78
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
83
+ function makeEvents() {
79
84
  return {
80
- session: makeSession(),
81
- events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
82
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
83
- promptPermission: vi
84
- .fn()
85
- .mockResolvedValue({ approved: true, state: "approved" }),
86
- createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
87
- stopPermissionRpcHandlers: vi.fn(),
88
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
89
- setActiveTools: vi.fn(),
85
+ emit: vi.fn(),
86
+ on: vi.fn().mockReturnValue(() => undefined),
87
+ };
88
+ }
89
+
90
+ function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
91
+ return {
92
+ getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
93
+ setActive: vi.fn(),
90
94
  ...overrides,
91
95
  };
92
96
  }
93
97
 
98
+ function makeHandler(overrides?: {
99
+ session?: Partial<Record<keyof PermissionSession, unknown>>;
100
+ toolRegistry?: Partial<ToolRegistry>;
101
+ }): {
102
+ handler: PermissionGateHandler;
103
+ session: PermissionSession;
104
+ toolRegistry: ToolRegistry;
105
+ } {
106
+ const session = makeSession(overrides?.session);
107
+ const events = makeEvents();
108
+ const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
109
+ const handler = new PermissionGateHandler(session, events, toolRegistry);
110
+ return { handler, session, toolRegistry };
111
+ }
112
+
94
113
  // ── getEventInput ──────────────────────────────────────────────────────────
95
114
 
96
115
  describe("getEventInput", () => {
@@ -122,14 +141,17 @@ describe("getEventInput", () => {
122
141
  describe("handleToolCall", () => {
123
142
  it("activates session with ctx", async () => {
124
143
  const ctx = makeCtx();
125
- const deps = makeDeps();
126
- await handleToolCall(deps, makeToolCallEvent("read"), ctx);
127
- expect(deps.session.activate).toHaveBeenCalledWith(ctx);
144
+ const { handler, session } = makeHandler();
145
+ await handler.handleToolCall(makeToolCallEvent("read"), ctx);
146
+ expect(session.activate).toHaveBeenCalledWith(ctx);
128
147
  });
129
148
 
130
149
  it("blocks when tool name cannot be resolved", async () => {
131
- const deps = makeDeps();
132
- const result = await handleToolCall(deps, { type: "tool_call" }, makeCtx());
150
+ const { handler } = makeHandler();
151
+ const result = await handler.handleToolCall(
152
+ { type: "tool_call" },
153
+ makeCtx(),
154
+ );
133
155
  expect(result).toEqual({
134
156
  block: true,
135
157
  reason: expect.stringContaining("tool"),
@@ -137,11 +159,12 @@ describe("handleToolCall", () => {
137
159
  });
138
160
 
139
161
  it("blocks when tool is not registered", async () => {
140
- const deps = makeDeps({
141
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
162
+ const { handler } = makeHandler({
163
+ toolRegistry: {
164
+ getAll: vi.fn().mockReturnValue([{ name: "read" }]),
165
+ },
142
166
  });
143
- const result = await handleToolCall(
144
- deps,
167
+ const result = await handler.handleToolCall(
145
168
  makeToolCallEvent("unknown-tool"),
146
169
  makeCtx(),
147
170
  );
@@ -149,9 +172,8 @@ describe("handleToolCall", () => {
149
172
  });
150
173
 
151
174
  it("returns empty object when tool is allowed", async () => {
152
- const deps = makeDeps();
153
- const result = await handleToolCall(
154
- deps,
175
+ const { handler } = makeHandler();
176
+ const result = await handler.handleToolCall(
155
177
  makeToolCallEvent("read"),
156
178
  makeCtx(),
157
179
  );
@@ -159,12 +181,12 @@ describe("handleToolCall", () => {
159
181
  });
160
182
 
161
183
  it("blocks when tool is denied by policy", async () => {
162
- const session = makeSession({
163
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
184
+ const { handler } = makeHandler({
185
+ session: {
186
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
187
+ },
164
188
  });
165
- const deps = makeDeps({ session });
166
- const result = await handleToolCall(
167
- deps,
189
+ const result = await handler.handleToolCall(
168
190
  makeToolCallEvent("read"),
169
191
  makeCtx(),
170
192
  );
@@ -184,12 +206,13 @@ describe("handleToolCall — skill-read gate", () => {
184
206
  normalizedLocation: "/skills/librarian/SKILL.md",
185
207
  normalizedBaseDir: "/skills/librarian",
186
208
  };
187
- const session = makeSession({
188
- getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
189
- });
190
- const deps = makeDeps({
191
- session,
192
- getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
209
+ const { handler } = makeHandler({
210
+ session: {
211
+ getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
212
+ },
213
+ toolRegistry: {
214
+ getAll: vi.fn().mockReturnValue([{ toolName: "read" }]),
215
+ },
193
216
  });
194
217
  const event = {
195
218
  type: "tool_call",
@@ -197,7 +220,7 @@ describe("handleToolCall — skill-read gate", () => {
197
220
  toolName: "read",
198
221
  input: { path: "/skills/librarian/SKILL.md" },
199
222
  };
200
- const result = await handleToolCall(deps, event, makeCtx());
223
+ const result = await handler.handleToolCall(event, makeCtx());
201
224
  expect(result).toMatchObject({ block: true });
202
225
  });
203
226
 
@@ -210,12 +233,13 @@ describe("handleToolCall — skill-read gate", () => {
210
233
  normalizedLocation: "/skills/librarian/SKILL.md",
211
234
  normalizedBaseDir: "/skills/librarian",
212
235
  };
213
- const session = makeSession({
214
- getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
215
- });
216
- const deps = makeDeps({
217
- session,
218
- getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
236
+ const { handler } = makeHandler({
237
+ session: {
238
+ getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
239
+ },
240
+ toolRegistry: {
241
+ getAll: vi.fn().mockReturnValue([{ toolName: "read" }]),
242
+ },
219
243
  });
220
244
  const event = {
221
245
  type: "tool_call",
@@ -223,7 +247,7 @@ describe("handleToolCall — skill-read gate", () => {
223
247
  toolName: "read",
224
248
  input: { path: "/test/project/src/index.ts" },
225
249
  };
226
- const result = await handleToolCall(deps, event, makeCtx());
250
+ const result = await handler.handleToolCall(event, makeCtx());
227
251
  expect(result).toEqual({});
228
252
  });
229
253
  });
@@ -232,12 +256,13 @@ describe("handleToolCall — skill-read gate", () => {
232
256
 
233
257
  describe("handleToolCall — external-directory gate", () => {
234
258
  it("blocks a read of a path outside cwd when policy is deny", async () => {
235
- const session = makeSession({
236
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
237
- });
238
- const deps = makeDeps({
239
- session,
240
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
259
+ const { handler } = makeHandler({
260
+ session: {
261
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
262
+ },
263
+ toolRegistry: {
264
+ getAll: vi.fn().mockReturnValue([{ name: "read" }]),
265
+ },
241
266
  });
242
267
  const event = {
243
268
  type: "tool_call",
@@ -245,7 +270,7 @@ describe("handleToolCall — external-directory gate", () => {
245
270
  name: "read",
246
271
  input: { path: "/outside/project/file.ts" },
247
272
  };
248
- const result = await handleToolCall(deps, event, makeCtx());
273
+ const result = await handler.handleToolCall(event, makeCtx());
249
274
  expect(result).toMatchObject({ block: true });
250
275
  });
251
276
  });
@@ -254,12 +279,13 @@ describe("handleToolCall — external-directory gate", () => {
254
279
 
255
280
  describe("handleToolCall — bash external-directory gate", () => {
256
281
  it("blocks a bash command referencing an external path when policy is deny", async () => {
257
- const session = makeSession({
258
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
259
- });
260
- const deps = makeDeps({
261
- session,
262
- getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
282
+ const { handler } = makeHandler({
283
+ session: {
284
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
285
+ },
286
+ toolRegistry: {
287
+ getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
288
+ },
263
289
  });
264
290
  const event = {
265
291
  type: "tool_call",
@@ -267,7 +293,7 @@ describe("handleToolCall — bash external-directory gate", () => {
267
293
  name: "bash",
268
294
  input: { command: "cat /outside/project/file.ts" },
269
295
  };
270
- const result = await handleToolCall(deps, event, makeCtx());
296
+ const result = await handler.handleToolCall(event, makeCtx());
271
297
  expect(result).toMatchObject({ block: true });
272
298
  });
273
299
  });
@@ -15,8 +15,8 @@ vi.mock("../src/forwarded-permissions/polling", () => ({
15
15
 
16
16
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
17
17
  import { DEFAULT_EXTENSION_CONFIG } from "../src/extension-config";
18
- import type { PromptPermissionDetails } from "../src/handlers/types";
19
18
  import type { PermissionPromptDecision } from "../src/permission-dialog";
19
+ import type { PromptPermissionDetails } from "../src/permission-prompter";
20
20
  import {
21
21
  PermissionPrompter,
22
22
  type PermissionPrompterDeps,