@zhushanwen/pi-permission 0.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.
Files changed (51) hide show
  1. package/README.md +455 -0
  2. package/index.ts +2 -0
  3. package/package.json +52 -0
  4. package/src/__tests__/approval.test.ts +456 -0
  5. package/src/__tests__/commands.test.ts +487 -0
  6. package/src/__tests__/config.test.ts +229 -0
  7. package/src/__tests__/e2e-modes.test.ts +253 -0
  8. package/src/__tests__/extension.test.ts +170 -0
  9. package/src/__tests__/index-integration.test.ts +282 -0
  10. package/src/__tests__/model-picker.test.ts +371 -0
  11. package/src/__tests__/pipeline.test.ts +976 -0
  12. package/src/__tests__/production.test.ts +102 -0
  13. package/src/__tests__/rule-editor-component.test.ts +475 -0
  14. package/src/__tests__/rule-editor.test.ts +252 -0
  15. package/src/__tests__/rule-templates.test.ts +264 -0
  16. package/src/__tests__/statusline.test.ts +221 -0
  17. package/src/__tests__/types.test.ts +59 -0
  18. package/src/approval.ts +352 -0
  19. package/src/ast/__tests__/analyzer.test.ts +262 -0
  20. package/src/ast/__tests__/loader.test.ts +136 -0
  21. package/src/ast/analyzer.ts +326 -0
  22. package/src/ast/index.ts +7 -0
  23. package/src/ast/loader.ts +113 -0
  24. package/src/classifier/__tests__/classifier.test.ts +285 -0
  25. package/src/classifier/__tests__/json-parser.test.ts +118 -0
  26. package/src/classifier/__tests__/model-resolver.test.ts +173 -0
  27. package/src/classifier/__tests__/prompt.test.ts +59 -0
  28. package/src/classifier/classifier.ts +242 -0
  29. package/src/classifier/index.ts +28 -0
  30. package/src/classifier/json-parser.ts +127 -0
  31. package/src/classifier/model-resolver.ts +295 -0
  32. package/src/classifier/prompt.ts +52 -0
  33. package/src/commands.ts +234 -0
  34. package/src/config.ts +216 -0
  35. package/src/index.ts +291 -0
  36. package/src/model-picker.ts +398 -0
  37. package/src/pipeline.ts +541 -0
  38. package/src/production.ts +102 -0
  39. package/src/rule-editor-component.ts +883 -0
  40. package/src/rule-editor.ts +262 -0
  41. package/src/rule-templates.ts +332 -0
  42. package/src/rules/__tests__/builtins.test.ts +614 -0
  43. package/src/rules/__tests__/index.test.ts +26 -0
  44. package/src/rules/__tests__/matcher.test.ts +378 -0
  45. package/src/rules/__tests__/wildcard.test.ts +84 -0
  46. package/src/rules/builtins.ts +680 -0
  47. package/src/rules/index.ts +27 -0
  48. package/src/rules/matcher.ts +160 -0
  49. package/src/rules/wildcard.ts +43 -0
  50. package/src/statusline.ts +226 -0
  51. package/src/types.ts +249 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * WT1-WT23 + WT-concurrent: analyzeBashStructure 集成测试(跑真实 wasm)。
3
+ *
4
+ * 覆盖矩阵:
5
+ * - WT1-WT7: 干净命令(clean=true,commands 正确)
6
+ * - WT8-WT15: 危险结构(clean=false,dangerousStructures 含对应 node type)
7
+ * - WT16-WT19: malformed 语法(parseError=true)
8
+ * - WT20: 空命令
9
+ * - WT21-WT22: 超长边界
10
+ * - WT23: 多管道
11
+ * - WT-concurrent: 并发共享 Parser 单例不 race
12
+ *
13
+ * 真实跑 wasm(不 mock)—— 验证 tree-sitter-bash grammar 实际行为。
14
+ * beforeAll 预热 Parser 一次,避免第一个测试承担初始化耗时。
15
+ */
16
+ import { beforeAll, describe, expect, it } from "vitest";
17
+
18
+ import { analyzeBashStructure } from "../analyzer.js";
19
+ import { getBashParser } from "../loader.js";
20
+
21
+ beforeAll(async () => {
22
+ // 预热 Parser 单例(所有测试共享)
23
+ await getBashParser();
24
+ });
25
+
26
+ // ──────────────────────── WT1-WT7: 干净命令 ────────────────────────
27
+
28
+ describe("WT1-WT7: 干净命令(clean=true)", () => {
29
+ it("WT1: ls -la", async () => {
30
+ const r = await analyzeBashStructure("ls -la");
31
+ expect(r.clean).toBe(true);
32
+ expect(r.parseError).toBe(false);
33
+ expect(r.dangerousStructures).toEqual([]);
34
+ expect(r.commands).toEqual([["ls", "-la"]]);
35
+ });
36
+
37
+ it("WT2: echo hi", async () => {
38
+ const r = await analyzeBashStructure("echo hi");
39
+ expect(r.clean).toBe(true);
40
+ expect(r.commands).toEqual([["echo", "hi"]]);
41
+ });
42
+
43
+ it("WT3: git status", async () => {
44
+ const r = await analyzeBashStructure("git status");
45
+ expect(r.clean).toBe(true);
46
+ expect(r.commands).toEqual([["git", "status"]]);
47
+ });
48
+
49
+ it("WT4: cmd1 && cmd2", async () => {
50
+ const r = await analyzeBashStructure("cmd1 && cmd2");
51
+ expect(r.clean).toBe(true);
52
+ expect(r.commands).toEqual([["cmd1"], ["cmd2"]]);
53
+ });
54
+
55
+ it("WT5: echo 'str' | wc -l", async () => {
56
+ const r = await analyzeBashStructure("echo 'str' | wc -l");
57
+ expect(r.clean).toBe(true);
58
+ expect(r.commands).toEqual([
59
+ ["echo", "str"],
60
+ ["wc", "-l"],
61
+ ]);
62
+ });
63
+
64
+ it("WT6: -g\"*.py\"(concatenation flag + 双引号)", async () => {
65
+ const r = await analyzeBashStructure('rg -g"*.py"');
66
+ expect(r.clean).toBe(true);
67
+ expect(r.commands).toEqual([["rg", "-g*.py"]]);
68
+ });
69
+
70
+ it("WT7: cd /tmp", async () => {
71
+ const r = await analyzeBashStructure("cd /tmp");
72
+ expect(r.clean).toBe(true);
73
+ expect(r.commands).toEqual([["cd", "/tmp"]]);
74
+ });
75
+ });
76
+
77
+ // ──────────────────────── WT8-WT15: 危险结构 ────────────────────────
78
+
79
+ describe("WT8-WT15: 危险结构(clean=false)", () => {
80
+ it("WT8: \$(pwd) —— command_substitution", async () => {
81
+ const r = await analyzeBashStructure("echo $(pwd)");
82
+ expect(r.clean).toBe(false);
83
+ expect(r.parseError).toBe(false);
84
+ expect(r.dangerousStructures).toContain("command_substitution");
85
+ });
86
+
87
+ it("WT9: `pwd` —— backtick command_substitution", async () => {
88
+ const r = await analyzeBashStructure("echo `pwd`");
89
+ expect(r.clean).toBe(false);
90
+ expect(r.dangerousStructures.length).toBeGreaterThan(0);
91
+ });
92
+
93
+ it("WT10: ls > out —— file_redirect", async () => {
94
+ const r = await analyzeBashStructure("ls > out");
95
+ expect(r.clean).toBe(false);
96
+ expect(r.dangerousStructures).toContain("file_redirect");
97
+ });
98
+
99
+ it("WT11: (ls) —— subshell", async () => {
100
+ const r = await analyzeBashStructure("(ls)");
101
+ expect(r.clean).toBe(false);
102
+ // subshell 节点 + 括号 anonymous token
103
+ expect(r.dangerousStructures).toContain("subshell");
104
+ });
105
+
106
+ it("WT12: FOO=bar ls —— variable_assignment", async () => {
107
+ const r = await analyzeBashStructure("FOO=bar ls");
108
+ expect(r.clean).toBe(false);
109
+ expect(r.dangerousStructures).toContain("variable_assignment");
110
+ });
111
+
112
+ it("WT13: echo a>b —— 内联重定向", async () => {
113
+ const r = await analyzeBashStructure("echo a>b");
114
+ expect(r.clean).toBe(false);
115
+ expect(r.dangerousStructures).toContain("file_redirect");
116
+ });
117
+
118
+ it("WT14: { ls; } —— brace compound(语法变体)", async () => {
119
+ // 注:`{ls;}` 无空格 grammar 可能不识别,用标准 `{ ls; }`
120
+ const r = await analyzeBashStructure("{ ls; }");
121
+ expect(r.clean).toBe(false);
122
+ expect(r.dangerousStructures.length).toBeGreaterThan(0);
123
+ });
124
+
125
+ it("WT15: [[ x ]] —— test_expression", async () => {
126
+ const r = await analyzeBashStructure("[[ x ]]");
127
+ expect(r.clean).toBe(false);
128
+ expect(r.dangerousStructures.length).toBeGreaterThan(0);
129
+ });
130
+
131
+ it("WT15b: echo $HOME —— simple_expansion(裸变量展开)", async () => {
132
+ // $HOME 在双引号外 → tree-sitter-bash 产出 simple_expansion named node(非白名单)
133
+ const r = await analyzeBashStructure("echo $HOME");
134
+ expect(r.clean).toBe(false);
135
+ expect(r.parseError).toBe(false);
136
+ expect(r.dangerousStructures).toContain("simple_expansion");
137
+ });
138
+ });
139
+
140
+ // ──────────────────────── WT16-WT19: malformed ────────────────────────
141
+
142
+ describe("WT16-WT19: malformed 语法(parseError=true)", () => {
143
+ it("WT16: ls && (trailing operator)", async () => {
144
+ const r = await analyzeBashStructure("ls &&");
145
+ expect(r.parseError).toBe(true);
146
+ expect(r.clean).toBe(false);
147
+ expect(r.dangerousStructures).toContain("ERROR");
148
+ });
149
+
150
+ it("WT17: ((incomplete —— 不完整括号", async () => {
151
+ const r = await analyzeBashStructure("((incomplete");
152
+ expect(r.parseError).toBe(true);
153
+ expect(r.clean).toBe(false);
154
+ });
155
+
156
+ it("WT18: unclosed \" —— 未闭合双引号", async () => {
157
+ const r = await analyzeBashStructure('echo "unclosed');
158
+ expect(r.parseError).toBe(true);
159
+ expect(r.clean).toBe(false);
160
+ });
161
+
162
+ it("WT19: 语法错误 token", async () => {
163
+ const r = await analyzeBashStructure(";;;");
164
+ expect(r.parseError).toBe(true);
165
+ expect(r.clean).toBe(false);
166
+ });
167
+ });
168
+
169
+ // ──────────────────────── WT20: 空命令 ────────────────────────
170
+
171
+ describe("WT20: 空命令", () => {
172
+ it("WT20: '' 空字符串", async () => {
173
+ const r = await analyzeBashStructure("");
174
+ expect(r.clean).toBe(true);
175
+ expect(r.commands).toEqual([]);
176
+ expect(r.dangerousStructures).toEqual([]);
177
+ expect(r.parseError).toBe(false);
178
+ });
179
+
180
+ it("WT20b: 纯空白", async () => {
181
+ const r = await analyzeBashStructure(" \t\n ");
182
+ expect(r.clean).toBe(true);
183
+ expect(r.commands).toEqual([]);
184
+ expect(r.dangerousStructures).toEqual([]);
185
+ expect(r.parseError).toBe(false);
186
+ });
187
+ });
188
+
189
+ // ──────────────────────── WT21-WT22: 超长边界 ────────────────────────
190
+
191
+ describe("WT21-WT22: 超长边界", () => {
192
+ it("WT21: 恰好 65536 字节(边界值,应可解析)", async () => {
193
+ // 65536 字节的合法命令,padEnd 到精确长度
194
+ const exact = "ls".padEnd(65536, " x");
195
+ expect(exact.length).toBe(65536);
196
+ const r = await analyzeBashStructure(exact);
197
+ // 边界值不超长 → 走正常解析(clean 取决于 grammar,但 parseError 不应是 INPUT_TOO_LONG)
198
+ expect(r.dangerousStructures).not.toContain("INPUT_TOO_LONG");
199
+ });
200
+
201
+ it("WT22: 65537 字节(超长,fail-closed)", async () => {
202
+ const cmd = "x".repeat(65537);
203
+ expect(cmd.length).toBe(65537);
204
+ const r = await analyzeBashStructure(cmd);
205
+ expect(r.clean).toBe(false);
206
+ expect(r.parseError).toBe(true);
207
+ expect(r.dangerousStructures).toContain("INPUT_TOO_LONG");
208
+ expect(r.commands).toEqual([]);
209
+ });
210
+ });
211
+
212
+ // ──────────────────────── WT23: 多管道 ────────────────────────
213
+
214
+ describe("WT23: 多管道(commands 顺序正确)", () => {
215
+ it("WT23: a | b | c", async () => {
216
+ const r = await analyzeBashStructure("a | b | c");
217
+ expect(r.clean).toBe(true);
218
+ expect(r.commands).toEqual([["a"], ["b"], ["c"]]);
219
+ });
220
+
221
+ it("WT23b: 复杂序列 ls && pwd; echo 'hi' | wc -l", async () => {
222
+ const r = await analyzeBashStructure("ls && pwd; echo 'hi' | wc -l");
223
+ expect(r.clean).toBe(true);
224
+ expect(r.commands).toEqual([
225
+ ["ls"],
226
+ ["pwd"],
227
+ ["echo", "hi"],
228
+ ["wc", "-l"],
229
+ ]);
230
+ });
231
+ });
232
+
233
+ // ──────────────────────── WT-concurrent: 并发 ────────────────────────
234
+
235
+ describe("WT-concurrent: 并发共享 Parser 单例", () => {
236
+ it("多次并行 analyzeBashStructure 返回各自正确结果(不 race)", async () => {
237
+ const inputs = [
238
+ "ls -la",
239
+ "echo hi && echo bye",
240
+ "git status",
241
+ "echo $(pwd)", // 危险
242
+ "a | b | c",
243
+ "FOO=bar ls", // 危险
244
+ 'rg -g"*.py"',
245
+ "",
246
+ ];
247
+ const results = await Promise.all(inputs.map((c) => analyzeBashStructure(c)));
248
+
249
+ // 每个 result 必须与对应的单次调用结果一致
250
+ expect(results[0].commands).toEqual([["ls", "-la"]]);
251
+ expect(results[0].clean).toBe(true);
252
+ expect(results[1].commands).toEqual([["echo", "hi"], ["echo", "bye"]]);
253
+ expect(results[1].clean).toBe(true);
254
+ expect(results[2].commands).toEqual([["git", "status"]]);
255
+ expect(results[3].clean).toBe(false);
256
+ expect(results[4].commands).toEqual([["a"], ["b"], ["c"]]);
257
+ expect(results[5].clean).toBe(false);
258
+ expect(results[6].commands).toEqual([["rg", "-g*.py"]]);
259
+ expect(results[7].clean).toBe(true);
260
+ expect(results[7].commands).toEqual([]);
261
+ });
262
+ });
@@ -0,0 +1,136 @@
1
+ /**
2
+ * WT24-WT27: loader 单元测试。
3
+ *
4
+ * - WT24: resolveWasmPaths 返回正确路径(文件/目录存在)
5
+ * - WT25: getBashParser 单例缓存(第二次返回同一 promise)
6
+ * - WT26: 并发调用共享同一 promise(Promise.all 返回同一 parser 实例)
7
+ * - WT27: 加载失败降级(篡改 require.resolve 抛错 → 返回 null,不 throw)
8
+ *
9
+ * WT27 用 vi.resetModules + 动态 import 隔离模块状态,避免污染 WT24-WT26 的单例缓存。
10
+ */
11
+ import { existsSync, statSync } from "node:fs";
12
+ import { dirname } from "node:path";
13
+
14
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
15
+
16
+ import { getBashParser, resolveWasmPaths } from "../loader.js";
17
+
18
+ // ──────────────────────── WT24: resolveWasmPaths ────────────────────────
19
+
20
+ describe("WT24: resolveWasmPaths 返回正确路径", () => {
21
+ it("bashWasmPath 是存在的文件", () => {
22
+ const { bashWasmPath } = resolveWasmPaths();
23
+ expect(bashWasmPath).toContain("tree-sitter-bash.wasm");
24
+ expect(existsSync(bashWasmPath)).toBe(true);
25
+ const stat = statSync(bashWasmPath);
26
+ expect(stat.isFile()).toBe(true);
27
+ expect(stat.size).toBeGreaterThan(0);
28
+ });
29
+
30
+ it("runtimeWasmDir 是存在的目录,含 web-tree-sitter.wasm(0.26 起的运行时 wasm)", () => {
31
+ const { runtimeWasmDir } = resolveWasmPaths();
32
+ expect(existsSync(runtimeWasmDir)).toBe(true);
33
+ // 0.26 起 wasm 与 .js 同名:web-tree-sitter.wasm
34
+ expect(existsSync(`${runtimeWasmDir}/web-tree-sitter.wasm`)).toBe(true);
35
+ const stat = statSync(runtimeWasmDir);
36
+ expect(stat.isDirectory()).toBe(true);
37
+ });
38
+
39
+ it("runtimeWasmDir 在 web-tree-sitter 包内(dirname 校验)", () => {
40
+ const { runtimeWasmDir } = resolveWasmPaths();
41
+ // dirname(运行时目录) 应该是 web-tree-sitter 包根 或 node_modules 父目录
42
+ expect(runtimeWasmDir).toContain("web-tree-sitter");
43
+ // 确认 dirname 是个有效的目录(路径结构合理)
44
+ expect(existsSync(dirname(runtimeWasmDir))).toBe(true);
45
+ });
46
+ });
47
+
48
+ // ──────────────────────── WT25-WT26: 单例缓存 + 并发 ────────────────────────
49
+
50
+ describe("WT25-WT26: getBashParser 单例 + 并发", () => {
51
+ beforeAll(async () => {
52
+ // 确保单例已初始化(后续测试复用)
53
+ const parser = await getBashParser();
54
+ expect(parser).not.toBeNull();
55
+ });
56
+
57
+ afterAll(async () => {
58
+ // 确保单例在所有测试结束后仍可用(无泄漏导致 null)
59
+ const parser = await getBashParser();
60
+ expect(parser).not.toBeNull();
61
+ });
62
+
63
+ it("WT25: 第二次调用返回同一 promise(缓存生效)", async () => {
64
+ const p1 = getBashParser();
65
+ const p2 = getBashParser();
66
+ expect(p1).toBe(p2); // 同一 promise 引用
67
+ const parser1 = await p1;
68
+ const parser2 = await p2;
69
+ expect(parser1).toBe(parser2); // 同一 parser 实例
70
+ expect(parser1).not.toBeNull();
71
+ });
72
+
73
+ it("WT26: 并发 Promise.all 共享同一 parser 实例", async () => {
74
+ // 注意:单例已缓存时 Promise.all 全是同一 promise;
75
+ // 这里验证即使并发也返回同一实例(不 race)。
76
+ const [a, b, c] = await Promise.all([
77
+ getBashParser(),
78
+ getBashParser(),
79
+ getBashParser(),
80
+ ]);
81
+ expect(a).toBe(b);
82
+ expect(b).toBe(c);
83
+ expect(a).not.toBeNull();
84
+ });
85
+
86
+ it("WT26b: parser 可重复 parse 不同输入(无状态污染)", async () => {
87
+ const parser = await getBashParser();
88
+ expect(parser).not.toBeNull();
89
+ // 同一 parser 解析两条不同命令,都应成功
90
+ const t1 = parser?.parse("ls -la");
91
+ const t2 = parser?.parse("echo hi && pwd");
92
+ expect(t1).not.toBeNull();
93
+ expect(t2).not.toBeNull();
94
+ expect(t1?.rootNode.type).toBe("program");
95
+ expect(t2?.rootNode.type).toBe("program");
96
+ t1?.delete();
97
+ t2?.delete();
98
+ });
99
+ });
100
+
101
+ // ──────────────────────── WT27: 加载失败降级 ────────────────────────
102
+
103
+ describe("WT27: 加载失败降级(fail-closed,返回 null 不 throw)", () => {
104
+ it("resolveWasmPaths 对不存在的包抛错", () => {
105
+ // 验证 resolveWasmPaths 本身在路径不存在时是 throw(fail-loud),
106
+ // 由 getBashParser 捕获后 fail-closed。
107
+ // 这里只验证契约:resolveWasmPaths 当前能解析成功(包已装)。
108
+ // 真正的降级路径在下个测试通过模块隔离验证。
109
+ expect(() => resolveWasmPaths()).not.toThrow();
110
+ });
111
+
112
+ it("getBashParser 在初始化失败时返回 null(模块隔离测试)", async () => {
113
+ // 用动态 import + vi.resetModules 拿到独立的 loader 模块副本,
114
+ // 篡改其内部 Parser.init 使其抛错,验证 getBashParser 降级返回 null。
115
+ const { vi } = await import("vitest");
116
+ vi.resetModules();
117
+
118
+ // mock web-tree-sitter 让 Parser.init 抛错
119
+ vi.doMock("web-tree-sitter", () => ({
120
+ Parser: {
121
+ init: () => Promise.reject(new Error("mock init failure")),
122
+ },
123
+ Language: { load: () => Promise.reject(new Error("never reached")) },
124
+ }));
125
+
126
+ try {
127
+ const isolatedLoader = await import("../loader.js");
128
+ const parser = await isolatedLoader.getBashParser();
129
+ // fail-closed:返回 null,不 throw
130
+ expect(parser).toBeNull();
131
+ } finally {
132
+ vi.doUnmock("web-tree-sitter");
133
+ vi.resetModules();
134
+ }
135
+ });
136
+ });