@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.
@@ -3,10 +3,10 @@ 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";
6
+ PermissionGateHandler,
7
+ } from "../../src/handlers/permission-gate-handler";
9
8
  import type { PermissionSession } from "../../src/permission-session";
9
+ import type { ToolRegistry } from "../../src/tool-registry";
10
10
  import type { PermissionState } from "../../src/types";
11
11
 
12
12
  // ── helpers ────────────────────────────────────────────────────────────────
@@ -45,26 +45,40 @@ function makeSession(
45
45
  getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
46
46
  getSessionRuleset: vi.fn().mockReturnValue([]),
47
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"),
48
51
  ...overrides,
49
52
  } as unknown as PermissionSession;
50
53
  }
51
54
 
52
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
55
+ function makeEvents() {
53
56
  return {
54
- session: makeSession(),
55
- events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
56
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
57
- promptPermission: vi
58
- .fn()
59
- .mockResolvedValue({ approved: true, state: "approved" }),
60
- createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
61
- stopPermissionRpcHandlers: vi.fn(),
62
- getAllTools: vi.fn().mockReturnValue([]),
63
- setActiveTools: vi.fn(),
64
- ...overrides,
57
+ emit: vi.fn(),
58
+ on: vi.fn().mockReturnValue(() => undefined),
59
+ };
60
+ }
61
+
62
+ function makeToolRegistry(): ToolRegistry {
63
+ return {
64
+ getAll: vi.fn().mockReturnValue([]),
65
+ setActive: vi.fn(),
65
66
  };
66
67
  }
67
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
+
68
82
  // ── extractSkillNameFromInput ──────────────────────────────────────────────
69
83
 
70
84
  describe("extractSkillNameFromInput", () => {
@@ -104,15 +118,14 @@ describe("extractSkillNameFromInput", () => {
104
118
  describe("handleInput", () => {
105
119
  it("activates session with ctx", async () => {
106
120
  const ctx = makeCtx();
107
- const deps = makeDeps();
108
- await handleInput(deps, makeInputEvent("hello"), ctx);
109
- expect(deps.session.activate).toHaveBeenCalledWith(ctx);
121
+ const { handler, session } = makeHandler();
122
+ await handler.handleInput(makeInputEvent("hello"), ctx);
123
+ expect(session.activate).toHaveBeenCalledWith(ctx);
110
124
  });
111
125
 
112
126
  it("returns continue for non-skill input", async () => {
113
- const deps = makeDeps();
114
- const result = await handleInput(
115
- deps,
127
+ const { handler } = makeHandler();
128
+ const result = await handler.handleInput(
116
129
  makeInputEvent("just a message"),
117
130
  makeCtx(),
118
131
  );
@@ -120,15 +133,14 @@ describe("handleInput", () => {
120
133
  });
121
134
 
122
135
  it("does not check permissions for non-skill input", async () => {
123
- const deps = makeDeps();
124
- await handleInput(deps, makeInputEvent("just a message"), makeCtx());
125
- expect(deps.session.checkPermission).not.toHaveBeenCalled();
136
+ const { handler, session } = makeHandler();
137
+ await handler.handleInput(makeInputEvent("just a message"), makeCtx());
138
+ expect(session.checkPermission).not.toHaveBeenCalled();
126
139
  });
127
140
 
128
141
  it("returns continue when skill is allowed", async () => {
129
- const deps = makeDeps();
130
- const result = await handleInput(
131
- deps,
142
+ const { handler } = makeHandler();
143
+ const result = await handler.handleInput(
132
144
  makeInputEvent("/skill:librarian"),
133
145
  makeCtx(),
134
146
  );
@@ -136,12 +148,12 @@ describe("handleInput", () => {
136
148
  });
137
149
 
138
150
  it("returns handled when skill is denied", async () => {
139
- const session = makeSession({
140
- checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
151
+ const { handler } = makeHandler({
152
+ session: {
153
+ checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
154
+ },
141
155
  });
142
- const deps = makeDeps({ session });
143
- const result = await handleInput(
144
- deps,
156
+ const result = await handler.handleInput(
145
157
  makeInputEvent("/skill:librarian"),
146
158
  makeCtx(),
147
159
  );
@@ -150,11 +162,12 @@ describe("handleInput", () => {
150
162
 
151
163
  it("shows a warning notification when skill is denied and UI is available", async () => {
152
164
  const ctx = makeCtx({ hasUI: true });
153
- const session = makeSession({
154
- checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
165
+ const { handler } = makeHandler({
166
+ session: {
167
+ checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
168
+ },
155
169
  });
156
- const deps = makeDeps({ session });
157
- await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
170
+ await handler.handleInput(makeInputEvent("/skill:librarian"), ctx);
158
171
  expect(ctx.ui.notify).toHaveBeenCalledWith(
159
172
  expect.stringContaining("librarian"),
160
173
  "warning",
@@ -163,24 +176,23 @@ describe("handleInput", () => {
163
176
 
164
177
  it("does not show a warning notification when skill is denied and UI is absent", async () => {
165
178
  const ctx = makeCtx({ hasUI: false });
166
- const session = makeSession({
167
- checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
179
+ const { handler } = makeHandler({
180
+ session: {
181
+ checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
182
+ },
168
183
  });
169
- const deps = makeDeps({ session });
170
- await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
184
+ await handler.handleInput(makeInputEvent("/skill:librarian"), ctx);
171
185
  expect(ctx.ui.notify).not.toHaveBeenCalled();
172
186
  });
173
187
 
174
188
  it("returns handled when skill requires approval but no UI is available", async () => {
175
- const session = makeSession({
176
- checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
177
- });
178
- const deps = makeDeps({
179
- session,
180
- 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
+ },
181
194
  });
182
- const result = await handleInput(
183
- deps,
195
+ const result = await handler.handleInput(
184
196
  makeInputEvent("/skill:librarian"),
185
197
  makeCtx(),
186
198
  );
@@ -188,38 +200,30 @@ describe("handleInput", () => {
188
200
  });
189
201
 
190
202
  it("prompts and returns continue when skill ask is approved", async () => {
191
- const session = makeSession({
192
- checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
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
+ },
193
210
  });
194
- const deps = makeDeps({
195
- session,
196
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
197
- promptPermission: vi
198
- .fn()
199
- .mockResolvedValue({ approved: true, state: "approved" }),
200
- });
201
- const result = await handleInput(
202
- deps,
211
+ const result = await handler.handleInput(
203
212
  makeInputEvent("/skill:librarian"),
204
213
  makeCtx(),
205
214
  );
206
215
  expect(result).toEqual({ action: "continue" });
207
- expect(deps.promptPermission).toHaveBeenCalledOnce();
216
+ expect(session.prompt).toHaveBeenCalledOnce();
208
217
  });
209
218
 
210
219
  it("returns handled when skill ask is denied by user", async () => {
211
- const session = makeSession({
212
- checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
213
- });
214
- const deps = makeDeps({
215
- session,
216
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
217
- promptPermission: vi
218
- .fn()
219
- .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
+ },
220
225
  });
221
- const result = await handleInput(
222
- deps,
226
+ const result = await handler.handleInput(
223
227
  makeInputEvent("/skill:librarian"),
224
228
  makeCtx(),
225
229
  );
@@ -227,19 +231,17 @@ describe("handleInput", () => {
227
231
  });
228
232
 
229
233
  it("passes agentName in the prompt permission request", async () => {
230
- const session = makeSession({
231
- checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
232
- resolveAgentName: vi.fn().mockReturnValue("code-agent"),
233
- });
234
- const deps = makeDeps({
235
- session,
236
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
237
- promptPermission: vi
238
- .fn()
239
- .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
+ },
240
242
  });
241
- await handleInput(deps, makeInputEvent("/skill:librarian"), makeCtx());
242
- expect(deps.promptPermission).toHaveBeenCalledWith(
243
+ await handler.handleInput(makeInputEvent("/skill:librarian"), makeCtx());
244
+ expect(session.prompt).toHaveBeenCalledWith(
243
245
  expect.anything(),
244
246
  expect.objectContaining({
245
247
  agentName: "code-agent",
@@ -1,11 +1,6 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import {
4
- handleResourcesDiscover,
5
- handleSessionShutdown,
6
- handleSessionStart,
7
- } from "../../src/handlers/lifecycle";
8
- import type { HandlerDeps } from "../../src/handlers/types";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { SessionLifecycleHandler } from "../../src/handlers/lifecycle";
9
4
  import type { PermissionSession } from "../../src/permission-session";
10
5
 
11
6
  // ── status stub ────────────────────────────────────────────────────────────
@@ -53,20 +48,17 @@ function makeSession(
53
48
  } as unknown as PermissionSession;
54
49
  }
55
50
 
56
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
57
- return {
58
- session: makeSession(),
59
- events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
60
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
61
- promptPermission: vi
62
- .fn()
63
- .mockResolvedValue({ approved: true, state: "approved" }),
64
- createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
65
- stopPermissionRpcHandlers: vi.fn(),
66
- getAllTools: vi.fn().mockReturnValue([]),
67
- setActiveTools: vi.fn(),
68
- ...overrides,
69
- };
51
+ function makeHandler(
52
+ overrides?: Partial<Record<keyof PermissionSession, unknown>>,
53
+ ): {
54
+ handler: SessionLifecycleHandler;
55
+ session: PermissionSession;
56
+ cleanupRpc: ReturnType<typeof vi.fn>;
57
+ } {
58
+ const session = makeSession(overrides);
59
+ const cleanupRpc = vi.fn();
60
+ const handler = new SessionLifecycleHandler(session, cleanupRpc);
61
+ return { handler, session, cleanupRpc };
70
62
  }
71
63
 
72
64
  // ── handleSessionStart ─────────────────────────────────────────────────────
@@ -74,52 +66,51 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
74
66
  describe("handleSessionStart", () => {
75
67
  it("refreshes config with ctx", async () => {
76
68
  const ctx = makeCtx();
77
- const deps = makeDeps();
78
- await handleSessionStart(deps, { reason: "startup" }, ctx);
79
- expect(deps.session.refreshConfig).toHaveBeenCalledWith(ctx);
69
+ const { handler, session } = makeHandler();
70
+ await handler.handleSessionStart({ reason: "startup" }, ctx);
71
+ expect(session.refreshConfig).toHaveBeenCalledWith(ctx);
80
72
  });
81
73
 
82
74
  it("calls resetForNewSession with ctx", async () => {
83
75
  const ctx = makeCtx();
84
- const deps = makeDeps();
85
- await handleSessionStart(deps, { reason: "startup" }, ctx);
86
- expect(deps.session.resetForNewSession).toHaveBeenCalledWith(ctx);
76
+ const { handler, session } = makeHandler();
77
+ await handler.handleSessionStart({ reason: "startup" }, ctx);
78
+ expect(session.resetForNewSession).toHaveBeenCalledWith(ctx);
87
79
  });
88
80
 
89
81
  it("logs resolved config paths", async () => {
90
- const deps = makeDeps();
91
- await handleSessionStart(deps, { reason: "startup" }, makeCtx());
92
- expect(deps.session.logResolvedConfigPaths).toHaveBeenCalledOnce();
82
+ const { handler, session } = makeHandler();
83
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
84
+ expect(session.logResolvedConfigPaths).toHaveBeenCalledOnce();
93
85
  });
94
86
 
95
87
  it("resolves agent name from ctx", async () => {
96
88
  const ctx = makeCtx();
97
- const deps = makeDeps();
98
- await handleSessionStart(deps, { reason: "startup" }, ctx);
99
- expect(deps.session.resolveAgentName).toHaveBeenCalledWith(ctx);
89
+ const { handler, session } = makeHandler();
90
+ await handler.handleSessionStart({ reason: "startup" }, ctx);
91
+ expect(session.resolveAgentName).toHaveBeenCalledWith(ctx);
100
92
  });
101
93
 
102
94
  it("notifies each policy issue", async () => {
103
- const session = makeSession({
95
+ const { handler, session } = makeHandler({
104
96
  getConfigIssues: vi.fn().mockReturnValue(["issue A", "issue B"]),
105
97
  });
106
- const deps = makeDeps({ session });
107
- await handleSessionStart(deps, { reason: "startup" }, makeCtx());
98
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
108
99
  expect(session.logger.warn).toHaveBeenCalledWith("issue A");
109
100
  expect(session.logger.warn).toHaveBeenCalledWith("issue B");
110
101
  });
111
102
 
112
103
  it("does not warn when there are no policy issues", async () => {
113
- const deps = makeDeps();
114
- await handleSessionStart(deps, { reason: "startup" }, makeCtx());
115
- expect(deps.session.logger.warn).not.toHaveBeenCalled();
104
+ const { handler, session } = makeHandler();
105
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
106
+ expect(session.logger.warn).not.toHaveBeenCalled();
116
107
  });
117
108
 
118
109
  it("writes lifecycle.reload debug log when reason is reload", async () => {
119
110
  const ctx = makeCtx({ cwd: "/proj" });
120
- const deps = makeDeps();
121
- await handleSessionStart(deps, { reason: "reload" }, ctx);
122
- expect(deps.session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
111
+ const { handler, session } = makeHandler();
112
+ await handler.handleSessionStart({ reason: "reload" }, ctx);
113
+ expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
123
114
  triggeredBy: "session_start",
124
115
  reason: "reload",
125
116
  cwd: "/proj",
@@ -127,19 +118,18 @@ describe("handleSessionStart", () => {
127
118
  });
128
119
 
129
120
  it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
130
- const deps = makeDeps();
131
- await handleSessionStart(deps, { reason: "startup" }, makeCtx());
132
- expect(deps.session.logger.debug).not.toHaveBeenCalled();
121
+ const { handler, session } = makeHandler();
122
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
123
+ expect(session.logger.debug).not.toHaveBeenCalled();
133
124
  });
134
125
 
135
126
  it("calls refreshConfig before resetForNewSession", async () => {
136
127
  const callOrder: string[] = [];
137
- const session = makeSession({
128
+ const { handler } = makeHandler({
138
129
  refreshConfig: vi.fn(() => callOrder.push("refreshConfig")),
139
130
  resetForNewSession: vi.fn(() => callOrder.push("resetForNewSession")),
140
131
  });
141
- const deps = makeDeps({ session });
142
- await handleSessionStart(deps, { reason: "startup" }, makeCtx());
132
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
143
133
  expect(callOrder).toEqual(["refreshConfig", "resetForNewSession"]);
144
134
  });
145
135
  });
@@ -148,24 +138,23 @@ describe("handleSessionStart", () => {
148
138
 
149
139
  describe("handleResourcesDiscover", () => {
150
140
  it("does nothing when reason is not reload", async () => {
151
- const deps = makeDeps();
152
- await handleResourcesDiscover(deps, { reason: "startup" });
153
- expect(deps.session.reload).not.toHaveBeenCalled();
141
+ const { handler, session } = makeHandler();
142
+ await handler.handleResourcesDiscover({ reason: "startup" });
143
+ expect(session.reload).not.toHaveBeenCalled();
154
144
  });
155
145
 
156
146
  it("calls reload on the session on reload", async () => {
157
- const deps = makeDeps();
158
- await handleResourcesDiscover(deps, { reason: "reload" });
159
- expect(deps.session.reload).toHaveBeenCalledOnce();
147
+ const { handler, session } = makeHandler();
148
+ await handler.handleResourcesDiscover({ reason: "reload" });
149
+ expect(session.reload).toHaveBeenCalledOnce();
160
150
  });
161
151
 
162
152
  it("writes lifecycle.reload debug log on reload", async () => {
163
153
  const ctx = makeCtx({ cwd: "/proj" });
164
- const session = makeSession({
154
+ const { handler, session } = makeHandler({
165
155
  getRuntimeContext: vi.fn().mockReturnValue(ctx),
166
156
  });
167
- const deps = makeDeps({ session });
168
- await handleResourcesDiscover(deps, { reason: "reload" });
157
+ await handler.handleResourcesDiscover({ reason: "reload" });
169
158
  expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
170
159
  triggeredBy: "resources_discover",
171
160
  reason: "reload",
@@ -174,9 +163,9 @@ describe("handleResourcesDiscover", () => {
174
163
  });
175
164
 
176
165
  it("logs cwd as null when runtimeContext is null on reload", async () => {
177
- const deps = makeDeps();
178
- await handleResourcesDiscover(deps, { reason: "reload" });
179
- expect(deps.session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
166
+ const { handler, session } = makeHandler();
167
+ await handler.handleResourcesDiscover({ reason: "reload" });
168
+ expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
180
169
  triggeredBy: "resources_discover",
181
170
  reason: "reload",
182
171
  cwd: null,
@@ -189,11 +178,10 @@ describe("handleResourcesDiscover", () => {
189
178
  describe("handleSessionShutdown", () => {
190
179
  it("clears UI status when runtime context is present", async () => {
191
180
  const ctx = makeCtx();
192
- const session = makeSession({
181
+ const { handler } = makeHandler({
193
182
  getRuntimeContext: vi.fn().mockReturnValue(ctx),
194
183
  });
195
- const deps = makeDeps({ session });
196
- await handleSessionShutdown(deps);
184
+ await handler.handleSessionShutdown();
197
185
  expect(ctx.ui.setStatus).toHaveBeenCalledWith(
198
186
  "permission-system",
199
187
  undefined,
@@ -201,19 +189,19 @@ describe("handleSessionShutdown", () => {
201
189
  });
202
190
 
203
191
  it("does not throw when runtime context is null", async () => {
204
- const deps = makeDeps();
205
- await expect(handleSessionShutdown(deps)).resolves.not.toThrow();
192
+ const { handler } = makeHandler();
193
+ await expect(handler.handleSessionShutdown()).resolves.not.toThrow();
206
194
  });
207
195
 
208
196
  it("calls shutdown on the session", async () => {
209
- const deps = makeDeps();
210
- await handleSessionShutdown(deps);
211
- expect(deps.session.shutdown).toHaveBeenCalledOnce();
197
+ const { handler, session } = makeHandler();
198
+ await handler.handleSessionShutdown();
199
+ expect(session.shutdown).toHaveBeenCalledOnce();
212
200
  });
213
201
 
214
- it("calls stopPermissionRpcHandlers", async () => {
215
- const deps = makeDeps();
216
- await handleSessionShutdown(deps);
217
- expect(deps.stopPermissionRpcHandlers).toHaveBeenCalledOnce();
202
+ it("calls cleanupRpc", async () => {
203
+ const { handler, cleanupRpc } = makeHandler();
204
+ await handler.handleSessionShutdown();
205
+ expect(cleanupRpc).toHaveBeenCalledOnce();
218
206
  });
219
207
  });