@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.
@@ -1,29 +1,9 @@
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";
9
- import type { PermissionManager } from "../../src/permission-manager";
10
- import type { SessionState } from "../../src/runtime";
11
- import type { SessionRules } from "../../src/session-rules";
12
- import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { SessionLifecycleHandler } from "../../src/handlers/lifecycle";
4
+ import type { PermissionSession } from "../../src/permission-session";
13
5
 
14
- // ── active-agent stub ──────────────────────────────────────────────────────
15
- const { mockGetActiveAgentName } = vi.hoisted(() => ({
16
- mockGetActiveAgentName: vi.fn<(ctx: ExtensionContext) => string | null>(),
17
- }));
18
-
19
- vi.mock("../../src/active-agent", () => ({
20
- getActiveAgentName: mockGetActiveAgentName,
21
- getActiveAgentNameFromSystemPrompt: vi.fn().mockReturnValue(null),
22
- }));
23
-
24
- // ── PERMISSION_SYSTEM_STATUS_KEY stub ──────────────────────────────────────
25
- // status.ts is re-exported through the handler; the key value doesn't matter
26
- // for these tests.
6
+ // ── status stub ────────────────────────────────────────────────────────────
27
7
  vi.mock("../../src/status", () => ({
28
8
  PERMISSION_SYSTEM_STATUS_KEY: "permission-system",
29
9
  syncPermissionSystemStatus: vi.fn(),
@@ -51,156 +31,86 @@ function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
51
31
  } as unknown as ExtensionContext;
52
32
  }
53
33
 
54
- function makePermissionManager(
55
- issues: string[] = [],
56
- ): Pick<PermissionManager, "getConfigIssues"> {
57
- return {
58
- getConfigIssues: vi.fn().mockReturnValue(issues),
59
- };
60
- }
61
-
62
- function makeSessionRules(): SessionRules {
63
- return {
64
- approve: vi.fn(),
65
- getRuleset: vi.fn().mockReturnValue([]),
66
- clear: vi.fn(),
67
- } as unknown as SessionRules;
68
- }
69
-
70
- function makeSession(overrides: Partial<SessionState> = {}): SessionState {
71
- return {
72
- runtimeContext: null,
73
- permissionManager: makePermissionManager() as unknown as PermissionManager,
74
- activeSkillEntries: [] as SkillPromptEntry[],
75
- lastKnownActiveAgentName: null,
76
- lastActiveToolsCacheKey: null,
77
- lastPromptStateCacheKey: null,
78
- sessionRules: makeSessionRules(),
79
- ...overrides,
80
- };
81
- }
82
-
83
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
34
+ function makeSession(
35
+ overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
36
+ ): PermissionSession {
84
37
  return {
85
- session: makeSession(),
86
38
  logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
87
- piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
88
- getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
89
- createPermissionManagerForCwd: vi
90
- .fn()
91
- .mockReturnValue(makePermissionManager()),
92
- refreshExtensionConfig: vi.fn(),
39
+ refreshConfig: vi.fn(),
40
+ resetForNewSession: vi.fn(),
93
41
  logResolvedConfigPaths: vi.fn(),
94
42
  resolveAgentName: vi.fn().mockReturnValue(null),
95
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
96
- promptPermission: vi
97
- .fn()
98
- .mockResolvedValue({ approved: true, state: "approved" }),
99
- createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
100
- events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
101
- forwarding: { start: vi.fn(), stop: vi.fn() },
102
- stopPermissionRpcHandlers: vi.fn(),
103
- getAllTools: vi.fn().mockReturnValue([]),
104
- setActiveTools: vi.fn(),
43
+ getConfigIssues: vi.fn().mockReturnValue([]),
44
+ reload: vi.fn(),
45
+ getRuntimeContext: vi.fn().mockReturnValue(null),
46
+ shutdown: vi.fn(),
105
47
  ...overrides,
106
- };
48
+ } as unknown as PermissionSession;
49
+ }
50
+
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 };
107
62
  }
108
63
 
109
64
  // ── handleSessionStart ─────────────────────────────────────────────────────
110
65
 
111
66
  describe("handleSessionStart", () => {
112
- beforeEach(() => {
113
- mockGetActiveAgentName.mockReset();
114
- mockGetActiveAgentName.mockReturnValue(null);
115
- });
116
-
117
- it("sets the runtime context", async () => {
118
- const ctx = makeCtx();
119
- const deps = makeDeps();
120
- await handleSessionStart(deps, { reason: "startup" }, ctx);
121
- expect(deps.session.runtimeContext).toBe(ctx);
122
- });
123
-
124
- it("refreshes extension config with ctx", async () => {
125
- const ctx = makeCtx();
126
- const deps = makeDeps();
127
- await handleSessionStart(deps, { reason: "startup" }, ctx);
128
- expect(deps.refreshExtensionConfig).toHaveBeenCalledWith(ctx);
129
- });
130
-
131
- it("creates a new permission manager for ctx.cwd and stores it", async () => {
132
- const ctx = makeCtx({ cwd: "/my/project" });
133
- const newPm = makePermissionManager();
134
- const deps = makeDeps({
135
- createPermissionManagerForCwd: vi.fn().mockReturnValue(newPm),
136
- });
137
- await handleSessionStart(deps, { reason: "startup" }, ctx);
138
- expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
139
- "/my/project",
140
- );
141
- expect(deps.session.permissionManager).toBe(newPm);
142
- });
143
-
144
- it("clears the before_agent_start cache", async () => {
145
- const ctx = makeCtx();
146
- const deps = makeDeps();
147
- await handleSessionStart(deps, { reason: "startup" }, ctx);
148
- expect(deps.session.activeSkillEntries).toEqual([]);
149
- expect(deps.session.lastActiveToolsCacheKey).toBeNull();
150
- expect(deps.session.lastPromptStateCacheKey).toBeNull();
151
- });
152
-
153
- it("sets lastKnownActiveAgentName from getActiveAgentName", async () => {
154
- mockGetActiveAgentName.mockReturnValue("my-agent");
67
+ it("refreshes config with ctx", async () => {
155
68
  const ctx = makeCtx();
156
- const deps = makeDeps();
157
- await handleSessionStart(deps, { reason: "startup" }, ctx);
158
- expect(deps.session.lastKnownActiveAgentName).toBe("my-agent");
69
+ const { handler, session } = makeHandler();
70
+ await handler.handleSessionStart({ reason: "startup" }, ctx);
71
+ expect(session.refreshConfig).toHaveBeenCalledWith(ctx);
159
72
  });
160
73
 
161
- it("sets lastKnownActiveAgentName to null when no agent is active", async () => {
162
- mockGetActiveAgentName.mockReturnValue(null);
74
+ it("calls resetForNewSession with ctx", async () => {
163
75
  const ctx = makeCtx();
164
- const deps = makeDeps();
165
- await handleSessionStart(deps, { reason: "startup" }, ctx);
166
- expect(deps.session.lastKnownActiveAgentName).toBeNull();
76
+ const { handler, session } = makeHandler();
77
+ await handler.handleSessionStart({ reason: "startup" }, ctx);
78
+ expect(session.resetForNewSession).toHaveBeenCalledWith(ctx);
167
79
  });
168
80
 
169
- it("starts forwarded permission polling", async () => {
170
- const ctx = makeCtx();
171
- const deps = makeDeps();
172
- await handleSessionStart(deps, { reason: "startup" }, ctx);
173
- expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
81
+ it("logs resolved config paths", async () => {
82
+ const { handler, session } = makeHandler();
83
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
84
+ expect(session.logResolvedConfigPaths).toHaveBeenCalledOnce();
174
85
  });
175
86
 
176
- it("logs resolved config paths", async () => {
87
+ it("resolves agent name from ctx", async () => {
177
88
  const ctx = makeCtx();
178
- const deps = makeDeps();
179
- await handleSessionStart(deps, { reason: "startup" }, ctx);
180
- expect(deps.logResolvedConfigPaths).toHaveBeenCalledOnce();
89
+ const { handler, session } = makeHandler();
90
+ await handler.handleSessionStart({ reason: "startup" }, ctx);
91
+ expect(session.resolveAgentName).toHaveBeenCalledWith(ctx);
181
92
  });
182
93
 
183
94
  it("notifies each policy issue", async () => {
184
- const pm = makePermissionManager(["issue A", "issue B"]);
185
- const deps = makeDeps({
186
- createPermissionManagerForCwd: vi.fn().mockReturnValue(pm),
95
+ const { handler, session } = makeHandler({
96
+ getConfigIssues: vi.fn().mockReturnValue(["issue A", "issue B"]),
187
97
  });
188
- await handleSessionStart(deps, { reason: "startup" }, makeCtx());
189
- expect(deps.logger.warn).toHaveBeenCalledWith("issue A");
190
- expect(deps.logger.warn).toHaveBeenCalledWith("issue B");
98
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
99
+ expect(session.logger.warn).toHaveBeenCalledWith("issue A");
100
+ expect(session.logger.warn).toHaveBeenCalledWith("issue B");
191
101
  });
192
102
 
193
- it("does not call notifyWarning when there are no policy issues", async () => {
194
- const deps = makeDeps();
195
- await handleSessionStart(deps, { reason: "startup" }, makeCtx());
196
- expect(deps.logger.warn).not.toHaveBeenCalled();
103
+ it("does not warn when there are no policy issues", async () => {
104
+ const { handler, session } = makeHandler();
105
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
106
+ expect(session.logger.warn).not.toHaveBeenCalled();
197
107
  });
198
108
 
199
109
  it("writes lifecycle.reload debug log when reason is reload", async () => {
200
110
  const ctx = makeCtx({ cwd: "/proj" });
201
- const deps = makeDeps();
202
- await handleSessionStart(deps, { reason: "reload" }, ctx);
203
- expect(deps.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", {
204
114
  triggeredBy: "session_start",
205
115
  reason: "reload",
206
116
  cwd: "/proj",
@@ -208,9 +118,19 @@ describe("handleSessionStart", () => {
208
118
  });
209
119
 
210
120
  it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
211
- const deps = makeDeps();
212
- await handleSessionStart(deps, { reason: "startup" }, makeCtx());
213
- expect(deps.logger.debug).not.toHaveBeenCalled();
121
+ const { handler, session } = makeHandler();
122
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
123
+ expect(session.logger.debug).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it("calls refreshConfig before resetForNewSession", async () => {
127
+ const callOrder: string[] = [];
128
+ const { handler } = makeHandler({
129
+ refreshConfig: vi.fn(() => callOrder.push("refreshConfig")),
130
+ resetForNewSession: vi.fn(() => callOrder.push("resetForNewSession")),
131
+ });
132
+ await handler.handleSessionStart({ reason: "startup" }, makeCtx());
133
+ expect(callOrder).toEqual(["refreshConfig", "resetForNewSession"]);
214
134
  });
215
135
  });
216
136
 
@@ -218,45 +138,24 @@ describe("handleSessionStart", () => {
218
138
 
219
139
  describe("handleResourcesDiscover", () => {
220
140
  it("does nothing when reason is not reload", async () => {
221
- const deps = makeDeps();
222
- await handleResourcesDiscover(deps, { reason: "startup" });
223
- expect(deps.createPermissionManagerForCwd).not.toHaveBeenCalled();
224
- expect(deps.logger.debug).not.toHaveBeenCalled();
225
- });
226
-
227
- it("creates and stores a new PM using runtimeContext.cwd on reload", async () => {
228
- const ctx = makeCtx({ cwd: "/runtime/cwd" });
229
- const newPm = makePermissionManager();
230
- const deps = makeDeps({
231
- session: makeSession({ runtimeContext: ctx }),
232
- createPermissionManagerForCwd: vi.fn().mockReturnValue(newPm),
233
- });
234
- await handleResourcesDiscover(deps, { reason: "reload" });
235
- expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
236
- "/runtime/cwd",
237
- );
238
- expect(deps.session.permissionManager).toBe(newPm);
141
+ const { handler, session } = makeHandler();
142
+ await handler.handleResourcesDiscover({ reason: "startup" });
143
+ expect(session.reload).not.toHaveBeenCalled();
239
144
  });
240
145
 
241
- it("uses undefined cwd when runtimeContext is null on reload", async () => {
242
- const deps = makeDeps();
243
- await handleResourcesDiscover(deps, { reason: "reload" });
244
- expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(undefined);
245
- });
246
-
247
- it("clears the before_agent_start cache on reload", async () => {
248
- const deps = makeDeps();
249
- await handleResourcesDiscover(deps, { reason: "reload" });
250
- expect(deps.session.activeSkillEntries).toEqual([]);
251
- expect(deps.session.lastActiveToolsCacheKey).toBeNull();
252
- expect(deps.session.lastPromptStateCacheKey).toBeNull();
146
+ it("calls reload on the session on reload", async () => {
147
+ const { handler, session } = makeHandler();
148
+ await handler.handleResourcesDiscover({ reason: "reload" });
149
+ expect(session.reload).toHaveBeenCalledOnce();
253
150
  });
254
151
 
255
152
  it("writes lifecycle.reload debug log on reload", async () => {
256
153
  const ctx = makeCtx({ cwd: "/proj" });
257
- const deps = makeDeps({ session: makeSession({ runtimeContext: ctx }) });
258
- await handleResourcesDiscover(deps, { reason: "reload" });
259
- expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
154
+ const { handler, session } = makeHandler({
155
+ getRuntimeContext: vi.fn().mockReturnValue(ctx),
156
+ });
157
+ await handler.handleResourcesDiscover({ reason: "reload" });
158
+ expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
260
159
  triggeredBy: "resources_discover",
261
160
  reason: "reload",
262
161
  cwd: "/proj",
@@ -264,9 +163,9 @@ describe("handleResourcesDiscover", () => {
264
163
  });
265
164
 
266
165
  it("logs cwd as null when runtimeContext is null on reload", async () => {
267
- const deps = makeDeps();
268
- await handleResourcesDiscover(deps, { reason: "reload" });
269
- expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
166
+ const { handler, session } = makeHandler();
167
+ await handler.handleResourcesDiscover({ reason: "reload" });
168
+ expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
270
169
  triggeredBy: "resources_discover",
271
170
  reason: "reload",
272
171
  cwd: null,
@@ -277,12 +176,12 @@ describe("handleResourcesDiscover", () => {
277
176
  // ── handleSessionShutdown ──────────────────────────────────────────────────
278
177
 
279
178
  describe("handleSessionShutdown", () => {
280
- it("clears the UI status when a runtime context is present", async () => {
179
+ it("clears UI status when runtime context is present", async () => {
281
180
  const ctx = makeCtx();
282
- const deps = makeDeps({
283
- session: makeSession({ runtimeContext: ctx }),
181
+ const { handler } = makeHandler({
182
+ getRuntimeContext: vi.fn().mockReturnValue(ctx),
284
183
  });
285
- await handleSessionShutdown(deps);
184
+ await handler.handleSessionShutdown();
286
185
  expect(ctx.ui.setStatus).toHaveBeenCalledWith(
287
186
  "permission-system",
288
187
  undefined,
@@ -290,48 +189,19 @@ describe("handleSessionShutdown", () => {
290
189
  });
291
190
 
292
191
  it("does not throw when runtime context is null", async () => {
293
- const deps = makeDeps();
294
- await expect(handleSessionShutdown(deps)).resolves.not.toThrow();
192
+ const { handler } = makeHandler();
193
+ await expect(handler.handleSessionShutdown()).resolves.not.toThrow();
295
194
  });
296
195
 
297
- it("sets runtime context to null", async () => {
298
- const ctx = makeCtx();
299
- const deps = makeDeps({ session: makeSession({ runtimeContext: ctx }) });
300
- await handleSessionShutdown(deps);
301
- expect(deps.session.runtimeContext).toBeNull();
196
+ it("calls shutdown on the session", async () => {
197
+ const { handler, session } = makeHandler();
198
+ await handler.handleSessionShutdown();
199
+ expect(session.shutdown).toHaveBeenCalledOnce();
302
200
  });
303
201
 
304
- it("clears the before_agent_start cache", async () => {
305
- const deps = makeDeps();
306
- await handleSessionShutdown(deps);
307
- expect(deps.session.activeSkillEntries).toEqual([]);
308
- expect(deps.session.lastActiveToolsCacheKey).toBeNull();
309
- expect(deps.session.lastPromptStateCacheKey).toBeNull();
310
- });
311
-
312
- it("clears the session rules", async () => {
313
- const deps = makeDeps();
314
- await handleSessionShutdown(deps);
315
- expect(deps.session.sessionRules.clear).toHaveBeenCalledOnce();
316
- });
317
-
318
- it("stops forwarded permission polling", async () => {
319
- const deps = makeDeps();
320
- await handleSessionShutdown(deps);
321
- expect(deps.forwarding.stop).toHaveBeenCalledOnce();
322
- });
323
-
324
- it("calls stopPermissionRpcHandlers on shutdown", async () => {
325
- const deps = makeDeps();
326
- await handleSessionShutdown(deps);
327
- expect(deps.stopPermissionRpcHandlers).toHaveBeenCalledOnce();
328
- });
329
-
330
- it("does not reset lastKnownActiveAgentName", async () => {
331
- const deps = makeDeps({
332
- session: makeSession({ lastKnownActiveAgentName: "remembered" }),
333
- });
334
- await handleSessionShutdown(deps);
335
- expect(deps.session.lastKnownActiveAgentName).toBe("remembered");
202
+ it("calls cleanupRpc", async () => {
203
+ const { handler, cleanupRpc } = makeHandler();
204
+ await handler.handleSessionShutdown();
205
+ expect(cleanupRpc).toHaveBeenCalledOnce();
336
206
  });
337
207
  });