@gotgenes/pi-permission-system 3.0.0 → 3.0.1

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.
@@ -0,0 +1,155 @@
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
+
3
+ import {
4
+ checkRequestedToolRegistration,
5
+ getToolNameFromValue,
6
+ } from "../src/tool-registry.js";
7
+
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ describe("getToolNameFromValue", () => {
13
+ test("returns string value directly", () => {
14
+ expect(getToolNameFromValue("read")).toBe("read");
15
+ });
16
+
17
+ test("returns null for empty string", () => {
18
+ expect(getToolNameFromValue("")).toBeNull();
19
+ });
20
+
21
+ test("returns null for whitespace-only string", () => {
22
+ expect(getToolNameFromValue(" ")).toBeNull();
23
+ });
24
+
25
+ test("returns null for null", () => {
26
+ expect(getToolNameFromValue(null)).toBeNull();
27
+ });
28
+
29
+ test("returns null for undefined", () => {
30
+ expect(getToolNameFromValue(undefined)).toBeNull();
31
+ });
32
+
33
+ test("extracts toolName from object", () => {
34
+ expect(getToolNameFromValue({ toolName: "write" })).toBe("write");
35
+ });
36
+
37
+ test("extracts name from object", () => {
38
+ expect(getToolNameFromValue({ name: "edit" })).toBe("edit");
39
+ });
40
+
41
+ test("extracts tool from object", () => {
42
+ expect(getToolNameFromValue({ tool: "bash" })).toBe("bash");
43
+ });
44
+
45
+ test("prefers toolName over name over tool", () => {
46
+ expect(
47
+ getToolNameFromValue({
48
+ toolName: "first",
49
+ name: "second",
50
+ tool: "third",
51
+ }),
52
+ ).toBe("first");
53
+ });
54
+
55
+ test("falls back to name when toolName is empty", () => {
56
+ expect(getToolNameFromValue({ toolName: "", name: "edit" })).toBe("edit");
57
+ });
58
+
59
+ test("returns null for object with no recognised keys", () => {
60
+ expect(getToolNameFromValue({ unknown: "read" })).toBeNull();
61
+ });
62
+
63
+ test("returns null for number input", () => {
64
+ expect(getToolNameFromValue(42)).toBeNull();
65
+ });
66
+ });
67
+
68
+ describe("checkRequestedToolRegistration", () => {
69
+ test("returns missing-tool-name for null requested name", () => {
70
+ const result = checkRequestedToolRegistration(null, []);
71
+ expect(result.status).toBe("missing-tool-name");
72
+ });
73
+
74
+ test("returns missing-tool-name for whitespace-only requested name", () => {
75
+ const result = checkRequestedToolRegistration(" ", []);
76
+ expect(result.status).toBe("missing-tool-name");
77
+ });
78
+
79
+ test("returns registered when tool name matches a string entry", () => {
80
+ const result = checkRequestedToolRegistration("read", ["read", "write"]);
81
+ expect(result.status).toBe("registered");
82
+ if (result.status === "registered") {
83
+ expect(result.requestedToolName).toBe("read");
84
+ expect(result.normalizedToolName).toBe("read");
85
+ }
86
+ });
87
+
88
+ test("returns registered when tool name matches an object entry by name", () => {
89
+ const result = checkRequestedToolRegistration("edit", [{ name: "edit" }]);
90
+ expect(result.status).toBe("registered");
91
+ });
92
+
93
+ test("returns registered when tool name matches an object entry by toolName", () => {
94
+ const result = checkRequestedToolRegistration("bash", [
95
+ { toolName: "bash" },
96
+ ]);
97
+ expect(result.status).toBe("registered");
98
+ });
99
+
100
+ test("returns unregistered when tool is not in the list", () => {
101
+ const result = checkRequestedToolRegistration("ghost", ["read", "write"]);
102
+ expect(result.status).toBe("unregistered");
103
+ if (result.status === "unregistered") {
104
+ expect(result.requestedToolName).toBe("ghost");
105
+ expect(result.availableToolNames).toContain("read");
106
+ expect(result.availableToolNames).toContain("write");
107
+ }
108
+ });
109
+
110
+ test("available tool names are sorted alphabetically", () => {
111
+ const result = checkRequestedToolRegistration("ghost", [
112
+ "write",
113
+ "read",
114
+ "edit",
115
+ ]);
116
+ if (result.status === "unregistered") {
117
+ expect(result.availableToolNames).toEqual(["edit", "read", "write"]);
118
+ }
119
+ });
120
+
121
+ test("resolves alias: requested alias maps to registered canonical name", () => {
122
+ const aliases = { Execute: "bash" };
123
+ const result = checkRequestedToolRegistration("Execute", ["bash"], aliases);
124
+ expect(result.status).toBe("registered");
125
+ if (result.status === "registered") {
126
+ expect(result.normalizedToolName).toBe("bash");
127
+ }
128
+ });
129
+
130
+ test("resolves alias: registered canonical is found via reverse alias lookup", () => {
131
+ // "bash" is registered; alias maps "Execute" → "bash"
132
+ // requesting "bash" directly should still resolve via the alias table
133
+ const aliases = { Execute: "bash" };
134
+ const result = checkRequestedToolRegistration("bash", ["bash"], aliases);
135
+ expect(result.status).toBe("registered");
136
+ });
137
+
138
+ test("returns unregistered with empty availableToolNames for empty tool list", () => {
139
+ const result = checkRequestedToolRegistration("read", []);
140
+ expect(result.status).toBe("unregistered");
141
+ if (result.status === "unregistered") {
142
+ expect(result.availableToolNames).toEqual([]);
143
+ }
144
+ });
145
+
146
+ test("skips tool list entries that yield no name", () => {
147
+ const result = checkRequestedToolRegistration("read", [
148
+ null,
149
+ {},
150
+ { unrelated: "x" },
151
+ "read",
152
+ ]);
153
+ expect(result.status).toBe("registered");
154
+ });
155
+ });
@@ -0,0 +1,180 @@
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
+
3
+ import {
4
+ compileWildcardPattern,
5
+ compileWildcardPatternEntries,
6
+ findCompiledWildcardMatch,
7
+ findCompiledWildcardMatchForNames,
8
+ } from "../src/wildcard-matcher.js";
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ describe("compileWildcardPatternEntries", () => {
15
+ test("returns empty array for empty iterable", () => {
16
+ const result = compileWildcardPatternEntries([]);
17
+ expect(result).toEqual([]);
18
+ });
19
+
20
+ test("compiles a single exact pattern", () => {
21
+ const result = compileWildcardPatternEntries([["read", "allow"]]);
22
+ expect(result).toHaveLength(1);
23
+ expect(result[0].pattern).toBe("read");
24
+ expect(result[0].state).toBe("allow");
25
+ });
26
+
27
+ test("compiles multiple patterns in order", () => {
28
+ const entries: [string, string][] = [
29
+ ["read", "allow"],
30
+ ["write", "deny"],
31
+ ["bash *", "ask"],
32
+ ];
33
+ const result = compileWildcardPatternEntries(entries);
34
+ expect(result).toHaveLength(3);
35
+ expect(result.map((r) => r.pattern)).toEqual(["read", "write", "bash *"]);
36
+ });
37
+ });
38
+
39
+ describe("findCompiledWildcardMatch", () => {
40
+ test("returns null for empty patterns array", () => {
41
+ const result = findCompiledWildcardMatch([], "read");
42
+ expect(result).toBeNull();
43
+ });
44
+
45
+ test("matches exact pattern", () => {
46
+ const patterns = compileWildcardPatternEntries([["read", "allow"]]);
47
+ const result = findCompiledWildcardMatch(patterns, "read");
48
+ expect(result).not.toBeNull();
49
+ expect(result?.state).toBe("allow");
50
+ expect(result?.matchedPattern).toBe("read");
51
+ expect(result?.matchedName).toBe("read");
52
+ });
53
+
54
+ test("returns null when no pattern matches", () => {
55
+ const patterns = compileWildcardPatternEntries([["read", "allow"]]);
56
+ const result = findCompiledWildcardMatch(patterns, "write");
57
+ expect(result).toBeNull();
58
+ });
59
+
60
+ test("matches glob * pattern", () => {
61
+ const patterns = compileWildcardPatternEntries([["git *", "allow"]]);
62
+ const result = findCompiledWildcardMatch(patterns, "git status");
63
+ expect(result).not.toBeNull();
64
+ expect(result?.state).toBe("allow");
65
+ expect(result?.matchedPattern).toBe("git *");
66
+ });
67
+
68
+ test("glob * matches zero or more characters", () => {
69
+ const patterns = compileWildcardPatternEntries([["git*", "allow"]]);
70
+ expect(findCompiledWildcardMatch(patterns, "git")).not.toBeNull();
71
+ expect(findCompiledWildcardMatch(patterns, "git status")).not.toBeNull();
72
+ expect(findCompiledWildcardMatch(patterns, "npm install")).toBeNull();
73
+ });
74
+
75
+ test("last-match-wins precedence: later pattern overrides earlier", () => {
76
+ const patterns = compileWildcardPatternEntries([
77
+ ["git *", "allow"],
78
+ ["git push *", "deny"],
79
+ ]);
80
+ const result = findCompiledWildcardMatch(patterns, "git push origin main");
81
+ expect(result).not.toBeNull();
82
+ expect(result?.state).toBe("deny");
83
+ expect(result?.matchedPattern).toBe("git push *");
84
+ });
85
+
86
+ test("last-match-wins: specific deny before broad allow matches the later one", () => {
87
+ const patterns = compileWildcardPatternEntries([
88
+ ["*", "deny"],
89
+ ["git status", "allow"],
90
+ ]);
91
+ const result = findCompiledWildcardMatch(patterns, "git status");
92
+ expect(result).not.toBeNull();
93
+ expect(result?.state).toBe("allow");
94
+ });
95
+
96
+ test("exact pattern does not match partial name", () => {
97
+ const patterns = compileWildcardPatternEntries([["read", "allow"]]);
98
+ expect(findCompiledWildcardMatch(patterns, "read ")).toBeNull();
99
+ expect(findCompiledWildcardMatch(patterns, "readonly")).toBeNull();
100
+ });
101
+
102
+ test("regex special characters in pattern are escaped", () => {
103
+ const patterns = compileWildcardPatternEntries([
104
+ ["tool.name", "allow"],
105
+ ["tool+extra", "deny"],
106
+ ]);
107
+ // "tool.name" should not match "toolXname" (dot is escaped)
108
+ expect(findCompiledWildcardMatch(patterns, "toolXname")).toBeNull();
109
+ // Exact match works
110
+ expect(findCompiledWildcardMatch(patterns, "tool.name")).not.toBeNull();
111
+ expect(findCompiledWildcardMatch(patterns, "tool+extra")).not.toBeNull();
112
+ });
113
+ });
114
+
115
+ describe("findCompiledWildcardMatchForNames", () => {
116
+ test("returns null for empty names array", () => {
117
+ const patterns = compileWildcardPatternEntries([["read", "allow"]]);
118
+ const result = findCompiledWildcardMatchForNames(patterns, []);
119
+ expect(result).toBeNull();
120
+ });
121
+
122
+ test("returns null when all names are whitespace", () => {
123
+ const patterns = compileWildcardPatternEntries([[" ", "allow"]]);
124
+ const result = findCompiledWildcardMatchForNames(patterns, [" ", "\t"]);
125
+ expect(result).toBeNull();
126
+ });
127
+
128
+ test("matches first name that has a pattern match", () => {
129
+ const patterns = compileWildcardPatternEntries([
130
+ ["read", "allow"],
131
+ ["write", "deny"],
132
+ ]);
133
+ const result = findCompiledWildcardMatchForNames(patterns, [
134
+ "grep",
135
+ "write",
136
+ ]);
137
+ expect(result).not.toBeNull();
138
+ expect(result?.matchedName).toBe("write");
139
+ expect(result?.state).toBe("deny");
140
+ });
141
+
142
+ test("trims whitespace from names before matching", () => {
143
+ const patterns = compileWildcardPatternEntries([["read", "allow"]]);
144
+ const result = findCompiledWildcardMatchForNames(patterns, [" read "]);
145
+ expect(result).not.toBeNull();
146
+ expect(result?.state).toBe("allow");
147
+ });
148
+
149
+ test("returns null when no name matches any pattern", () => {
150
+ const patterns = compileWildcardPatternEntries([["read", "allow"]]);
151
+ const result = findCompiledWildcardMatchForNames(patterns, [
152
+ "write",
153
+ "grep",
154
+ ]);
155
+ expect(result).toBeNull();
156
+ });
157
+
158
+ test("multi-name lookup: returns match for first matching name in order", () => {
159
+ const patterns = compileWildcardPatternEntries([
160
+ ["read", "allow"],
161
+ ["write", "deny"],
162
+ ]);
163
+ // "read" comes before "write" in names array, so "read" should match first
164
+ const result = findCompiledWildcardMatchForNames(patterns, [
165
+ "read",
166
+ "write",
167
+ ]);
168
+ expect(result).not.toBeNull();
169
+ expect(result?.matchedName).toBe("read");
170
+ expect(result?.state).toBe("allow");
171
+ });
172
+
173
+ test("compileWildcardPattern produces correct pattern metadata", () => {
174
+ const compiled = compileWildcardPattern("bash *", "ask");
175
+ expect(compiled.pattern).toBe("bash *");
176
+ expect(compiled.state).toBe("ask");
177
+ expect(compiled.regex.test("bash ls -la")).toBe(true);
178
+ expect(compiled.regex.test("echo hello")).toBe(false);
179
+ });
180
+ });
@@ -0,0 +1,110 @@
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
+ import type { PermissionSystemExtensionConfig } from "../src/extension-config.js";
3
+ import {
4
+ canResolveAskPermissionRequest,
5
+ shouldAutoApprovePermissionState,
6
+ } from "../src/yolo-mode.js";
7
+
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ function makeConfig(
13
+ yoloMode: boolean | undefined,
14
+ ): PermissionSystemExtensionConfig {
15
+ return { yoloMode } as PermissionSystemExtensionConfig;
16
+ }
17
+
18
+ describe("shouldAutoApprovePermissionState", () => {
19
+ test("returns true for 'ask' when yolo mode is on", () => {
20
+ expect(shouldAutoApprovePermissionState("ask", makeConfig(true))).toBe(
21
+ true,
22
+ );
23
+ });
24
+
25
+ test("returns false for 'ask' when yolo mode is off", () => {
26
+ expect(shouldAutoApprovePermissionState("ask", makeConfig(false))).toBe(
27
+ false,
28
+ );
29
+ });
30
+
31
+ test("returns false for 'ask' when yolo mode is undefined", () => {
32
+ expect(shouldAutoApprovePermissionState("ask", makeConfig(undefined))).toBe(
33
+ false,
34
+ );
35
+ });
36
+
37
+ test("returns false for 'allow' even when yolo mode is on", () => {
38
+ expect(shouldAutoApprovePermissionState("allow", makeConfig(true))).toBe(
39
+ false,
40
+ );
41
+ });
42
+
43
+ test("returns false for 'deny' even when yolo mode is on", () => {
44
+ expect(shouldAutoApprovePermissionState("deny", makeConfig(true))).toBe(
45
+ false,
46
+ );
47
+ });
48
+ });
49
+
50
+ describe("canResolveAskPermissionRequest", () => {
51
+ test("returns true when hasUI is true regardless of other flags", () => {
52
+ expect(
53
+ canResolveAskPermissionRequest({
54
+ config: makeConfig(false),
55
+ hasUI: true,
56
+ isSubagent: false,
57
+ }),
58
+ ).toBe(true);
59
+ });
60
+
61
+ test("returns true when isSubagent is true regardless of other flags", () => {
62
+ expect(
63
+ canResolveAskPermissionRequest({
64
+ config: makeConfig(false),
65
+ hasUI: false,
66
+ isSubagent: true,
67
+ }),
68
+ ).toBe(true);
69
+ });
70
+
71
+ test("returns true when yolo mode is on regardless of UI/subagent flags", () => {
72
+ expect(
73
+ canResolveAskPermissionRequest({
74
+ config: makeConfig(true),
75
+ hasUI: false,
76
+ isSubagent: false,
77
+ }),
78
+ ).toBe(true);
79
+ });
80
+
81
+ test("returns false when no UI, not a subagent, and yolo mode is off", () => {
82
+ expect(
83
+ canResolveAskPermissionRequest({
84
+ config: makeConfig(false),
85
+ hasUI: false,
86
+ isSubagent: false,
87
+ }),
88
+ ).toBe(false);
89
+ });
90
+
91
+ test("returns false when no UI, not a subagent, and yolo mode is undefined", () => {
92
+ expect(
93
+ canResolveAskPermissionRequest({
94
+ config: makeConfig(undefined),
95
+ hasUI: false,
96
+ isSubagent: false,
97
+ }),
98
+ ).toBe(false);
99
+ });
100
+
101
+ test("returns true when all three conditions are true", () => {
102
+ expect(
103
+ canResolveAskPermissionRequest({
104
+ config: makeConfig(true),
105
+ hasUI: true,
106
+ isSubagent: true,
107
+ }),
108
+ ).toBe(true);
109
+ });
110
+ });