@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.
- package/README.md +455 -0
- package/index.ts +2 -0
- package/package.json +52 -0
- package/src/__tests__/approval.test.ts +456 -0
- package/src/__tests__/commands.test.ts +487 -0
- package/src/__tests__/config.test.ts +229 -0
- package/src/__tests__/e2e-modes.test.ts +253 -0
- package/src/__tests__/extension.test.ts +170 -0
- package/src/__tests__/index-integration.test.ts +282 -0
- package/src/__tests__/model-picker.test.ts +371 -0
- package/src/__tests__/pipeline.test.ts +976 -0
- package/src/__tests__/production.test.ts +102 -0
- package/src/__tests__/rule-editor-component.test.ts +475 -0
- package/src/__tests__/rule-editor.test.ts +252 -0
- package/src/__tests__/rule-templates.test.ts +264 -0
- package/src/__tests__/statusline.test.ts +221 -0
- package/src/__tests__/types.test.ts +59 -0
- package/src/approval.ts +352 -0
- package/src/ast/__tests__/analyzer.test.ts +262 -0
- package/src/ast/__tests__/loader.test.ts +136 -0
- package/src/ast/analyzer.ts +326 -0
- package/src/ast/index.ts +7 -0
- package/src/ast/loader.ts +113 -0
- package/src/classifier/__tests__/classifier.test.ts +285 -0
- package/src/classifier/__tests__/json-parser.test.ts +118 -0
- package/src/classifier/__tests__/model-resolver.test.ts +173 -0
- package/src/classifier/__tests__/prompt.test.ts +59 -0
- package/src/classifier/classifier.ts +242 -0
- package/src/classifier/index.ts +28 -0
- package/src/classifier/json-parser.ts +127 -0
- package/src/classifier/model-resolver.ts +295 -0
- package/src/classifier/prompt.ts +52 -0
- package/src/commands.ts +234 -0
- package/src/config.ts +216 -0
- package/src/index.ts +291 -0
- package/src/model-picker.ts +398 -0
- package/src/pipeline.ts +541 -0
- package/src/production.ts +102 -0
- package/src/rule-editor-component.ts +883 -0
- package/src/rule-editor.ts +262 -0
- package/src/rule-templates.ts +332 -0
- package/src/rules/__tests__/builtins.test.ts +614 -0
- package/src/rules/__tests__/index.test.ts +26 -0
- package/src/rules/__tests__/matcher.test.ts +378 -0
- package/src/rules/__tests__/wildcard.test.ts +84 -0
- package/src/rules/builtins.ts +680 -0
- package/src/rules/index.ts +27 -0
- package/src/rules/matcher.ts +160 -0
- package/src/rules/wildcard.ts +43 -0
- package/src/statusline.ts +226 -0
- package/src/types.ts +249 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rule-editor.test.ts — W8 T8: rule-editor 三模式分发 + RPC 循环测试。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - editRulesViaOverlay headless 降级(notify + undefined)
|
|
6
|
+
* - editRulesViaOverlay TUI 模式(mock custom → done)
|
|
7
|
+
* - editViaRpc:cancel(第一次 select undefined)
|
|
8
|
+
* - editViaRpc:[Done] → undefined(无变更)
|
|
9
|
+
* - editViaRpc:[+ Add rule] → 选模板 → 选命令 → ops
|
|
10
|
+
* - editViaRpc:选已有规则 → delete → ops
|
|
11
|
+
* - editViaRpc:G16 多循环反映前序 ops
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it, vi } from "vitest";
|
|
14
|
+
|
|
15
|
+
import { editRulesViaOverlay,type RuleEditorContext } from "../rule-editor.js";
|
|
16
|
+
import type { Rule } from "../types.js";
|
|
17
|
+
|
|
18
|
+
// ──────────────────────── helpers ────────────────────────
|
|
19
|
+
|
|
20
|
+
function makeRule(overrides: Partial<Rule> = {}): Rule {
|
|
21
|
+
return {
|
|
22
|
+
id: "user-1",
|
|
23
|
+
tool: "bash",
|
|
24
|
+
pattern: "npm *",
|
|
25
|
+
action: "allow",
|
|
26
|
+
source: "user",
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeCtx(overrides: Partial<RuleEditorContext> = {}): RuleEditorContext {
|
|
32
|
+
const ctx: RuleEditorContext = {
|
|
33
|
+
mode: overrides.mode ?? "rpc",
|
|
34
|
+
ui: {
|
|
35
|
+
notify: overrides.ui?.notify ?? vi.fn(),
|
|
36
|
+
select: overrides.ui?.select ?? vi.fn(() => Promise.resolve(undefined)),
|
|
37
|
+
custom: overrides.ui?.custom ?? vi.fn(() => Promise.resolve(undefined)),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
// 透传可选 input(M7:rule-editor custom 模板用真实文本输入)
|
|
41
|
+
if (overrides.ui?.input !== undefined) {
|
|
42
|
+
ctx.ui.input = overrides.ui.input;
|
|
43
|
+
}
|
|
44
|
+
return ctx;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeCounter(): () => string {
|
|
48
|
+
let n = 1;
|
|
49
|
+
return (): string => `user-${n++}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ──────────────────────── headless 降级 ────────────────────────
|
|
53
|
+
|
|
54
|
+
describe("RE1: headless 模式降级", () => {
|
|
55
|
+
it("json mode → notify + undefined", async () => {
|
|
56
|
+
const notify = vi.fn();
|
|
57
|
+
const ctx = makeCtx({ mode: "json", ui: { notify, select: vi.fn(), custom: vi.fn() } });
|
|
58
|
+
const result = await editRulesViaOverlay(ctx, [], makeCounter());
|
|
59
|
+
expect(result).toBeUndefined();
|
|
60
|
+
expect(notify).toHaveBeenCalledWith(expect.stringContaining("not available in headless"), "warning");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("print mode → notify + undefined", async () => {
|
|
64
|
+
const notify = vi.fn();
|
|
65
|
+
const ctx = makeCtx({ mode: "print", ui: { notify, select: vi.fn(), custom: vi.fn() } });
|
|
66
|
+
const result = await editRulesViaOverlay(ctx, [], makeCounter());
|
|
67
|
+
expect(result).toBeUndefined();
|
|
68
|
+
expect(notify).toHaveBeenCalledOnce();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ──────────────────────── TUI 模式 ────────────────────────
|
|
73
|
+
|
|
74
|
+
describe("RE2: TUI 模式", () => {
|
|
75
|
+
it("custom factory 被调用,comp cancel settle 空 ops", async () => {
|
|
76
|
+
const customMock = vi.fn(
|
|
77
|
+
<T,>(factory: (tui: unknown, theme: unknown, kb: unknown, done: (r: T) => void) => unknown) =>
|
|
78
|
+
new Promise<T>((resolve) => {
|
|
79
|
+
const comp = factory({}, {}, {}, (r: T) => resolve(r));
|
|
80
|
+
// 模拟 cancel:comp 是 RuleEditorComponent,调 cancel → done(ops)
|
|
81
|
+
if (comp !== null && typeof comp === "object" && "cancel" in comp) {
|
|
82
|
+
(comp as { cancel: () => void }).cancel();
|
|
83
|
+
}
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
const ctx = makeCtx({ mode: "tui", ui: { notify: vi.fn(), select: vi.fn(), custom: customMock } });
|
|
87
|
+
const result = await editRulesViaOverlay(ctx, [], makeCounter());
|
|
88
|
+
expect(customMock).toHaveBeenCalledOnce();
|
|
89
|
+
expect(result).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ──────────────────────── RPC: cancel ────────────────────────
|
|
94
|
+
|
|
95
|
+
describe("RE3: RPC cancel", () => {
|
|
96
|
+
it("第一次 select undefined → 返回 undefined", async () => {
|
|
97
|
+
const ctx = makeCtx({
|
|
98
|
+
mode: "rpc",
|
|
99
|
+
ui: { notify: vi.fn(), select: vi.fn(() => Promise.resolve(undefined)), custom: vi.fn() },
|
|
100
|
+
});
|
|
101
|
+
const result = await editRulesViaOverlay(ctx, [makeRule()], makeCounter());
|
|
102
|
+
expect(result).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ──────────────────────── RPC: [Done] ────────────────────────
|
|
107
|
+
|
|
108
|
+
describe("RE4: RPC [Done]", () => {
|
|
109
|
+
it("选 [Done](无变更)→ undefined", async () => {
|
|
110
|
+
const selectMock = vi.fn()
|
|
111
|
+
.mockResolvedValueOnce("[Done]"); // list 选 [Done]
|
|
112
|
+
const ctx = makeCtx({
|
|
113
|
+
mode: "rpc",
|
|
114
|
+
ui: { notify: vi.fn(), select: selectMock, custom: vi.fn() },
|
|
115
|
+
});
|
|
116
|
+
const result = await editRulesViaOverlay(ctx, [makeRule()], makeCounter());
|
|
117
|
+
expect(result).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ──────────────────────── RPC: add flow ────────────────────────
|
|
122
|
+
|
|
123
|
+
describe("RE5: RPC add flow", () => {
|
|
124
|
+
it("[+ Add rule] → 选 allow-family → 选 npm → ops 含 add", async () => {
|
|
125
|
+
// rpcAddFlow 调用序列(allow-family 模板,无 scope 选择):
|
|
126
|
+
// list → template → rpcSelectCommand[mode: Browse] → rpcSelectCommand[cmd: npm] → 第二次 list
|
|
127
|
+
// 注:rpcSelectCommand 现先问 "How to select command?"(Browse / Type-to-search),多一步 select。
|
|
128
|
+
// allow-family 模板在 build 前不再追加 scope 选择(仅 deny-family/allow-subcmd 需要)。
|
|
129
|
+
const selectMock = vi.fn()
|
|
130
|
+
.mockResolvedValueOnce("[+ Add rule]") // list(call 0)
|
|
131
|
+
.mockResolvedValueOnce("Allow command family") // template(call 1)
|
|
132
|
+
.mockResolvedValueOnce("[Browse list]") // rpcSelectCommand mode(call 2)
|
|
133
|
+
.mockResolvedValueOnce("npm (Node package manager)") // command(call 3)
|
|
134
|
+
.mockResolvedValueOnce("[Done]"); // list 第二次循环(call 4)
|
|
135
|
+
const ctx = makeCtx({
|
|
136
|
+
mode: "rpc",
|
|
137
|
+
ui: { notify: vi.fn(), select: selectMock, custom: vi.fn() },
|
|
138
|
+
});
|
|
139
|
+
const result = await editRulesViaOverlay(ctx, [], makeCounter());
|
|
140
|
+
expect(result).toBeDefined();
|
|
141
|
+
expect(result).toHaveLength(1);
|
|
142
|
+
expect(result![0]!.kind).toBe("add");
|
|
143
|
+
expect(result![0]!.rule.pattern).toBe("npm *");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ──────────────────────── RPC: delete flow ────────────────────────
|
|
148
|
+
|
|
149
|
+
describe("RE6: RPC delete flow", () => {
|
|
150
|
+
it("选已有规则 → [Delete this rule] → ops 含 delete", async () => {
|
|
151
|
+
const existingRule = makeRule({ id: "user-1", pattern: "npm *" });
|
|
152
|
+
const selectMock = vi.fn()
|
|
153
|
+
.mockResolvedValueOnce("[allow] npm *") // list 选规则
|
|
154
|
+
.mockResolvedValueOnce("[Delete this rule]") // edit action
|
|
155
|
+
.mockResolvedValueOnce("[Done]"); // list(第二次循环)
|
|
156
|
+
const ctx = makeCtx({
|
|
157
|
+
mode: "rpc",
|
|
158
|
+
ui: { notify: vi.fn(), select: selectMock, custom: vi.fn() },
|
|
159
|
+
});
|
|
160
|
+
const result = await editRulesViaOverlay(ctx, [existingRule], makeCounter());
|
|
161
|
+
expect(result).toBeDefined();
|
|
162
|
+
expect(result).toHaveLength(1);
|
|
163
|
+
expect(result![0]!.kind).toBe("delete");
|
|
164
|
+
expect(result![0]!.id).toBe("user-1");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ──────────────────────── RPC: G16 多循环反映前序 ops ────────────────────────
|
|
169
|
+
|
|
170
|
+
describe("RE7: G16 多循环反映前序 ops", () => {
|
|
171
|
+
it("add 后第二次 list 包含新规则", async () => {
|
|
172
|
+
// ask-before 模板调用序列(无 scope 选择):
|
|
173
|
+
// list → template → rpcSelectCommand[mode: Browse] → rpcSelectCommand[cmd: docker] → 第二次 list
|
|
174
|
+
// rpcSelectCommand 现多一步 mode 选择,故第二次 list 落在 call 4(非 call 3)。
|
|
175
|
+
const selectMock = vi.fn()
|
|
176
|
+
.mockResolvedValueOnce("[+ Add rule]") // list 1 (call 0)
|
|
177
|
+
.mockResolvedValueOnce("Ask before command") // template (call 1)
|
|
178
|
+
.mockResolvedValueOnce("[Browse list]") // rpcSelectCommand mode (call 2)
|
|
179
|
+
.mockResolvedValueOnce("docker (container runtime)") // command (call 3)
|
|
180
|
+
.mockResolvedValueOnce("[Done]"); // list 2 (call 4, 应含新规则)
|
|
181
|
+
const ctx = makeCtx({
|
|
182
|
+
mode: "rpc",
|
|
183
|
+
ui: { notify: vi.fn(), select: selectMock, custom: vi.fn() },
|
|
184
|
+
});
|
|
185
|
+
await editRulesViaOverlay(ctx, [], makeCounter());
|
|
186
|
+
// 第五次 select(index 4)是第二次 list,options 应包含新规则
|
|
187
|
+
const secondListArgs = selectMock.mock.calls[4] as [string, string[]];
|
|
188
|
+
expect(secondListArgs[1]).toContain("[ask] docker *");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ──────────────────────── RPC: custom 模板用真实 ctx.ui.input(M7) ────────────────────────
|
|
193
|
+
|
|
194
|
+
describe("RE8: RPC custom 模板用真实 ctx.ui.input(M7)", () => {
|
|
195
|
+
it("ctx.ui.input 可用 → custom 模板 pattern/description 走真实 input(不 fallback select)", async () => {
|
|
196
|
+
const selectMock = vi.fn()
|
|
197
|
+
.mockResolvedValueOnce("[+ Add rule]") // list
|
|
198
|
+
.mockResolvedValueOnce("Custom (advanced)") // template
|
|
199
|
+
.mockResolvedValueOnce("allow") // action select
|
|
200
|
+
.mockResolvedValueOnce("[Done]"); // list(第二次循环)
|
|
201
|
+
const inputMock = vi.fn()
|
|
202
|
+
.mockResolvedValueOnce("foo *") // pattern input
|
|
203
|
+
.mockResolvedValueOnce("my rule"); // description input
|
|
204
|
+
const ctx = makeCtx({
|
|
205
|
+
mode: "rpc",
|
|
206
|
+
ui: {
|
|
207
|
+
notify: vi.fn(),
|
|
208
|
+
select: selectMock,
|
|
209
|
+
custom: vi.fn(),
|
|
210
|
+
input: inputMock,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
const result = await editRulesViaOverlay(ctx, [], makeCounter());
|
|
214
|
+
expect(result).toBeDefined();
|
|
215
|
+
expect(result).toHaveLength(1);
|
|
216
|
+
expect(result![0]!.kind).toBe("add");
|
|
217
|
+
// custom 模板的 pattern 来自真实 input
|
|
218
|
+
expect(result![0]!.rule.pattern).toBe("foo *");
|
|
219
|
+
// input 被调 2 次(pattern + description),且首参是提示文案
|
|
220
|
+
expect(inputMock).toHaveBeenCalledTimes(2);
|
|
221
|
+
expect(inputMock.mock.calls[0]![0]).toContain("Pattern");
|
|
222
|
+
expect(inputMock.mock.calls[1]![0]).toContain("Description");
|
|
223
|
+
// select 不应被当作 fallback input(pattern/description 走 input,非 select)
|
|
224
|
+
// select 仅用于:list / template / action / 第二次 list,无一含 Pattern/Description
|
|
225
|
+
for (const call of selectMock.mock.calls) {
|
|
226
|
+
const title = call[0] as string;
|
|
227
|
+
expect(title).not.toContain("Pattern");
|
|
228
|
+
expect(title).not.toContain("Description");
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("ctx.ui.input 缺失 → fallback 用 select 占位(保底,不崩)", async () => {
|
|
233
|
+
// input 降级为单选项 select:返回占位符文本(= 用户"选了"占位符)
|
|
234
|
+
const selectMock = vi.fn()
|
|
235
|
+
.mockResolvedValueOnce("[+ Add rule]") // list
|
|
236
|
+
.mockResolvedValueOnce("Custom (advanced)") // template
|
|
237
|
+
// pattern fallback select 返回占位符 "npm *"
|
|
238
|
+
.mockResolvedValueOnce("npm *")
|
|
239
|
+
.mockResolvedValueOnce("allow") // action select
|
|
240
|
+
// description fallback select 返回占位符 ""(空)→ 模板 description=undefined
|
|
241
|
+
.mockResolvedValueOnce("")
|
|
242
|
+
.mockResolvedValueOnce("[Done]"); // list(第二次循环)
|
|
243
|
+
const ctx = makeCtx({
|
|
244
|
+
mode: "rpc",
|
|
245
|
+
ui: { notify: vi.fn(), select: selectMock, custom: vi.fn() },
|
|
246
|
+
});
|
|
247
|
+
const result = await editRulesViaOverlay(ctx, [], makeCounter());
|
|
248
|
+
expect(result).toBeDefined();
|
|
249
|
+
expect(result).toHaveLength(1);
|
|
250
|
+
expect(result![0]!.rule.pattern).toBe("npm *");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rule-templates.test.ts — W8 T6: rule-templates 单元测试。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - PRESET_COMMANDS 结构(31 命令,7 类 + Other)
|
|
6
|
+
* - 5 个模板 build(G4 wildcard pattern)
|
|
7
|
+
* - classifyRuleTemplate(FAMILY_RE / SUBCMD_RE + action 映射)
|
|
8
|
+
* - applyOps(add / edit / delete,纯函数)
|
|
9
|
+
* - makeNextIdCounter(从已有规则提取最大 user-N)
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
allowFamilyTemplate,
|
|
15
|
+
allowSubcmdTemplate,
|
|
16
|
+
applyOps,
|
|
17
|
+
askBeforeTemplate,
|
|
18
|
+
classifyRuleTemplate,
|
|
19
|
+
customTemplate,
|
|
20
|
+
denyFamilyTemplate,
|
|
21
|
+
makeNextIdCounter,
|
|
22
|
+
PRESET_COMMANDS,
|
|
23
|
+
} from "../rule-templates.js";
|
|
24
|
+
import type { Rule } from "../types.js";
|
|
25
|
+
|
|
26
|
+
// ──────────────────────── helpers ────────────────────────
|
|
27
|
+
|
|
28
|
+
function makeRule(overrides: Partial<Rule> = {}): Rule {
|
|
29
|
+
return {
|
|
30
|
+
id: "user-1",
|
|
31
|
+
tool: "bash",
|
|
32
|
+
pattern: "npm *",
|
|
33
|
+
action: "allow",
|
|
34
|
+
source: "user",
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ──────────────────────── PRESET_COMMANDS ────────────────────────
|
|
40
|
+
|
|
41
|
+
describe("RT1: PRESET_COMMANDS", () => {
|
|
42
|
+
it("共 53 个命令", () => {
|
|
43
|
+
expect(PRESET_COMMANDS).toHaveLength(53);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("每个命令有 cmd / label / category", () => {
|
|
47
|
+
for (const cmd of PRESET_COMMANDS) {
|
|
48
|
+
expect(typeof cmd.cmd).toBe("string");
|
|
49
|
+
expect(cmd.cmd.length).toBeGreaterThan(0);
|
|
50
|
+
expect(typeof cmd.label).toBe("string");
|
|
51
|
+
expect(typeof cmd.category).toBe("string");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("涵盖 9 个 category", () => {
|
|
56
|
+
const categories = new Set(PRESET_COMMANDS.map((c) => c.category));
|
|
57
|
+
expect(categories.size).toBe(9);
|
|
58
|
+
expect(categories.has("Package Managers")).toBe(true);
|
|
59
|
+
expect(categories.has("Containers & Cloud")).toBe(true);
|
|
60
|
+
expect(categories.has("Version Control")).toBe(true);
|
|
61
|
+
expect(categories.has("Build & Tasks")).toBe(true);
|
|
62
|
+
expect(categories.has("Network & Download")).toBe(true);
|
|
63
|
+
expect(categories.has("Filesystem")).toBe(true);
|
|
64
|
+
expect(categories.has("Runtime")).toBe(true);
|
|
65
|
+
expect(categories.has("Text Processing")).toBe(true);
|
|
66
|
+
expect(categories.has("System & Info")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("cmd 不含空格(单 token)", () => {
|
|
70
|
+
for (const cmd of PRESET_COMMANDS) {
|
|
71
|
+
expect(cmd.cmd).not.toMatch(/\s/);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ──────────────────────── 模板 build(G4 wildcard) ────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("RT2: 模板 build(G4 wildcard pattern)", () => {
|
|
79
|
+
it("allow-family:{tool:'bash', pattern:'npm *', action:'allow', source:'user'}", () => {
|
|
80
|
+
const built = allowFamilyTemplate.build({ cmd: "npm" });
|
|
81
|
+
expect(built.tool).toBe("bash");
|
|
82
|
+
expect(built.pattern).toBe("npm *");
|
|
83
|
+
expect(built.action).toBe("allow");
|
|
84
|
+
expect(built.source).toBe("user");
|
|
85
|
+
expect(built).not.toHaveProperty("id"); // G8:无 id
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("deny-family any:{pattern:'git *', action:'deny'}", () => {
|
|
89
|
+
const built = denyFamilyTemplate.build({ cmd: "git", subcmd: "__any__" });
|
|
90
|
+
expect(built.pattern).toBe("git *");
|
|
91
|
+
expect(built.action).toBe("deny");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("deny-family specific:{pattern:'git push *', action:'deny'}", () => {
|
|
95
|
+
const built = denyFamilyTemplate.build({ cmd: "git", subcmd: "push" });
|
|
96
|
+
expect(built.pattern).toBe("git push *");
|
|
97
|
+
expect(built.action).toBe("deny");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("ask-before:{pattern:'docker *', action:'ask'}", () => {
|
|
101
|
+
const built = askBeforeTemplate.build({ cmd: "docker" });
|
|
102
|
+
expect(built.pattern).toBe("docker *");
|
|
103
|
+
expect(built.action).toBe("ask");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("allow-subcmd:{pattern:'git status *', action:'allow'}", () => {
|
|
107
|
+
const built = allowSubcmdTemplate.build({ cmd: "git", subcmd: "status" });
|
|
108
|
+
expect(built.pattern).toBe("git status *");
|
|
109
|
+
expect(built.action).toBe("allow");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("allow-subcmd __any__:退化为 allow-family 语义(pattern:'git *')", () => {
|
|
113
|
+
const built = allowSubcmdTemplate.build({ cmd: "git", subcmd: "__any__" });
|
|
114
|
+
expect(built.pattern).toBe("git *");
|
|
115
|
+
expect(built.action).toBe("allow");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("allow-subcmd undefined subcmd:也退化为 allow-family", () => {
|
|
119
|
+
const built = allowSubcmdTemplate.build({ cmd: "npm" });
|
|
120
|
+
expect(built.pattern).toBe("npm *");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("custom:直接用 selections 字段组装", () => {
|
|
124
|
+
const built = customTemplate.build({
|
|
125
|
+
pattern: "rm -rf *",
|
|
126
|
+
action: "deny",
|
|
127
|
+
tool: "bash",
|
|
128
|
+
description: "never rm -rf",
|
|
129
|
+
});
|
|
130
|
+
expect(built.pattern).toBe("rm -rf *");
|
|
131
|
+
expect(built.action).toBe("deny");
|
|
132
|
+
expect(built.description).toBe("never rm -rf");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("build 无 id 字段(G8)", () => {
|
|
136
|
+
const built = allowFamilyTemplate.build({ cmd: "npm" });
|
|
137
|
+
expect(built).not.toHaveProperty("id");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("pattern 用 wildcard 不用正则(无 \\b 等)", () => {
|
|
141
|
+
const built = denyFamilyTemplate.build({ cmd: "git", subcmd: "push" });
|
|
142
|
+
expect(built.pattern).not.toContain("\\b");
|
|
143
|
+
expect(built.pattern).not.toContain("\\s");
|
|
144
|
+
expect(built.pattern).toBe("git push *");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ──────────────────────── classifyRuleTemplate ────────────────────────
|
|
149
|
+
|
|
150
|
+
describe("RT3: classifyRuleTemplate", () => {
|
|
151
|
+
it("npm * + allow → allow-family", () => {
|
|
152
|
+
expect(classifyRuleTemplate(makeRule({ pattern: "npm *", action: "allow" }))).toBe("allow-family");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("git * + deny → deny-family(any)", () => {
|
|
156
|
+
expect(classifyRuleTemplate(makeRule({ pattern: "git *", action: "deny" }))).toBe("deny-family");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("git push * + deny → deny-family(specific)", () => {
|
|
160
|
+
expect(classifyRuleTemplate(makeRule({ pattern: "git push *", action: "deny" }))).toBe("deny-family");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("docker * + ask → ask-before", () => {
|
|
164
|
+
expect(classifyRuleTemplate(makeRule({ pattern: "docker *", action: "ask" }))).toBe("ask-before");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("git status * + allow → allow-subcmd", () => {
|
|
168
|
+
expect(classifyRuleTemplate(makeRule({ pattern: "git status *", action: "allow" }))).toBe("allow-subcmd");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("不匹配已知格式 → custom", () => {
|
|
172
|
+
expect(classifyRuleTemplate(makeRule({ pattern: "rm -rf /tmp/*", action: "deny" }))).toBe("custom");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("* alone + allow → custom(不匹配 FAMILY_RE,无 cmd 前缀)", () => {
|
|
176
|
+
expect(classifyRuleTemplate(makeRule({ pattern: "*", action: "allow" }))).toBe("custom");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ──────────────────────── applyOps ────────────────────────
|
|
181
|
+
|
|
182
|
+
describe("RT4: applyOps", () => {
|
|
183
|
+
it("空 ops → 原数组不变", () => {
|
|
184
|
+
const rules = [makeRule()];
|
|
185
|
+
const result = applyOps(rules, []);
|
|
186
|
+
expect(result).toEqual(rules);
|
|
187
|
+
expect(result).not.toBe(rules); // 新数组
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("add:push 新规则", () => {
|
|
191
|
+
const rules = [makeRule({ id: "user-1" })];
|
|
192
|
+
const newRule = makeRule({ id: "user-2", pattern: "git *" });
|
|
193
|
+
const result = applyOps(rules, [{ kind: "add", rule: newRule }]);
|
|
194
|
+
expect(result).toHaveLength(2);
|
|
195
|
+
expect(result[1]!.id).toBe("user-2");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("edit:替换指定 id 的规则", () => {
|
|
199
|
+
const rules = [makeRule({ id: "user-1", pattern: "npm *" })];
|
|
200
|
+
const edited = makeRule({ id: "user-1", pattern: "yarn *" });
|
|
201
|
+
const result = applyOps(rules, [{ kind: "edit", id: "user-1", rule: edited }]);
|
|
202
|
+
expect(result).toHaveLength(1);
|
|
203
|
+
expect(result[0]!.pattern).toBe("yarn *");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("delete:过滤指定 id", () => {
|
|
207
|
+
const rules = [makeRule({ id: "user-1" }), makeRule({ id: "user-2" })];
|
|
208
|
+
const result = applyOps(rules, [{ kind: "delete", id: "user-1" }]);
|
|
209
|
+
expect(result).toHaveLength(1);
|
|
210
|
+
expect(result[0]!.id).toBe("user-2");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("多 ops 顺序执行", () => {
|
|
214
|
+
const rules = [makeRule({ id: "user-1" })];
|
|
215
|
+
const ops = [
|
|
216
|
+
{ kind: "add" as const, rule: makeRule({ id: "user-2", pattern: "git *" }) },
|
|
217
|
+
{ kind: "edit" as const, id: "user-1", rule: makeRule({ id: "user-1", pattern: "yarn *" }) },
|
|
218
|
+
{ kind: "delete" as const, id: "user-2" },
|
|
219
|
+
];
|
|
220
|
+
const result = applyOps(rules, ops);
|
|
221
|
+
expect(result).toHaveLength(1);
|
|
222
|
+
expect(result[0]!.id).toBe("user-1");
|
|
223
|
+
expect(result[0]!.pattern).toBe("yarn *");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("edit 不存在的 id → 不变", () => {
|
|
227
|
+
const rules = [makeRule({ id: "user-1" })];
|
|
228
|
+
const result = applyOps(rules, [{ kind: "edit", id: "user-999", rule: makeRule({ id: "user-999" }) }]);
|
|
229
|
+
expect(result).toHaveLength(1);
|
|
230
|
+
expect(result[0]!.id).toBe("user-1");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ──────────────────────── makeNextIdCounter ────────────────────────
|
|
235
|
+
|
|
236
|
+
describe("RT5: makeNextIdCounter", () => {
|
|
237
|
+
it("空规则 → 从 user-1 开始", () => {
|
|
238
|
+
const counter = makeNextIdCounter([]);
|
|
239
|
+
expect(counter()).toBe("user-1");
|
|
240
|
+
expect(counter()).toBe("user-2");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("已有 user-3 → 从 user-4 开始", () => {
|
|
244
|
+
const existing = [makeRule({ id: "user-1" }), makeRule({ id: "user-3" })];
|
|
245
|
+
const counter = makeNextIdCounter(existing);
|
|
246
|
+
expect(counter()).toBe("user-4");
|
|
247
|
+
expect(counter()).toBe("user-5");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("忽略非 user-<n> 格式的 id", () => {
|
|
251
|
+
const existing = [makeRule({ id: "builtin-safe-1" }), makeRule({ id: "user-5" })];
|
|
252
|
+
const counter = makeNextIdCounter(existing);
|
|
253
|
+
expect(counter()).toBe("user-6");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("单调递增(多次调用)", () => {
|
|
257
|
+
const counter = makeNextIdCounter([]);
|
|
258
|
+
const ids = Array.from({ length: 10 }, () => counter());
|
|
259
|
+
expect(ids).toEqual([
|
|
260
|
+
"user-1", "user-2", "user-3", "user-4", "user-5",
|
|
261
|
+
"user-6", "user-7", "user-8", "user-9", "user-10",
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
});
|