@gotgenes/pi-permission-system 10.5.1 → 10.5.2

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,7 +1,20 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { join } from "node:path";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ const mockHomedir = vi.hoisted(() => vi.fn(() => "/mock/home"));
5
+
6
+ vi.mock("node:os", () => ({
7
+ homedir: mockHomedir,
8
+ default: { homedir: mockHomedir },
9
+ }));
10
+
2
11
  import { normalizeInput } from "#src/input-normalizer";
3
12
  import { createMcpPermissionTargets } from "#src/mcp-targets";
4
13
 
14
+ afterEach(() => {
15
+ mockHomedir.mockClear();
16
+ });
17
+
5
18
  describe("normalizeInput — non-MCP surfaces", () => {
6
19
  describe("special / path", () => {
7
20
  it("uses path from input as the lookup value", () => {
@@ -21,10 +34,40 @@ describe("normalizeInput — non-MCP surfaces", () => {
21
34
  expect(result.values).toEqual(["*"]);
22
35
  });
23
36
 
37
+ it("falls back to '*' when path is an empty string", () => {
38
+ const result = normalizeInput("path", { path: "" }, []);
39
+ expect(result.values).toEqual(["*"]);
40
+ });
41
+
42
+ it("falls back to '*' when path is whitespace-only", () => {
43
+ const result = normalizeInput("path", { path: " " }, []);
44
+ expect(result.values).toEqual(["*"]);
45
+ });
46
+
24
47
  it("handles null input", () => {
25
48
  const result = normalizeInput("path", null, []);
26
49
  expect(result.values).toEqual(["*"]);
27
50
  });
51
+
52
+ it("expands ~/... path value to absolute home path", () => {
53
+ const result = normalizeInput("path", { path: "~/.ssh/config" }, []);
54
+ expect(result.values).toEqual([join("/mock/home", ".ssh/config")]);
55
+ });
56
+
57
+ it("expands $HOME/... path value to absolute home path", () => {
58
+ const result = normalizeInput("path", { path: "$HOME/.ssh/config" }, []);
59
+ expect(result.values).toEqual([join("/mock/home", ".ssh/config")]);
60
+ });
61
+
62
+ it("does not expand non-home values", () => {
63
+ const result = normalizeInput("path", { path: ".env" }, []);
64
+ expect(result.values).toEqual([".env"]);
65
+ });
66
+
67
+ it("does not expand the '*' fallback", () => {
68
+ const result = normalizeInput("path", {}, []);
69
+ expect(result.values).toEqual(["*"]);
70
+ });
28
71
  });
29
72
 
30
73
  describe("special / external_directory", () => {
@@ -49,10 +92,33 @@ describe("normalizeInput — non-MCP surfaces", () => {
49
92
  expect(result.values).toEqual(["*"]);
50
93
  });
51
94
 
95
+ it("falls back to '*' when path is an empty string", () => {
96
+ const result = normalizeInput("external_directory", { path: "" }, []);
97
+ expect(result.values).toEqual(["*"]);
98
+ });
99
+
52
100
  it("handles null input", () => {
53
101
  const result = normalizeInput("external_directory", null, []);
54
102
  expect(result.values).toEqual(["*"]);
55
103
  });
104
+
105
+ it("expands ~/... path value to absolute home path", () => {
106
+ const result = normalizeInput(
107
+ "external_directory",
108
+ { path: "~/dev/project" },
109
+ [],
110
+ );
111
+ expect(result.values).toEqual([join("/mock/home", "dev/project")]);
112
+ });
113
+
114
+ it("expands $HOME/... path value to absolute home path", () => {
115
+ const result = normalizeInput(
116
+ "external_directory",
117
+ { path: "$HOME/dev/project" },
118
+ [],
119
+ );
120
+ expect(result.values).toEqual([join("/mock/home", "dev/project")]);
121
+ });
56
122
  });
57
123
 
58
124
  describe("skill", () => {
@@ -130,6 +196,16 @@ describe("normalizeInput — non-MCP surfaces", () => {
130
196
  const result = normalizeInput("edit", null, []);
131
197
  expect(result.values).toEqual(["*"]);
132
198
  });
199
+
200
+ it("expands ~/... path value to absolute home path", () => {
201
+ const result = normalizeInput("read", { path: "~/.ssh/config" }, []);
202
+ expect(result.values).toEqual([join("/mock/home", ".ssh/config")]);
203
+ });
204
+
205
+ it("expands $HOME/... path value to absolute home path", () => {
206
+ const result = normalizeInput("write", { path: "$HOME/.ssh/config" }, []);
207
+ expect(result.values).toEqual([join("/mock/home", ".ssh/config")]);
208
+ });
133
209
  });
134
210
 
135
211
  describe("extension tools (non-path-bearing)", () => {
@@ -0,0 +1,51 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ readFileSync,
6
+ rmSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { expect, test } from "vitest";
11
+ import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
12
+ import { createPermissionSystemLogger } from "#src/logging";
13
+
14
+ test("Permission-system logger respects debug toggle and keeps review log enabled by default", () => {
15
+ const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-logs-"));
16
+ const logsDir = join(baseDir, "logs");
17
+ const debugLogPath = join(logsDir, "debug.jsonl");
18
+ const reviewLogPath = join(logsDir, "review.jsonl");
19
+ const config = { ...DEFAULT_EXTENSION_CONFIG };
20
+ const logger = createPermissionSystemLogger({
21
+ getConfig: () => config,
22
+ debugLogPath,
23
+ reviewLogPath,
24
+ ensureLogsDirectory: () => {
25
+ mkdirSync(logsDir, { recursive: true });
26
+ return undefined;
27
+ },
28
+ });
29
+
30
+ try {
31
+ const initialDebugWarning = logger.debug("debug.disabled", {
32
+ sample: true,
33
+ });
34
+ const reviewWarning = logger.review("permission_request.waiting", {
35
+ toolName: "write",
36
+ });
37
+
38
+ expect(initialDebugWarning).toBe(undefined);
39
+ expect(reviewWarning).toBe(undefined);
40
+ expect(existsSync(debugLogPath)).toBe(false);
41
+ expect(existsSync(reviewLogPath)).toBe(true);
42
+
43
+ config.debugLog = true;
44
+ const enabledDebugWarning = logger.debug("debug.enabled", { sample: true });
45
+ expect(enabledDebugWarning).toBe(undefined);
46
+ expect(existsSync(debugLogPath)).toBe(true);
47
+ expect(readFileSync(debugLogPath, "utf8")).toMatch(/debug\.enabled/);
48
+ } finally {
49
+ rmSync(baseDir, { recursive: true, force: true });
50
+ }
51
+ });
@@ -47,6 +47,16 @@ describe("normalizePathForComparison", () => {
47
47
  );
48
48
  });
49
49
 
50
+ test("expands bare $HOME to homedir", () => {
51
+ expect(normalizePathForComparison("$HOME", cwd)).toBe("/mock/home");
52
+ });
53
+
54
+ test("expands $HOME/... to homedir-relative path", () => {
55
+ expect(normalizePathForComparison("$HOME/.ssh/config", cwd)).toBe(
56
+ join("/mock/home", ".ssh/config"),
57
+ );
58
+ });
59
+
50
60
  test("strips leading @ before resolving", () => {
51
61
  expect(normalizePathForComparison("@/usr/local/bin", cwd)).toBe(
52
62
  "/usr/local/bin",
@@ -1,5 +1,9 @@
1
+ import { tmpdir } from "node:os";
2
+ import { join } from "node:path";
1
3
  import { afterEach, describe, expect, test, vi } from "vitest";
2
4
  import {
5
+ createPermissionForwardingLocation,
6
+ isForwardedPermissionRequestForSession,
3
7
  resolvePermissionForwardingTargetSessionId,
4
8
  SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
5
9
  SUBAGENT_PARENT_SESSION_ENV_KEY,
@@ -240,3 +244,72 @@ describe("resolvePermissionForwardingTargetSessionId — registry resolution", (
240
244
  ).toBe("parent-from-env");
241
245
  });
242
246
  });
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Moved from permission-system.test.ts catch-all (#342)
250
+ // ---------------------------------------------------------------------------
251
+
252
+ test("Permission forwarding resolves the parent interactive session from subagent runtime env", () => {
253
+ const targetSessionId = resolvePermissionForwardingTargetSessionId({
254
+ hasUI: false,
255
+ isSubagent: true,
256
+ currentSessionId: "child-session",
257
+ env: {
258
+ PI_AGENT_ROUTER_PARENT_SESSION_ID: "parent-session",
259
+ },
260
+ });
261
+
262
+ expect(targetSessionId).toBe("parent-session");
263
+ });
264
+
265
+ test("Permission forwarding does not guess a target session when subagent runtime env is missing", () => {
266
+ const targetSessionId = resolvePermissionForwardingTargetSessionId({
267
+ hasUI: false,
268
+ isSubagent: true,
269
+ currentSessionId: "child-session",
270
+ env: {},
271
+ });
272
+
273
+ expect(targetSessionId).toBe(null);
274
+ });
275
+
276
+ test("Permission forwarding uses session-scoped directories per interactive session", () => {
277
+ const forwardingRoot = join(tmpdir(), "pi-permission-system-forwarding-root");
278
+ const sessionA = createPermissionForwardingLocation(
279
+ forwardingRoot,
280
+ "session-a",
281
+ );
282
+ const sessionB = createPermissionForwardingLocation(
283
+ forwardingRoot,
284
+ "session-b",
285
+ );
286
+
287
+ expect(sessionA.sessionRootDir).not.toBe(sessionB.sessionRootDir);
288
+ expect(sessionA.requestsDir).not.toBe(sessionB.requestsDir);
289
+ expect(sessionA.responsesDir).not.toBe(sessionB.responsesDir);
290
+ });
291
+
292
+ test("Permission forwarding request routing only matches the intended UI session", () => {
293
+ expect(
294
+ isForwardedPermissionRequestForSession(
295
+ { targetSessionId: "session-a" },
296
+ "session-a",
297
+ ),
298
+ ).toBe(true);
299
+ expect(
300
+ isForwardedPermissionRequestForSession(
301
+ { targetSessionId: "session-a" },
302
+ "session-b",
303
+ ),
304
+ ).toBe(false);
305
+ });
306
+
307
+ test("Permission forwarding rejects unresolved sentinel session ids", () => {
308
+ const targetSessionId = resolvePermissionForwardingTargetSessionId({
309
+ hasUI: true,
310
+ isSubagent: false,
311
+ currentSessionId: "unknown",
312
+ });
313
+
314
+ expect(targetSessionId).toBe(null);
315
+ });