@gotgenes/pi-permission-system 10.1.0 → 10.3.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,42 +3,34 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
 
4
4
  // ── Module mocks (hoisted) ─────────────────────────────────────────────────
5
5
 
6
- const {
7
- mockGetActiveAgentName,
8
- mockGetActiveAgentNameFromSystemPrompt,
9
- mockCreatePermissionManagerForCwd,
10
- } = vi.hoisted(() => ({
11
- mockGetActiveAgentName: vi.fn<(ctx: ExtensionContext) => string | null>(),
12
- mockGetActiveAgentNameFromSystemPrompt:
13
- vi.fn<(systemPrompt?: string) => string | null>(),
14
- mockCreatePermissionManagerForCwd: vi.fn(),
15
- }));
6
+ const { mockGetActiveAgentName, mockGetActiveAgentNameFromSystemPrompt } =
7
+ vi.hoisted(() => ({
8
+ mockGetActiveAgentName: vi.fn<(ctx: ExtensionContext) => string | null>(),
9
+ mockGetActiveAgentNameFromSystemPrompt:
10
+ vi.fn<(systemPrompt?: string) => string | null>(),
11
+ }));
16
12
 
17
13
  vi.mock("../src/active-agent", () => ({
18
14
  getActiveAgentName: mockGetActiveAgentName,
19
15
  getActiveAgentNameFromSystemPrompt: mockGetActiveAgentNameFromSystemPrompt,
20
16
  }));
21
17
 
22
- vi.mock("../src/runtime", async (importOriginal) => {
23
- const original = await importOriginal<typeof import("../src/runtime")>();
24
- return {
25
- ...original,
26
- createPermissionManagerForCwd: mockCreatePermissionManagerForCwd,
27
- };
28
- });
29
-
30
18
  // ── Test helpers ───────────────────────────────────────────────────────────
31
19
 
20
+ import type { SessionConfigStore } from "#src/config-store";
21
+ import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
32
22
  import type { ExtensionPaths } from "#src/extension-paths";
33
23
  import type { ForwardingController } from "#src/forwarding-manager";
34
- import type { PermissionManager } from "#src/permission-manager";
24
+ import type { ScopedPermissionManager } from "#src/permission-manager";
35
25
  import {
36
26
  PermissionSession,
37
27
  type PermissionSessionRuntimeDeps,
38
28
  } from "#src/permission-session";
29
+ import type { Ruleset } from "#src/rule";
39
30
  import { SessionApproval } from "#src/session-approval";
40
31
  import type { SessionLogger } from "#src/session-logger";
41
32
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
33
+ import type { PermissionCheckResult, PermissionState } from "#src/types";
42
34
  import { makeCtx } from "#test/helpers/handler-fixtures";
43
35
 
44
36
  function makeSkillEntry(
@@ -76,11 +68,22 @@ function makeLogger(): SessionLogger {
76
68
  };
77
69
  }
78
70
 
71
+ function makeConfigStore(
72
+ overrides: Partial<SessionConfigStore> = {},
73
+ ): SessionConfigStore {
74
+ return {
75
+ current:
76
+ overrides.current ??
77
+ vi
78
+ .fn<() => typeof DEFAULT_EXTENSION_CONFIG>()
79
+ .mockReturnValue({ ...DEFAULT_EXTENSION_CONFIG }),
80
+ refresh: overrides.refresh ?? vi.fn<(ctx?: ExtensionContext) => void>(),
81
+ logResolvedPaths: overrides.logResolvedPaths ?? vi.fn<() => void>(),
82
+ };
83
+ }
84
+
79
85
  function makeRuntimeDeps(): PermissionSessionRuntimeDeps {
80
86
  return {
81
- refreshExtensionConfig: vi.fn(),
82
- logResolvedConfigPaths: vi.fn(),
83
- getConfig: vi.fn().mockReturnValue({}),
84
87
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
85
88
  promptPermission: vi
86
89
  .fn()
@@ -95,43 +98,63 @@ function makeForwarding(): ForwardingController {
95
98
  };
96
99
  }
97
100
 
98
- function makePermissionManager(
99
- overrides: Partial<PermissionManager> = {},
100
- ): PermissionManager {
101
+ function makePermissionManager() {
101
102
  return {
102
- checkPermission: vi.fn().mockReturnValue({
103
- state: "allow",
104
- toolName: "read",
105
- source: "tool",
106
- origin: "builtin",
107
- }),
108
- getToolPermission: vi.fn().mockReturnValue("allow"),
109
- getConfigIssues: vi.fn().mockReturnValue([]),
110
- getPolicyCacheStamp: vi.fn().mockReturnValue("stamp-1"),
111
- getComposedConfigRules: vi.fn().mockReturnValue([]),
112
- getResolvedPolicyPaths: vi.fn().mockReturnValue({}),
113
- ...overrides,
114
- } as unknown as PermissionManager;
103
+ configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
104
+ checkPermission: vi
105
+ .fn<
106
+ (
107
+ toolName: string,
108
+ input: unknown,
109
+ agentName?: string,
110
+ sessionRules?: Ruleset,
111
+ ) => PermissionCheckResult
112
+ >()
113
+ .mockReturnValue({
114
+ state: "allow",
115
+ toolName: "read",
116
+ source: "tool",
117
+ origin: "builtin",
118
+ }),
119
+ getToolPermission: vi
120
+ .fn<(toolName: string, agentName?: string) => PermissionState>()
121
+ .mockReturnValue("allow"),
122
+ getConfigIssues: vi.fn((): string[] => []),
123
+ getPolicyCacheStamp: vi.fn((): string => "stamp-1"),
124
+ };
115
125
  }
116
126
 
117
127
  function createSession(overrides?: {
118
128
  paths?: Partial<ExtensionPaths>;
119
129
  logger?: SessionLogger;
120
130
  forwarding?: ForwardingController;
131
+ permissionManager?: ScopedPermissionManager;
132
+ configStore?: SessionConfigStore;
121
133
  runtimeDeps?: PermissionSessionRuntimeDeps;
122
134
  }): {
123
135
  session: PermissionSession;
124
136
  paths: ExtensionPaths;
125
137
  logger: SessionLogger;
126
138
  forwarding: ForwardingController;
139
+ configStore: SessionConfigStore;
127
140
  runtimeDeps: PermissionSessionRuntimeDeps;
128
141
  } {
129
142
  const paths = makePaths(overrides?.paths);
130
143
  const logger = overrides?.logger ?? makeLogger();
131
144
  const forwarding = overrides?.forwarding ?? makeForwarding();
145
+ const permissionManager =
146
+ overrides?.permissionManager ?? makePermissionManager();
147
+ const configStore = overrides?.configStore ?? makeConfigStore();
132
148
  const runtimeDeps = overrides?.runtimeDeps ?? makeRuntimeDeps();
133
- const session = new PermissionSession(paths, logger, forwarding, runtimeDeps);
134
- return { session, paths, logger, forwarding, runtimeDeps };
149
+ const session = new PermissionSession(
150
+ paths,
151
+ logger,
152
+ forwarding,
153
+ permissionManager,
154
+ configStore,
155
+ runtimeDeps,
156
+ );
157
+ return { session, paths, logger, forwarding, configStore, runtimeDeps };
135
158
  }
136
159
 
137
160
  // ── Tests ──────────────────────────────────────────────────────────────────
@@ -139,10 +162,6 @@ function createSession(overrides?: {
139
162
  beforeEach(() => {
140
163
  mockGetActiveAgentName.mockReset();
141
164
  mockGetActiveAgentNameFromSystemPrompt.mockReset();
142
- mockCreatePermissionManagerForCwd.mockReset();
143
-
144
- // Default: createPermissionManagerForCwd returns a fresh mock PM
145
- mockCreatePermissionManagerForCwd.mockReturnValue(makePermissionManager());
146
165
  mockGetActiveAgentName.mockReturnValue(null);
147
166
  mockGetActiveAgentNameFromSystemPrompt.mockReturnValue(null);
148
167
  });
@@ -151,8 +170,7 @@ describe("PermissionSession", () => {
151
170
  describe("constructor and delegation", () => {
152
171
  it("delegates checkPermission to internal PermissionManager", () => {
153
172
  const pm = makePermissionManager();
154
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
155
- const { session } = createSession();
173
+ const { session } = createSession({ permissionManager: pm });
156
174
 
157
175
  const result = session.checkPermission("bash", { command: "ls" });
158
176
 
@@ -167,8 +185,7 @@ describe("PermissionSession", () => {
167
185
 
168
186
  it("delegates getToolPermission to internal PermissionManager", () => {
169
187
  const pm = makePermissionManager();
170
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
171
- const { session } = createSession();
188
+ const { session } = createSession({ permissionManager: pm });
172
189
 
173
190
  const result = session.getToolPermission("read");
174
191
 
@@ -177,11 +194,9 @@ describe("PermissionSession", () => {
177
194
  });
178
195
 
179
196
  it("delegates getConfigIssues to internal PermissionManager", () => {
180
- const pm = makePermissionManager({
181
- getConfigIssues: vi.fn().mockReturnValue(["issue1"]),
182
- });
183
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
184
- const { session } = createSession();
197
+ const pm = makePermissionManager();
198
+ vi.mocked(pm.getConfigIssues).mockReturnValue(["issue1"]);
199
+ const { session } = createSession({ permissionManager: pm });
185
200
 
186
201
  expect(session.getConfigIssues("agent1")).toEqual(["issue1"]);
187
202
  expect(pm.getConfigIssues).toHaveBeenCalledWith("agent1");
@@ -189,8 +204,7 @@ describe("PermissionSession", () => {
189
204
 
190
205
  it("delegates getPolicyCacheStamp to internal PermissionManager", () => {
191
206
  const pm = makePermissionManager();
192
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
193
- const { session } = createSession();
207
+ const { session } = createSession({ permissionManager: pm });
194
208
 
195
209
  expect(session.getPolicyCacheStamp("agent1")).toBe("stamp-1");
196
210
  expect(pm.getPolicyCacheStamp).toHaveBeenCalledWith("agent1");
@@ -220,8 +234,7 @@ describe("PermissionSession", () => {
220
234
  describe("resolve", () => {
221
235
  it("forwards surface, input, and agentName, applying the empty session ruleset", () => {
222
236
  const pm = makePermissionManager();
223
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
224
- const { session } = createSession();
237
+ const { session } = createSession({ permissionManager: pm });
225
238
 
226
239
  session.resolve("bash", { command: "ls" }, "agent-x");
227
240
 
@@ -235,8 +248,7 @@ describe("PermissionSession", () => {
235
248
 
236
249
  it("defaults agentName to undefined when omitted", () => {
237
250
  const pm = makePermissionManager();
238
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
239
- const { session } = createSession();
251
+ const { session } = createSession({ permissionManager: pm });
240
252
 
241
253
  session.resolve("read", { path: ".env" });
242
254
 
@@ -250,8 +262,7 @@ describe("PermissionSession", () => {
250
262
 
251
263
  it("applies a recorded session approval on the next resolve", () => {
252
264
  const pm = makePermissionManager();
253
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
254
- const { session } = createSession();
265
+ const { session } = createSession({ permissionManager: pm });
255
266
 
256
267
  session.recordSessionApproval(SessionApproval.single("bash", "git *"));
257
268
  session.resolve("bash", { command: "git status" });
@@ -266,17 +277,15 @@ describe("PermissionSession", () => {
266
277
  });
267
278
 
268
279
  it("returns the PermissionManager's check result", () => {
269
- const pm = makePermissionManager({
270
- checkPermission: vi.fn().mockReturnValue({
271
- state: "deny",
272
- toolName: "bash",
273
- source: "bash",
274
- origin: "global",
275
- matchedPattern: "rm *",
276
- }),
280
+ const pm = makePermissionManager();
281
+ vi.mocked(pm.checkPermission).mockReturnValue({
282
+ state: "deny",
283
+ toolName: "bash",
284
+ source: "bash",
285
+ origin: "global",
286
+ matchedPattern: "rm *",
277
287
  });
278
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
279
- const { session } = createSession();
288
+ const { session } = createSession({ permissionManager: pm });
280
289
 
281
290
  const result = session.resolve("bash", { command: "rm -rf /" });
282
291
 
@@ -310,28 +319,14 @@ describe("PermissionSession", () => {
310
319
  });
311
320
 
312
321
  describe("resetForNewSession", () => {
313
- it("creates a new PermissionManager for the context cwd", () => {
314
- const pm2 = makePermissionManager({
315
- checkPermission: vi.fn().mockReturnValue({
316
- state: "deny",
317
- toolName: "bash",
318
- source: "bash",
319
- origin: "global",
320
- }),
321
- });
322
- mockCreatePermissionManagerForCwd.mockReturnValue(pm2);
323
- const { session } = createSession();
322
+ it("configures the injected PermissionManager for the context cwd", () => {
323
+ const pm = makePermissionManager();
324
+ const { session } = createSession({ permissionManager: pm });
324
325
  const ctx = makeCtx({ cwd: "/new/project" });
325
326
 
326
327
  session.resetForNewSession(ctx);
327
328
 
328
- expect(mockCreatePermissionManagerForCwd).toHaveBeenCalledWith(
329
- "/test/agent",
330
- "/new/project",
331
- );
332
- // Verify the new PM is used for subsequent calls
333
- const result = session.checkPermission("bash", { command: "rm" });
334
- expect(result.state).toBe("deny");
329
+ expect(pm.configureForCwd).toHaveBeenCalledWith("/new/project");
335
330
  });
336
331
 
337
332
  it("clears cache keys", () => {
@@ -506,11 +501,12 @@ describe("PermissionSession", () => {
506
501
 
507
502
  describe("infrastructure paths", () => {
508
503
  it("getInfrastructureReadDirs combines piInfrastructureDirs and piInfrastructureReadPaths", () => {
509
- const runtimeDeps = makeRuntimeDeps();
510
- (runtimeDeps.getConfig as ReturnType<typeof vi.fn>).mockReturnValue({
511
- piInfrastructureReadPaths: ["/extra/path"],
504
+ const configStore = makeConfigStore({
505
+ current: vi.fn().mockReturnValue({
506
+ piInfrastructureReadPaths: ["/extra/path"],
507
+ }),
512
508
  });
513
- const { session } = createSession({ runtimeDeps });
509
+ const { session } = createSession({ configStore });
514
510
  expect(session.getInfrastructureReadDirs()).toEqual([
515
511
  "/test/agent",
516
512
  "/test/agent/git",
@@ -528,36 +524,36 @@ describe("PermissionSession", () => {
528
524
  });
529
525
 
530
526
  describe("config delegation", () => {
531
- it("refreshConfig delegates to runtimeDeps", () => {
532
- const { session, runtimeDeps } = createSession();
527
+ it("refreshConfig delegates to configStore.refresh", () => {
528
+ const { session, configStore } = createSession();
533
529
  const ctx = makeCtx();
534
530
  session.refreshConfig(ctx);
535
- expect(runtimeDeps.refreshExtensionConfig).toHaveBeenCalledWith(ctx);
531
+ expect(configStore.refresh).toHaveBeenCalledWith(ctx);
536
532
  });
537
533
 
538
- it("logResolvedConfigPaths delegates to runtimeDeps", () => {
539
- const { session, runtimeDeps } = createSession();
534
+ it("logResolvedConfigPaths delegates to configStore.logResolvedPaths", () => {
535
+ const { session, configStore } = createSession();
540
536
  session.logResolvedConfigPaths();
541
- expect(runtimeDeps.logResolvedConfigPaths).toHaveBeenCalled();
537
+ expect(configStore.logResolvedPaths).toHaveBeenCalled();
542
538
  });
543
539
 
544
- it("config getter delegates to runtimeDeps.getConfig", () => {
545
- const runtimeDeps = makeRuntimeDeps();
546
- const fakeConfig = { debugLog: true };
547
- (runtimeDeps.getConfig as ReturnType<typeof vi.fn>).mockReturnValue(
548
- fakeConfig,
549
- );
550
- const { session } = createSession({ runtimeDeps });
540
+ it("config getter delegates to configStore.current()", () => {
541
+ const fakeConfig = { debugLog: true } as typeof DEFAULT_EXTENSION_CONFIG;
542
+ const configStore = makeConfigStore({
543
+ current: vi.fn().mockReturnValue(fakeConfig),
544
+ });
545
+ const { session } = createSession({ configStore });
551
546
  expect(session.config).toBe(fakeConfig);
552
547
  });
553
548
 
554
549
  it("getToolPreviewLimits returns resolved preview limits from config", () => {
555
- const runtimeDeps = makeRuntimeDeps();
556
- (runtimeDeps.getConfig as ReturnType<typeof vi.fn>).mockReturnValue({
557
- toolInputPreviewMaxLength: 400,
558
- toolTextSummaryMaxLength: 120,
550
+ const configStore = makeConfigStore({
551
+ current: vi.fn().mockReturnValue({
552
+ toolInputPreviewMaxLength: 400,
553
+ toolTextSummaryMaxLength: 120,
554
+ }),
559
555
  });
560
- const { session } = createSession({ runtimeDeps });
556
+ const { session } = createSession({ configStore });
561
557
  const limits = session.getToolPreviewLimits();
562
558
  expect(limits.toolInputPreviewMaxLength).toBe(400);
563
559
  expect(limits.toolTextSummaryMaxLength).toBe(120);
@@ -573,20 +569,15 @@ describe("PermissionSession", () => {
573
569
  });
574
570
 
575
571
  describe("reload", () => {
576
- it("recreates PermissionManager for current context cwd", () => {
577
- const { session } = createSession();
572
+ it("configures PermissionManager for current context cwd", () => {
573
+ const pm = makePermissionManager();
574
+ const { session } = createSession({ permissionManager: pm });
578
575
  const ctx = makeCtx({ cwd: "/project" });
579
576
  session.activate(ctx);
580
577
 
581
- const pm2 = makePermissionManager();
582
- mockCreatePermissionManagerForCwd.mockReturnValue(pm2);
583
-
584
578
  session.reload();
585
579
 
586
- expect(mockCreatePermissionManagerForCwd).toHaveBeenCalledWith(
587
- "/test/agent",
588
- "/project",
589
- );
580
+ expect(pm.configureForCwd).toHaveBeenCalledWith("/project");
590
581
  });
591
582
 
592
583
  it("clears caches and skill entries", () => {