@gotgenes/pi-permission-system 10.1.0 → 10.2.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.
@@ -30,7 +30,7 @@ import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
30
30
  import type { SessionLogger } from "#src/session-logger";
31
31
  import { resolveToolPreviewLimits } from "#src/tool-preview-formatter";
32
32
  import type { ToolRegistry } from "#src/tool-registry";
33
- import type { PermissionCheckResult } from "#src/types";
33
+ import type { PermissionCheckResult, PermissionState } from "#src/types";
34
34
 
35
35
  /**
36
36
  * Precise mock boundary for PermissionGateHandler integration tests.
@@ -220,6 +220,78 @@ export function makeToolRegistry(
220
220
  };
221
221
  }
222
222
 
223
+ /**
224
+ * Surface-dispatching `checkPermission` mock.
225
+ *
226
+ * Builds a `vi.fn()` that returns a `PermissionCheckResult` for each surface,
227
+ * using `bySurface[surface]` when matched and `defaultResult` otherwise.
228
+ * Default fields: `toolName` = the surface string, `source: "tool"`,
229
+ * `origin: "builtin"` — callers override by including the field in the
230
+ * per-surface or default partial (e.g. `{ path: { state: "allow", source: "special" } }`).
231
+ *
232
+ * Return type is intentionally unannotated so callers retain full `vi.fn()`
233
+ * mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.).
234
+ */
235
+ export function makeSurfaceCheck(
236
+ bySurface: Record<
237
+ string,
238
+ Partial<PermissionCheckResult> & { state: PermissionState }
239
+ >,
240
+ defaultResult: Partial<PermissionCheckResult> & { state: PermissionState } = {
241
+ state: "allow",
242
+ },
243
+ ) {
244
+ return vi
245
+ .fn<MockGateHandlerSession["checkPermission"]>()
246
+ .mockImplementation((surface): PermissionCheckResult => {
247
+ const base = bySurface[surface] ?? defaultResult;
248
+ return {
249
+ toolName: surface,
250
+ source: "tool",
251
+ origin: "builtin",
252
+ ...base,
253
+ };
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Bash-surface `checkPermission` mock that dispatches on a command regex.
259
+ *
260
+ * For the `bash` surface: returns a deny result when `opts.deny` matches the
261
+ * command, and an allow result otherwise. For all other surfaces, returns a
262
+ * plain allow result.
263
+ *
264
+ * Return type is intentionally unannotated so callers retain full `vi.fn()`
265
+ * mock access.
266
+ */
267
+ export function makeBashCommandCheck(opts: {
268
+ deny: RegExp;
269
+ denyMatched: string;
270
+ allowMatched?: string;
271
+ }) {
272
+ return vi
273
+ .fn<MockGateHandlerSession["checkPermission"]>()
274
+ .mockImplementation((surface, input): PermissionCheckResult => {
275
+ if (surface === "bash") {
276
+ const command = (input as { command?: string }).command ?? "";
277
+ return opts.deny.test(command)
278
+ ? makeCheckResult({
279
+ state: "deny",
280
+ source: "bash",
281
+ command,
282
+ matchedPattern: opts.denyMatched,
283
+ })
284
+ : makeCheckResult({
285
+ state: "allow",
286
+ source: "bash",
287
+ command,
288
+ matchedPattern: opts.allowMatched,
289
+ });
290
+ }
291
+ return makeCheckResult({ state: "allow" });
292
+ });
293
+ }
294
+
223
295
  /**
224
296
  * Constructs a PermissionGateHandler with mocked collaborators.
225
297
  *
@@ -229,10 +301,19 @@ export function makeToolRegistry(
229
301
  export function makeHandler(overrides?: {
230
302
  session?: Partial<MockGateHandlerSession>;
231
303
  toolRegistry?: Partial<ToolRegistry>;
304
+ /** Sugar: builds the `getAll` mock from a list of tool names. */
305
+ tools?: string[];
232
306
  }) {
233
307
  const session = makeSession(overrides?.session);
234
308
  const events = makeEvents();
235
- const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
309
+ const toolRegistry =
310
+ overrides?.tools !== undefined
311
+ ? makeToolRegistry({
312
+ getAll: vi
313
+ .fn()
314
+ .mockReturnValue(overrides.tools.map((name) => ({ name }))),
315
+ })
316
+ : makeToolRegistry(overrides?.toolRegistry);
236
317
  const pipeline = new ToolCallGatePipeline(session);
237
318
  const skillInputPipeline = new SkillInputGatePipeline(session);
238
319
  const reporter = new GateDecisionReporter(session.logger, events);
@@ -8,7 +8,11 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
8
  import { homedir, tmpdir } from "node:os";
9
9
  import { join } from "node:path";
10
10
  import { describe, expect, it } from "vitest";
11
- import { PermissionManager } from "#src/permission-manager";
11
+ import { getGlobalConfigPath, getProjectConfigPath } from "#src/config-paths";
12
+ import {
13
+ PermissionManager,
14
+ type ScopedPermissionManager,
15
+ } from "#src/permission-manager";
12
16
  import type { Rule, Ruleset } from "#src/rule";
13
17
 
14
18
  // ---------------------------------------------------------------------------
@@ -1349,3 +1353,157 @@ describe("cross-cutting path surface", () => {
1349
1353
  }
1350
1354
  });
1351
1355
  });
1356
+
1357
+ // ---------------------------------------------------------------------------
1358
+ // configureForCwd and agentDir construction
1359
+ // ---------------------------------------------------------------------------
1360
+
1361
+ describe("PermissionManager — configureForCwd and agentDir option", () => {
1362
+ /**
1363
+ * Build a temp agentDir with a global config and an optional cwd with a
1364
+ * project config. Returns the paths and a cleanup function.
1365
+ */
1366
+ function makeAgentDirSetup(opts: {
1367
+ globalPermission: Record<string, unknown>;
1368
+ projectPermission?: Record<string, unknown>;
1369
+ }): {
1370
+ agentDir: string;
1371
+ cwd: string;
1372
+ globalConfigPath: string;
1373
+ projectConfigPath: string;
1374
+ cleanup: () => void;
1375
+ } {
1376
+ const baseDir = mkdtempSync(join(tmpdir(), "pm-agent-dir-test-"));
1377
+ const agentDir = join(baseDir, "agent");
1378
+ const cwd = join(baseDir, "project");
1379
+
1380
+ // Write global config under getGlobalConfigPath(agentDir)
1381
+ const globalConfigPath = getGlobalConfigPath(agentDir);
1382
+ mkdirSync(join(agentDir, "extensions", "pi-permission-system"), {
1383
+ recursive: true,
1384
+ });
1385
+ writeFileSync(
1386
+ globalConfigPath,
1387
+ JSON.stringify({ permission: opts.globalPermission }, null, 2),
1388
+ );
1389
+
1390
+ // Write project config under getProjectConfigPath(cwd)
1391
+ const projectConfigPath = getProjectConfigPath(cwd);
1392
+ mkdirSync(join(cwd, ".pi", "extensions", "pi-permission-system"), {
1393
+ recursive: true,
1394
+ });
1395
+ if (opts.projectPermission) {
1396
+ writeFileSync(
1397
+ projectConfigPath,
1398
+ JSON.stringify({ permission: opts.projectPermission }, null, 2),
1399
+ );
1400
+ }
1401
+
1402
+ return {
1403
+ agentDir,
1404
+ cwd,
1405
+ globalConfigPath,
1406
+ projectConfigPath,
1407
+ cleanup: () => rmSync(baseDir, { recursive: true, force: true }),
1408
+ };
1409
+ }
1410
+
1411
+ it("ScopedPermissionManager is exported and PermissionManager satisfies it", () => {
1412
+ // Type-level assertion: assigning PermissionManager to ScopedPermissionManager compiles.
1413
+ const manager = new PermissionManager({
1414
+ globalConfigPath: "/nonexistent/config.json",
1415
+ agentsDir: "/nonexistent/agents",
1416
+ });
1417
+ const scoped: ScopedPermissionManager = manager;
1418
+ expect(typeof scoped.configureForCwd).toBe("function");
1419
+ expect(typeof scoped.checkPermission).toBe("function");
1420
+ expect(typeof scoped.getToolPermission).toBe("function");
1421
+ expect(typeof scoped.getConfigIssues).toBe("function");
1422
+ expect(typeof scoped.getPolicyCacheStamp).toBe("function");
1423
+ });
1424
+
1425
+ it("construction with { agentDir } reads global config from getGlobalConfigPath(agentDir)", () => {
1426
+ const { agentDir, cleanup } = makeAgentDirSetup({
1427
+ globalPermission: { read: "deny" },
1428
+ });
1429
+ try {
1430
+ const manager = new PermissionManager({ agentDir });
1431
+ const result = manager.checkPermission("read", { path: "foo.txt" });
1432
+ expect(result.state).toBe("deny");
1433
+ } finally {
1434
+ cleanup();
1435
+ }
1436
+ });
1437
+
1438
+ it("configureForCwd(cwd) applies project config (project overrides global)", () => {
1439
+ const { agentDir, cwd, cleanup } = makeAgentDirSetup({
1440
+ globalPermission: { read: "deny" },
1441
+ projectPermission: { read: "allow" },
1442
+ });
1443
+ try {
1444
+ const manager = new PermissionManager({ agentDir });
1445
+ // Before configureForCwd: global policy applies
1446
+ expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
1447
+ "deny",
1448
+ );
1449
+
1450
+ manager.configureForCwd(cwd);
1451
+
1452
+ // After configureForCwd: project override applies (last-match-wins)
1453
+ expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
1454
+ "allow",
1455
+ );
1456
+ } finally {
1457
+ cleanup();
1458
+ }
1459
+ });
1460
+
1461
+ it("configureForCwd(undefined) reverts to global-only", () => {
1462
+ const { agentDir, cwd, cleanup } = makeAgentDirSetup({
1463
+ globalPermission: { read: "deny" },
1464
+ projectPermission: { read: "allow" },
1465
+ });
1466
+ try {
1467
+ const manager = new PermissionManager({ agentDir });
1468
+ manager.configureForCwd(cwd);
1469
+ expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
1470
+ "allow",
1471
+ );
1472
+
1473
+ manager.configureForCwd(undefined);
1474
+
1475
+ // After reverting: global policy applies again
1476
+ expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
1477
+ "deny",
1478
+ );
1479
+ } finally {
1480
+ cleanup();
1481
+ }
1482
+ });
1483
+
1484
+ it("configureForCwd clears the resolved-permissions cache", () => {
1485
+ const { agentDir, globalConfigPath, cleanup } = makeAgentDirSetup({
1486
+ globalPermission: { read: "allow" },
1487
+ });
1488
+ try {
1489
+ const manager = new PermissionManager({ agentDir });
1490
+ // Warm the cache
1491
+ expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
1492
+ "allow",
1493
+ );
1494
+ // Update global config on disk to deny read
1495
+ writeFileSync(
1496
+ globalConfigPath,
1497
+ JSON.stringify({ permission: { read: "deny" } }, null, 2),
1498
+ );
1499
+ // configureForCwd clears cache + rebuilds loader
1500
+ manager.configureForCwd(undefined);
1501
+ // Should pick up the changed global config
1502
+ expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
1503
+ "deny",
1504
+ );
1505
+ } finally {
1506
+ cleanup();
1507
+ }
1508
+ });
1509
+ });
@@ -3,42 +3,32 @@ 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
 
32
20
  import type { ExtensionPaths } from "#src/extension-paths";
33
21
  import type { ForwardingController } from "#src/forwarding-manager";
34
- import type { PermissionManager } from "#src/permission-manager";
22
+ import type { ScopedPermissionManager } from "#src/permission-manager";
35
23
  import {
36
24
  PermissionSession,
37
25
  type PermissionSessionRuntimeDeps,
38
26
  } from "#src/permission-session";
27
+ import type { Ruleset } from "#src/rule";
39
28
  import { SessionApproval } from "#src/session-approval";
40
29
  import type { SessionLogger } from "#src/session-logger";
41
30
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
31
+ import type { PermissionCheckResult, PermissionState } from "#src/types";
42
32
  import { makeCtx } from "#test/helpers/handler-fixtures";
43
33
 
44
34
  function makeSkillEntry(
@@ -95,29 +85,37 @@ function makeForwarding(): ForwardingController {
95
85
  };
96
86
  }
97
87
 
98
- function makePermissionManager(
99
- overrides: Partial<PermissionManager> = {},
100
- ): PermissionManager {
88
+ function makePermissionManager() {
101
89
  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;
90
+ configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
91
+ checkPermission: vi
92
+ .fn<
93
+ (
94
+ toolName: string,
95
+ input: unknown,
96
+ agentName?: string,
97
+ sessionRules?: Ruleset,
98
+ ) => PermissionCheckResult
99
+ >()
100
+ .mockReturnValue({
101
+ state: "allow",
102
+ toolName: "read",
103
+ source: "tool",
104
+ origin: "builtin",
105
+ }),
106
+ getToolPermission: vi
107
+ .fn<(toolName: string, agentName?: string) => PermissionState>()
108
+ .mockReturnValue("allow"),
109
+ getConfigIssues: vi.fn((): string[] => []),
110
+ getPolicyCacheStamp: vi.fn((): string => "stamp-1"),
111
+ };
115
112
  }
116
113
 
117
114
  function createSession(overrides?: {
118
115
  paths?: Partial<ExtensionPaths>;
119
116
  logger?: SessionLogger;
120
117
  forwarding?: ForwardingController;
118
+ permissionManager?: ScopedPermissionManager;
121
119
  runtimeDeps?: PermissionSessionRuntimeDeps;
122
120
  }): {
123
121
  session: PermissionSession;
@@ -129,8 +127,16 @@ function createSession(overrides?: {
129
127
  const paths = makePaths(overrides?.paths);
130
128
  const logger = overrides?.logger ?? makeLogger();
131
129
  const forwarding = overrides?.forwarding ?? makeForwarding();
130
+ const permissionManager =
131
+ overrides?.permissionManager ?? makePermissionManager();
132
132
  const runtimeDeps = overrides?.runtimeDeps ?? makeRuntimeDeps();
133
- const session = new PermissionSession(paths, logger, forwarding, runtimeDeps);
133
+ const session = new PermissionSession(
134
+ paths,
135
+ logger,
136
+ forwarding,
137
+ permissionManager,
138
+ runtimeDeps,
139
+ );
134
140
  return { session, paths, logger, forwarding, runtimeDeps };
135
141
  }
136
142
 
@@ -139,10 +145,6 @@ function createSession(overrides?: {
139
145
  beforeEach(() => {
140
146
  mockGetActiveAgentName.mockReset();
141
147
  mockGetActiveAgentNameFromSystemPrompt.mockReset();
142
- mockCreatePermissionManagerForCwd.mockReset();
143
-
144
- // Default: createPermissionManagerForCwd returns a fresh mock PM
145
- mockCreatePermissionManagerForCwd.mockReturnValue(makePermissionManager());
146
148
  mockGetActiveAgentName.mockReturnValue(null);
147
149
  mockGetActiveAgentNameFromSystemPrompt.mockReturnValue(null);
148
150
  });
@@ -151,8 +153,7 @@ describe("PermissionSession", () => {
151
153
  describe("constructor and delegation", () => {
152
154
  it("delegates checkPermission to internal PermissionManager", () => {
153
155
  const pm = makePermissionManager();
154
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
155
- const { session } = createSession();
156
+ const { session } = createSession({ permissionManager: pm });
156
157
 
157
158
  const result = session.checkPermission("bash", { command: "ls" });
158
159
 
@@ -167,8 +168,7 @@ describe("PermissionSession", () => {
167
168
 
168
169
  it("delegates getToolPermission to internal PermissionManager", () => {
169
170
  const pm = makePermissionManager();
170
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
171
- const { session } = createSession();
171
+ const { session } = createSession({ permissionManager: pm });
172
172
 
173
173
  const result = session.getToolPermission("read");
174
174
 
@@ -177,11 +177,9 @@ describe("PermissionSession", () => {
177
177
  });
178
178
 
179
179
  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();
180
+ const pm = makePermissionManager();
181
+ vi.mocked(pm.getConfigIssues).mockReturnValue(["issue1"]);
182
+ const { session } = createSession({ permissionManager: pm });
185
183
 
186
184
  expect(session.getConfigIssues("agent1")).toEqual(["issue1"]);
187
185
  expect(pm.getConfigIssues).toHaveBeenCalledWith("agent1");
@@ -189,8 +187,7 @@ describe("PermissionSession", () => {
189
187
 
190
188
  it("delegates getPolicyCacheStamp to internal PermissionManager", () => {
191
189
  const pm = makePermissionManager();
192
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
193
- const { session } = createSession();
190
+ const { session } = createSession({ permissionManager: pm });
194
191
 
195
192
  expect(session.getPolicyCacheStamp("agent1")).toBe("stamp-1");
196
193
  expect(pm.getPolicyCacheStamp).toHaveBeenCalledWith("agent1");
@@ -220,8 +217,7 @@ describe("PermissionSession", () => {
220
217
  describe("resolve", () => {
221
218
  it("forwards surface, input, and agentName, applying the empty session ruleset", () => {
222
219
  const pm = makePermissionManager();
223
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
224
- const { session } = createSession();
220
+ const { session } = createSession({ permissionManager: pm });
225
221
 
226
222
  session.resolve("bash", { command: "ls" }, "agent-x");
227
223
 
@@ -235,8 +231,7 @@ describe("PermissionSession", () => {
235
231
 
236
232
  it("defaults agentName to undefined when omitted", () => {
237
233
  const pm = makePermissionManager();
238
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
239
- const { session } = createSession();
234
+ const { session } = createSession({ permissionManager: pm });
240
235
 
241
236
  session.resolve("read", { path: ".env" });
242
237
 
@@ -250,8 +245,7 @@ describe("PermissionSession", () => {
250
245
 
251
246
  it("applies a recorded session approval on the next resolve", () => {
252
247
  const pm = makePermissionManager();
253
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
254
- const { session } = createSession();
248
+ const { session } = createSession({ permissionManager: pm });
255
249
 
256
250
  session.recordSessionApproval(SessionApproval.single("bash", "git *"));
257
251
  session.resolve("bash", { command: "git status" });
@@ -266,17 +260,15 @@ describe("PermissionSession", () => {
266
260
  });
267
261
 
268
262
  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
- }),
263
+ const pm = makePermissionManager();
264
+ vi.mocked(pm.checkPermission).mockReturnValue({
265
+ state: "deny",
266
+ toolName: "bash",
267
+ source: "bash",
268
+ origin: "global",
269
+ matchedPattern: "rm *",
277
270
  });
278
- mockCreatePermissionManagerForCwd.mockReturnValue(pm);
279
- const { session } = createSession();
271
+ const { session } = createSession({ permissionManager: pm });
280
272
 
281
273
  const result = session.resolve("bash", { command: "rm -rf /" });
282
274
 
@@ -310,28 +302,14 @@ describe("PermissionSession", () => {
310
302
  });
311
303
 
312
304
  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();
305
+ it("configures the injected PermissionManager for the context cwd", () => {
306
+ const pm = makePermissionManager();
307
+ const { session } = createSession({ permissionManager: pm });
324
308
  const ctx = makeCtx({ cwd: "/new/project" });
325
309
 
326
310
  session.resetForNewSession(ctx);
327
311
 
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");
312
+ expect(pm.configureForCwd).toHaveBeenCalledWith("/new/project");
335
313
  });
336
314
 
337
315
  it("clears cache keys", () => {
@@ -573,20 +551,15 @@ describe("PermissionSession", () => {
573
551
  });
574
552
 
575
553
  describe("reload", () => {
576
- it("recreates PermissionManager for current context cwd", () => {
577
- const { session } = createSession();
554
+ it("configures PermissionManager for current context cwd", () => {
555
+ const pm = makePermissionManager();
556
+ const { session } = createSession({ permissionManager: pm });
578
557
  const ctx = makeCtx({ cwd: "/project" });
579
558
  session.activate(ctx);
580
559
 
581
- const pm2 = makePermissionManager();
582
- mockCreatePermissionManagerForCwd.mockReturnValue(pm2);
583
-
584
560
  session.reload();
585
561
 
586
- expect(mockCreatePermissionManagerForCwd).toHaveBeenCalledWith(
587
- "/test/agent",
588
- "/project",
589
- );
562
+ expect(pm.configureForCwd).toHaveBeenCalledWith("/project");
590
563
  });
591
564
 
592
565
  it("clears caches and skill entries", () => {
@@ -1,4 +1,3 @@
1
- import { join } from "node:path";
2
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
2
 
4
3
  // ── logger stub ────────────────────────────────────────────────────────────
@@ -63,19 +62,9 @@ vi.mock("../src/session-rules", () => ({
63
62
  }));
64
63
 
65
64
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
66
- import {
67
- getGlobalConfigPath,
68
- getGlobalLogsDir,
69
- getProjectConfigPath,
70
- } from "#src/config-paths";
65
+ import { getGlobalLogsDir } from "#src/config-paths";
71
66
  import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
72
- import { PermissionManager } from "#src/permission-manager";
73
- import {
74
- createExtensionRuntime,
75
- createPermissionManagerForCwd,
76
- derivePiProjectPaths,
77
- refreshExtensionConfig,
78
- } from "#src/runtime";
67
+ import { createExtensionRuntime, refreshExtensionConfig } from "#src/runtime";
79
68
 
80
69
  // ── test suite ─────────────────────────────────────────────────────────────
81
70
 
@@ -351,75 +340,6 @@ describe("createExtensionRuntime", () => {
351
340
  });
352
341
  });
353
342
 
354
- // ── derivePiProjectPaths ───────────────────────────────────────────────────
355
-
356
- describe("derivePiProjectPaths", () => {
357
- it("returns null for null cwd", () => {
358
- expect(derivePiProjectPaths(null)).toBeNull();
359
- });
360
-
361
- it("returns null for undefined cwd", () => {
362
- expect(derivePiProjectPaths(undefined)).toBeNull();
363
- });
364
-
365
- it("returns null for empty string cwd", () => {
366
- expect(derivePiProjectPaths("")).toBeNull();
367
- });
368
-
369
- it("returns projectGlobalConfigPath via getProjectConfigPath", () => {
370
- const result = derivePiProjectPaths("/my/project");
371
- expect(result?.projectGlobalConfigPath).toBe(
372
- getProjectConfigPath("/my/project"),
373
- );
374
- });
375
-
376
- it("returns projectAgentsDir as .pi/agent/agents under cwd", () => {
377
- const result = derivePiProjectPaths("/my/project");
378
- expect(result?.projectAgentsDir).toBe(
379
- join("/my/project", ".pi", "agent", "agents"),
380
- );
381
- });
382
- });
383
-
384
- // ── createPermissionManagerForCwd ─────────────────────────────────────────
385
-
386
- describe("createPermissionManagerForCwd", () => {
387
- beforeEach(() => {
388
- // PermissionManager is already mocked as vi.fn() at module scope.
389
- });
390
-
391
- it("creates a PermissionManager with globalConfigPath from agentDir", () => {
392
- const MockPM = PermissionManager as ReturnType<typeof vi.fn>;
393
- MockPM.mockClear();
394
- createPermissionManagerForCwd("/test/agent", null);
395
- expect(MockPM).toHaveBeenCalledWith(
396
- expect.objectContaining({
397
- globalConfigPath: getGlobalConfigPath("/test/agent"),
398
- }),
399
- );
400
- });
401
-
402
- it("includes projectGlobalConfigPath when cwd is provided", () => {
403
- const MockPM = PermissionManager as ReturnType<typeof vi.fn>;
404
- MockPM.mockClear();
405
- createPermissionManagerForCwd("/test/agent", "/my/project");
406
- expect(MockPM).toHaveBeenCalledWith(
407
- expect.objectContaining({
408
- globalConfigPath: getGlobalConfigPath("/test/agent"),
409
- projectGlobalConfigPath: getProjectConfigPath("/my/project"),
410
- }),
411
- );
412
- });
413
-
414
- it("excludes projectGlobalConfigPath when cwd is null", () => {
415
- const MockPM = PermissionManager as ReturnType<typeof vi.fn>;
416
- MockPM.mockClear();
417
- createPermissionManagerForCwd("/test/agent", null);
418
- const callArg = MockPM.mock.calls[0][0] as Record<string, unknown>;
419
- expect(callArg.projectGlobalConfigPath).toBeUndefined();
420
- });
421
- });
422
-
423
343
  // ── refreshExtensionConfig ────────────────────────────────────────────────
424
344
 
425
345
  describe("refreshExtensionConfig", () => {