@gotgenes/pi-permission-system 5.9.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.
@@ -3,11 +3,11 @@ import { describe, expect, it, vi } from "vitest";
3
3
 
4
4
  import {
5
5
  extractSkillNameFromInput,
6
- handleInput,
7
- } from "../../src/handlers/input";
8
- import type { HandlerDeps } from "../../src/handlers/types";
9
- import type { SessionState } from "../../src/runtime";
10
- import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
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 { PermissionState } from "../../src/types";
11
11
 
12
12
  // ── helpers ────────────────────────────────────────────────────────────────
13
13
 
@@ -34,49 +34,51 @@ function makeInputEvent(text: string) {
34
34
  return { text };
35
35
  }
36
36
 
37
- function makeSession(overrides: Partial<SessionState> = {}): SessionState {
37
+ function makeSession(
38
+ overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
39
+ ): PermissionSession {
38
40
  return {
39
- runtimeContext: null,
40
- permissionManager: {
41
- checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
42
- } as unknown as SessionState["permissionManager"],
43
- activeSkillEntries: [] as SkillPromptEntry[],
44
- lastKnownActiveAgentName: null,
45
- lastActiveToolsCacheKey: null,
46
- lastPromptStateCacheKey: null,
47
- sessionRules: {
48
- approve: vi.fn(),
49
- getRuleset: vi.fn().mockReturnValue([]),
50
- clear: vi.fn(),
51
- } as unknown as SessionState["sessionRules"],
41
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
42
+ activate: vi.fn(),
43
+ resolveAgentName: vi.fn().mockReturnValue(null),
44
+ checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
45
+ getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
46
+ getSessionRuleset: vi.fn().mockReturnValue([]),
47
+ approveSessionRule: vi.fn(),
48
+ canPrompt: vi.fn().mockReturnValue(true),
49
+ prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
50
+ createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
52
51
  ...overrides,
52
+ } as unknown as PermissionSession;
53
+ }
54
+
55
+ function makeEvents() {
56
+ return {
57
+ emit: vi.fn(),
58
+ on: vi.fn().mockReturnValue(() => undefined),
53
59
  };
54
60
  }
55
61
 
56
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
62
+ function makeToolRegistry(): ToolRegistry {
57
63
  return {
58
- session: makeSession(),
59
- logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
60
- piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
61
- getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
62
- createPermissionManagerForCwd: vi.fn(),
63
- refreshExtensionConfig: vi.fn(),
64
- logResolvedConfigPaths: vi.fn(),
65
- resolveAgentName: vi.fn().mockReturnValue(null),
66
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
67
- promptPermission: vi
68
- .fn()
69
- .mockResolvedValue({ approved: true, state: "approved" }),
70
- createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
71
- events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
72
- forwarding: { start: vi.fn(), stop: vi.fn() },
73
- stopPermissionRpcHandlers: vi.fn(),
74
- getAllTools: vi.fn().mockReturnValue([]),
75
- setActiveTools: vi.fn(),
76
- ...overrides,
64
+ getAll: vi.fn().mockReturnValue([]),
65
+ setActive: vi.fn(),
77
66
  };
78
67
  }
79
68
 
69
+ function makeHandler(overrides?: {
70
+ session?: Partial<Record<keyof PermissionSession, unknown>>;
71
+ }): {
72
+ handler: PermissionGateHandler;
73
+ session: PermissionSession;
74
+ } {
75
+ const session = makeSession(overrides?.session);
76
+ const events = makeEvents();
77
+ const toolRegistry = makeToolRegistry();
78
+ const handler = new PermissionGateHandler(session, events, toolRegistry);
79
+ return { handler, session };
80
+ }
81
+
80
82
  // ── extractSkillNameFromInput ──────────────────────────────────────────────
81
83
 
82
84
  describe("extractSkillNameFromInput", () => {
@@ -114,24 +116,16 @@ describe("extractSkillNameFromInput", () => {
114
116
  // ── handleInput ───────────────────────────────────────────────────────────
115
117
 
116
118
  describe("handleInput", () => {
117
- it("sets runtime context", async () => {
118
- const ctx = makeCtx();
119
- const deps = makeDeps();
120
- await handleInput(deps, makeInputEvent("hello"), ctx);
121
- expect(deps.session.runtimeContext).toBe(ctx);
122
- });
123
-
124
- it("starts forwarded permission polling", async () => {
119
+ it("activates session with ctx", async () => {
125
120
  const ctx = makeCtx();
126
- const deps = makeDeps();
127
- await handleInput(deps, makeInputEvent("hello"), ctx);
128
- expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
121
+ const { handler, session } = makeHandler();
122
+ await handler.handleInput(makeInputEvent("hello"), ctx);
123
+ expect(session.activate).toHaveBeenCalledWith(ctx);
129
124
  });
130
125
 
131
126
  it("returns continue for non-skill input", async () => {
132
- const deps = makeDeps();
133
- const result = await handleInput(
134
- deps,
127
+ const { handler } = makeHandler();
128
+ const result = await handler.handleInput(
135
129
  makeInputEvent("just a message"),
136
130
  makeCtx(),
137
131
  );
@@ -139,18 +133,14 @@ describe("handleInput", () => {
139
133
  });
140
134
 
141
135
  it("does not check permissions for non-skill input", async () => {
142
- const deps = makeDeps();
143
- await handleInput(deps, makeInputEvent("just a message"), makeCtx());
144
- expect(
145
- deps.session.permissionManager.checkPermission,
146
- ).not.toHaveBeenCalled();
136
+ const { handler, session } = makeHandler();
137
+ await handler.handleInput(makeInputEvent("just a message"), makeCtx());
138
+ expect(session.checkPermission).not.toHaveBeenCalled();
147
139
  });
148
140
 
149
141
  it("returns continue when skill is allowed", async () => {
150
- const deps = makeDeps();
151
- // default makeRuntime() has checkPermission → { state: "allow" }
152
- const result = await handleInput(
153
- deps,
142
+ const { handler } = makeHandler();
143
+ const result = await handler.handleInput(
154
144
  makeInputEvent("/skill:librarian"),
155
145
  makeCtx(),
156
146
  );
@@ -158,16 +148,12 @@ describe("handleInput", () => {
158
148
  });
159
149
 
160
150
  it("returns handled when skill is denied", async () => {
161
- const pm = {
162
- checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
163
- };
164
- const deps = makeDeps({
165
- session: makeSession({
166
- permissionManager: pm as unknown as SessionState["permissionManager"],
167
- }),
151
+ const { handler } = makeHandler({
152
+ session: {
153
+ checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
154
+ },
168
155
  });
169
- const result = await handleInput(
170
- deps,
156
+ const result = await handler.handleInput(
171
157
  makeInputEvent("/skill:librarian"),
172
158
  makeCtx(),
173
159
  );
@@ -176,15 +162,12 @@ describe("handleInput", () => {
176
162
 
177
163
  it("shows a warning notification when skill is denied and UI is available", async () => {
178
164
  const ctx = makeCtx({ hasUI: true });
179
- const pm = {
180
- checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
181
- };
182
- const deps = makeDeps({
183
- session: makeSession({
184
- permissionManager: pm as unknown as SessionState["permissionManager"],
185
- }),
165
+ const { handler } = makeHandler({
166
+ session: {
167
+ checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
168
+ },
186
169
  });
187
- await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
170
+ await handler.handleInput(makeInputEvent("/skill:librarian"), ctx);
188
171
  expect(ctx.ui.notify).toHaveBeenCalledWith(
189
172
  expect.stringContaining("librarian"),
190
173
  "warning",
@@ -193,26 +176,23 @@ describe("handleInput", () => {
193
176
 
194
177
  it("does not show a warning notification when skill is denied and UI is absent", async () => {
195
178
  const ctx = makeCtx({ hasUI: false });
196
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "deny" }) };
197
- const deps = makeDeps({
198
- session: makeSession({
199
- permissionManager: pm as unknown as SessionState["permissionManager"],
200
- }),
179
+ const { handler } = makeHandler({
180
+ session: {
181
+ checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
182
+ },
201
183
  });
202
- await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
184
+ await handler.handleInput(makeInputEvent("/skill:librarian"), ctx);
203
185
  expect(ctx.ui.notify).not.toHaveBeenCalled();
204
186
  });
205
187
 
206
188
  it("returns handled when skill requires approval but no UI is available", async () => {
207
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
208
- const deps = makeDeps({
209
- session: makeSession({
210
- permissionManager: pm as unknown as SessionState["permissionManager"],
211
- }),
212
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
189
+ const { handler } = makeHandler({
190
+ session: {
191
+ checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
192
+ canPrompt: vi.fn().mockReturnValue(false),
193
+ },
213
194
  });
214
- const result = await handleInput(
215
- deps,
195
+ const result = await handler.handleInput(
216
196
  makeInputEvent("/skill:librarian"),
217
197
  makeCtx(),
218
198
  );
@@ -220,38 +200,30 @@ describe("handleInput", () => {
220
200
  });
221
201
 
222
202
  it("prompts and returns continue when skill ask is approved", async () => {
223
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
224
- const deps = makeDeps({
225
- session: makeSession({
226
- permissionManager: pm as unknown as SessionState["permissionManager"],
227
- }),
228
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
229
- promptPermission: vi
230
- .fn()
231
- .mockResolvedValue({ approved: true, state: "approved" }),
203
+ const { handler, session } = makeHandler({
204
+ session: {
205
+ checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
206
+ prompt: vi
207
+ .fn()
208
+ .mockResolvedValue({ approved: true, state: "approved" }),
209
+ },
232
210
  });
233
- const result = await handleInput(
234
- deps,
211
+ const result = await handler.handleInput(
235
212
  makeInputEvent("/skill:librarian"),
236
213
  makeCtx(),
237
214
  );
238
215
  expect(result).toEqual({ action: "continue" });
239
- expect(deps.promptPermission).toHaveBeenCalledOnce();
216
+ expect(session.prompt).toHaveBeenCalledOnce();
240
217
  });
241
218
 
242
219
  it("returns handled when skill ask is denied by user", async () => {
243
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
244
- const deps = makeDeps({
245
- session: makeSession({
246
- permissionManager: pm as unknown as SessionState["permissionManager"],
247
- }),
248
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
249
- promptPermission: vi
250
- .fn()
251
- .mockResolvedValue({ approved: false, state: "denied" }),
220
+ const { handler } = makeHandler({
221
+ session: {
222
+ checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
223
+ prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
224
+ },
252
225
  });
253
- const result = await handleInput(
254
- deps,
226
+ const result = await handler.handleInput(
255
227
  makeInputEvent("/skill:librarian"),
256
228
  makeCtx(),
257
229
  );
@@ -259,19 +231,17 @@ describe("handleInput", () => {
259
231
  });
260
232
 
261
233
  it("passes agentName in the prompt permission request", async () => {
262
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
263
- const deps = makeDeps({
264
- session: makeSession({
265
- permissionManager: pm as unknown as SessionState["permissionManager"],
266
- }),
267
- resolveAgentName: vi.fn().mockReturnValue("code-agent"),
268
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
269
- promptPermission: vi
270
- .fn()
271
- .mockResolvedValue({ approved: true, state: "approved" }),
234
+ const { handler, session } = makeHandler({
235
+ session: {
236
+ checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
237
+ resolveAgentName: vi.fn().mockReturnValue("code-agent"),
238
+ prompt: vi
239
+ .fn()
240
+ .mockResolvedValue({ approved: true, state: "approved" }),
241
+ },
272
242
  });
273
- await handleInput(deps, makeInputEvent("/skill:librarian"), makeCtx());
274
- expect(deps.promptPermission).toHaveBeenCalledWith(
243
+ await handler.handleInput(makeInputEvent("/skill:librarian"), makeCtx());
244
+ expect(session.prompt).toHaveBeenCalledWith(
275
245
  expect.anything(),
276
246
  expect.objectContaining({
277
247
  agentName: "code-agent",