pi-shepherd 0.1.1 → 0.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.
Files changed (58) hide show
  1. package/README.en.md +206 -83
  2. package/README.md +209 -88
  3. package/index.ts +262 -229
  4. package/package.json +60 -58
  5. package/rules.json +197 -218
  6. package/shepherd/compaction.ts +76 -0
  7. package/shepherd/conditions.ts +98 -0
  8. package/shepherd/ephemeral.ts +55 -52
  9. package/shepherd/git.ts +64 -0
  10. package/shepherd/index.ts +14 -0
  11. package/shepherd/line-count.ts +86 -86
  12. package/shepherd/message-end.ts +120 -0
  13. package/shepherd/rules-editor.ts +250 -135
  14. package/shepherd/rules-tool-helpers.ts +119 -0
  15. package/shepherd/rules-tool-list.ts +124 -0
  16. package/shepherd/rules-tool.ts +182 -99
  17. package/shepherd/rules-validate.ts +89 -44
  18. package/shepherd/rules.ts +366 -283
  19. package/shepherd/tool-event-types.ts +27 -14
  20. package/shepherd/tool-hooks.ts +165 -176
  21. package/shepherd/worktree-check.ts +130 -130
  22. package/tsconfig.json +21 -14
  23. package/node_modules/@pi-atelier/shared-utils/README.en.md +0 -182
  24. package/node_modules/@pi-atelier/shared-utils/README.md +0 -182
  25. package/node_modules/@pi-atelier/shared-utils/package.json +0 -51
  26. package/node_modules/@pi-atelier/shared-utils/src/__tests__/agents.test.ts +0 -120
  27. package/node_modules/@pi-atelier/shared-utils/src/__tests__/ephemeral.test.ts +0 -100
  28. package/node_modules/@pi-atelier/shared-utils/src/__tests__/file-lock.test.ts +0 -152
  29. package/node_modules/@pi-atelier/shared-utils/src/__tests__/filter-match.test.ts +0 -187
  30. package/node_modules/@pi-atelier/shared-utils/src/__tests__/memory-parser.test.ts +0 -170
  31. package/node_modules/@pi-atelier/shared-utils/src/__tests__/paths.test.ts +0 -126
  32. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config-edge.test.ts +0 -138
  33. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config.test.ts +0 -257
  34. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools-mcp.test.ts +0 -189
  35. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools.test.ts +0 -204
  36. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-advanced.test.ts +0 -269
  37. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-array.test.ts +0 -267
  38. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup.test.ts +0 -520
  39. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-read.test.ts +0 -116
  40. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-write.test.ts +0 -119
  41. package/node_modules/@pi-atelier/shared-utils/src/__tests__/tool-output.test.ts +0 -145
  42. package/node_modules/@pi-atelier/shared-utils/src/agents.ts +0 -39
  43. package/node_modules/@pi-atelier/shared-utils/src/ephemeral.ts +0 -42
  44. package/node_modules/@pi-atelier/shared-utils/src/file-lock.ts +0 -62
  45. package/node_modules/@pi-atelier/shared-utils/src/filter-match.ts +0 -100
  46. package/node_modules/@pi-atelier/shared-utils/src/index.ts +0 -71
  47. package/node_modules/@pi-atelier/shared-utils/src/memory-parser.ts +0 -96
  48. package/node_modules/@pi-atelier/shared-utils/src/paths.ts +0 -23
  49. package/node_modules/@pi-atelier/shared-utils/src/project-config.ts +0 -241
  50. package/node_modules/@pi-atelier/shared-utils/src/project-tools.ts +0 -191
  51. package/node_modules/@pi-atelier/shared-utils/src/settings-array.ts +0 -73
  52. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-rollback.ts +0 -104
  53. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-utils.ts +0 -75
  54. package/node_modules/@pi-atelier/shared-utils/src/settings-backup.ts +0 -172
  55. package/node_modules/@pi-atelier/shared-utils/src/settings.ts +0 -104
  56. package/node_modules/@pi-atelier/shared-utils/src/tool-output.ts +0 -149
  57. package/node_modules/@pi-atelier/shared-utils/tsconfig.json +0 -9
  58. package/node_modules/@pi-atelier/shared-utils/vitest.config.ts +0 -24
@@ -1,138 +0,0 @@
1
- /**
2
- * 项目级配置边界情况测试
3
- * - 文件损坏/空配置/未知字段
4
- * - 不同项目目录隔离性
5
- * - 值相同时不报冲突
6
- * - 类型不匹配的各种情况
7
- */
8
-
9
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
- import { tmpdir } from "node:os";
11
- import { join } from "node:path";
12
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
13
- import {
14
- clearProjectSettingsCache,
15
- detectConfigConflicts,
16
- getEffectiveConfig,
17
- validateConfigSchema,
18
- } from "../project-config";
19
-
20
- const TEST_DIR = join(tmpdir(), "pi-shared-utils-edge-test");
21
-
22
- function createProjectSettings(dir: string, content: Record<string, any>) {
23
- const piDir = join(dir, ".pi");
24
- mkdirSync(piDir, { recursive: true });
25
- writeFileSync(join(piDir, "settings.json"), JSON.stringify(content, null, 2));
26
- }
27
-
28
- function cleanupDir(dir: string) {
29
- if (existsSync(dir)) rmSync(dir, { recursive: true });
30
- }
31
-
32
- const SETTINGS_PATH = join(require("node:os").homedir(), ".pi/agent/settings.json");
33
-
34
- function mockGlobalSettings(content: Record<string, any>) {
35
- const original = readFileSync(SETTINGS_PATH, "utf-8");
36
- writeFileSync(SETTINGS_PATH, JSON.stringify(content, null, 2));
37
- return () => writeFileSync(SETTINGS_PATH, original);
38
- }
39
-
40
- let restoreGlobal: (() => void) | undefined;
41
-
42
- describe("边界情况与隔离性", () => {
43
- beforeEach(() => {
44
- cleanupDir(TEST_DIR);
45
- clearProjectSettingsCache();
46
- mkdirSync(TEST_DIR, { recursive: true });
47
- restoreGlobal = mockGlobalSettings({});
48
- });
49
- afterEach(() => {
50
- restoreGlobal?.();
51
- cleanupDir(TEST_DIR);
52
- });
53
-
54
- it("项目配置为空对象时不影响默认值", () => {
55
- createProjectSettings(TEST_DIR, {
56
- context: {},
57
- });
58
-
59
- const defaults = { distillThreshold: 5000, agingThreshold: 10 };
60
- const result = getEffectiveConfig("context", defaults, TEST_DIR);
61
- expect(typeof result.config.distillThreshold).toBe("number");
62
- expect(typeof result.config.agingThreshold).toBe("number");
63
- });
64
-
65
- it("项目配置文件损坏(无效 JSON)时回退到默认值", () => {
66
- const piDir = join(TEST_DIR, ".pi");
67
- mkdirSync(piDir, { recursive: true });
68
- writeFileSync(join(piDir, "settings.json"), "{ invalid json ");
69
-
70
- const defaults = { distillThreshold: 5000 };
71
- const result = getEffectiveConfig("context", defaults, TEST_DIR);
72
- expect(result.config.distillThreshold).toBe(5000);
73
- });
74
-
75
- it("项目配置含未知字段时保留(不丢弃)", () => {
76
- createProjectSettings(TEST_DIR, {
77
- context: { distillThreshold: 8000, unknownField: "hello" },
78
- });
79
-
80
- const defaults = { distillThreshold: 5000, agingThreshold: 10 };
81
- const result = getEffectiveConfig("context", defaults, TEST_DIR);
82
- expect(result.config.distillThreshold).toBe(8000);
83
- expect(typeof result.config.agingThreshold).toBe("number");
84
- expect((result.config as any).unknownField).toBe("hello");
85
- });
86
-
87
- it("不同项目目录的配置互不影响", () => {
88
- const dirA = join(TEST_DIR, "project-a");
89
- const dirB = join(TEST_DIR, "project-b");
90
- mkdirSync(dirA, { recursive: true });
91
- mkdirSync(dirB, { recursive: true });
92
-
93
- createProjectSettings(dirA, { context: { distillThreshold: 1000 } });
94
- createProjectSettings(dirB, { context: { distillThreshold: 2000 } });
95
-
96
- const defaults = { distillThreshold: 5000 };
97
- const resultA = getEffectiveConfig("context", defaults, dirA);
98
- const resultB = getEffectiveConfig("context", defaults, dirB);
99
-
100
- expect(resultA.config.distillThreshold).toBe(1000);
101
- expect(resultB.config.distillThreshold).toBe(2000);
102
- });
103
-
104
- it("项目值与全局值相同时不产生冲突", () => {
105
- const defaults = { distillThreshold: 5000 };
106
- const globalResult = getEffectiveConfig("context", defaults, TEST_DIR);
107
-
108
- createProjectSettings(TEST_DIR, {
109
- context: { distillThreshold: globalResult.config.distillThreshold },
110
- });
111
-
112
- const conflicts = detectConfigConflicts(TEST_DIR);
113
- const contextConflicts = conflicts.filter((c) => c.section === "context" && c.key === "distillThreshold");
114
- expect(contextConflicts).toHaveLength(0);
115
- });
116
-
117
- it("validateConfigSchema 忽略 defaults 中不存在的字段", () => {
118
- createProjectSettings(TEST_DIR, {
119
- context: { distillThreshold: 8000, extraField: "surprise" },
120
- });
121
-
122
- const errors = validateConfigSchema("context", { distillThreshold: 5000 }, TEST_DIR);
123
- expect(errors).toEqual([]);
124
- });
125
-
126
- it("数组字段类型不匹配时报错", () => {
127
- createProjectSettings(TEST_DIR, {
128
- context: { tags: "not-an-array" },
129
- });
130
-
131
- const defaults = { tags: ["default"] };
132
- const errors = validateConfigSchema("context", defaults, TEST_DIR);
133
- expect(errors.length).toBeGreaterThan(0);
134
- expect(errors[0].key).toBe("tags");
135
- expect(errors[0].expectedType).toBe("array");
136
- expect(errors[0].actualType).toBe("string");
137
- });
138
- });
@@ -1,257 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import {
6
- clearProjectSettingsCache,
7
- detectConfigConflicts,
8
- getEffectiveConfig,
9
- validateConfigSchema,
10
- } from "../project-config";
11
- import { getDisabledMcpServers, getEnabledTools } from "../project-tools";
12
-
13
- const TEST_DIR = join(tmpdir(), "pi-shared-utils-test");
14
-
15
- // ── 测试辅助 ────────────────────────────────────────────
16
-
17
- function createProjectSettings(dir: string, content: Record<string, any>) {
18
- const piDir = join(dir, ".pi");
19
- mkdirSync(piDir, { recursive: true });
20
- writeFileSync(join(piDir, "settings.json"), JSON.stringify(content, null, 2));
21
- }
22
-
23
- function cleanupDir(dir: string) {
24
- if (existsSync(dir)) rmSync(dir, { recursive: true });
25
- }
26
-
27
- const SETTINGS_PATH = join(require("node:os").homedir(), ".pi/agent/settings.json");
28
-
29
- /** 临时替换全局 settings.json 内容(用于测试三层合并) */
30
- function mockGlobalSettings(content: Record<string, any>) {
31
- const original = readFileSync(SETTINGS_PATH, "utf-8");
32
- writeFileSync(SETTINGS_PATH, JSON.stringify(content, null, 2));
33
- return () => writeFileSync(SETTINGS_PATH, original);
34
- }
35
-
36
- // ── 测试 ─────────────────────────────────────────────────
37
-
38
- describe("project-config", () => {
39
- beforeEach(() => {
40
- cleanupDir(TEST_DIR);
41
- clearProjectSettingsCache();
42
- mkdirSync(TEST_DIR, { recursive: true });
43
- });
44
- afterEach(() => {
45
- cleanupDir(TEST_DIR);
46
- });
47
-
48
- describe("getEffectiveConfig", () => {
49
- it("无项目配置时返回默认值", () => {
50
- const defaults = { threshold: 5000, enabled: true };
51
- const result = getEffectiveConfig("nonexistent-section", defaults, TEST_DIR);
52
- expect(result.config.threshold).toBe(5000);
53
- expect(result.config.enabled).toBe(true);
54
- });
55
-
56
- it("项目配置覆盖默认值", () => {
57
- createProjectSettings(TEST_DIR, {
58
- context: { distillThreshold: 8000 },
59
- });
60
-
61
- const defaults = { distillThreshold: 5000, agingThreshold: 20 };
62
- const result = getEffectiveConfig("context", defaults, TEST_DIR);
63
- expect(result.config.distillThreshold).toBe(8000);
64
- expect(result.config.agingThreshold).toBe(20);
65
- expect(result.sources.distillThreshold).toBe("project");
66
- });
67
-
68
- it("嵌套对象 deep merge", () => {
69
- createProjectSettings(TEST_DIR, {
70
- retry: { provider: { timeoutMs: 120000 } },
71
- });
72
-
73
- const defaults = {
74
- enabled: true,
75
- maxRetries: 3,
76
- provider: { timeoutMs: 600000, maxRetries: 2 },
77
- };
78
- const result = getEffectiveConfig("retry", defaults, TEST_DIR);
79
- expect(result.config.provider.timeoutMs).toBe(120000);
80
- expect(result.config.provider.maxRetries).toBe(2);
81
- expect(result.config.enabled).toBe(true);
82
- });
83
- });
84
-
85
- describe("detectConfigConflicts", () => {
86
- it("无项目配置时返回空", () => {
87
- const conflicts = detectConfigConflicts(TEST_DIR);
88
- expect(conflicts).toEqual([]);
89
- });
90
-
91
- it("检测到全局与项目值不同的冲突", () => {
92
- createProjectSettings(TEST_DIR, {
93
- context: { distillThreshold: 500 },
94
- });
95
-
96
- const conflicts = detectConfigConflicts(TEST_DIR);
97
- // 只检查格式正确,不硬编码全局值
98
- if (conflicts.length > 0) {
99
- expect(conflicts[0].section).toBe("context");
100
- expect(conflicts[0].key).toBe("distillThreshold");
101
- expect(conflicts[0].projectValue).toBe(500);
102
- }
103
- });
104
-
105
- it("跳过 prompts 和 skills 段", () => {
106
- createProjectSettings(TEST_DIR, {
107
- prompts: ["prompts/"],
108
- skills: ["+skills/test/SKILL.md"],
109
- });
110
-
111
- const conflicts = detectConfigConflicts(TEST_DIR);
112
- expect(conflicts).toEqual([]);
113
- });
114
- });
115
-
116
- describe("getEnabledTools", () => {
117
- it("无项目配置时返回全部工具", () => {
118
- const tools = getEnabledTools(["vision_analyze", "payload_analyze", "code_search"], TEST_DIR);
119
- expect(tools).toEqual(["vision_analyze", "payload_analyze", "code_search"]);
120
- });
121
-
122
- it("禁用指定工具", () => {
123
- createProjectSettings(TEST_DIR, {
124
- tools: { disabled: ["payload_analyze"] },
125
- });
126
-
127
- const tools = getEnabledTools(["vision_analyze", "payload_analyze", "code_search"], TEST_DIR);
128
- expect(tools).toEqual(["vision_analyze", "code_search"]);
129
- });
130
-
131
- it("额外启用工具", () => {
132
- createProjectSettings(TEST_DIR, {
133
- tools: { enabled: ["custom_tool"] },
134
- });
135
-
136
- const tools = getEnabledTools(["vision_analyze"], TEST_DIR);
137
- expect(tools).toContain("vision_analyze");
138
- expect(tools).toContain("custom_tool");
139
- });
140
-
141
- it("同时禁用和启用", () => {
142
- createProjectSettings(TEST_DIR, {
143
- tools: { disabled: ["payload_analyze"], enabled: ["custom_tool"] },
144
- });
145
-
146
- const tools = getEnabledTools(["vision_analyze", "payload_analyze"], TEST_DIR);
147
- expect(tools).toEqual(["vision_analyze", "custom_tool"]);
148
- });
149
- });
150
-
151
- describe("数组合并策略", () => {
152
- it("默认:数组字段被项目级替换", () => {
153
- createProjectSettings(TEST_DIR, {
154
- test: { items: [4, 5, 6] },
155
- });
156
-
157
- const defaults = { items: [1, 2, 3] };
158
- const result = getEffectiveConfig("test", defaults, TEST_DIR);
159
- expect(result.config.items).toEqual([4, 5, 6]);
160
- });
161
-
162
- it("concatMerge 策略:数组字段做追加而非替换", () => {
163
- createProjectSettings(TEST_DIR, {
164
- shepherd: {
165
- rules: [{ comment: "项目规则", hook: "tool_call", tool: "bash", action: "notify", reason: "test" }],
166
- },
167
- });
168
-
169
- const defaults = {
170
- rules: [{ comment: "全局规则", hook: "tool_call", tool: "bash", action: "block", reason: "test" }],
171
- };
172
- const result = getEffectiveConfig("shepherd", defaults, TEST_DIR, {
173
- arrayMerge: "concat",
174
- });
175
- // 全局 + 项目 追加
176
- expect(result.config.rules).toHaveLength(2);
177
- expect(result.config.rules[0].comment).toBe("全局规则");
178
- expect(result.config.rules[1].comment).toBe("项目规则");
179
- });
180
-
181
- it("concatMerge 策略:无项目级配置时不改变数组", () => {
182
- const defaults = {
183
- rules: [{ comment: "唯一规则" }],
184
- };
185
- const result = getEffectiveConfig("shepherd", defaults, TEST_DIR, {
186
- arrayMerge: "concat",
187
- });
188
- expect(result.config.rules).toHaveLength(1);
189
- expect(result.config.rules[0].comment).toBe("唯一规则");
190
- });
191
- });
192
-
193
- describe("validateConfigSchema", () => {
194
- it("类型一致时无错误", () => {
195
- createProjectSettings(TEST_DIR, {
196
- context: { distillThreshold: 8000 },
197
- });
198
-
199
- const errors = validateConfigSchema("context", { distillThreshold: 5000 }, TEST_DIR);
200
- expect(errors).toEqual([]);
201
- });
202
-
203
- it("类型不一致时报错", () => {
204
- createProjectSettings(TEST_DIR, {
205
- context: { distillThreshold: "8000" },
206
- });
207
-
208
- const errors = validateConfigSchema("context", { distillThreshold: 5000 }, TEST_DIR);
209
- expect(errors.length).toBeGreaterThan(0);
210
- expect(errors[0].key).toBe("distillThreshold");
211
- expect(errors[0].expectedType).toBe("number");
212
- expect(errors[0].actualType).toBe("string");
213
- });
214
-
215
- it("无项目配置时无错误", () => {
216
- const errors = validateConfigSchema("context", { distillThreshold: 5000 }, TEST_DIR);
217
- expect(errors).toEqual([]);
218
- });
219
-
220
- it("嵌套对象类型不一致", () => {
221
- createProjectSettings(TEST_DIR, {
222
- retry: { provider: "not-an-object" },
223
- });
224
-
225
- const defaults = { provider: { timeoutMs: 60000 } };
226
- const errors = validateConfigSchema("retry", defaults, TEST_DIR);
227
- expect(errors.length).toBeGreaterThan(0);
228
- expect(errors[0].key).toBe("provider");
229
- });
230
- });
231
-
232
- describe("getDisabledMcpServers", () => {
233
- it("无配置时返回空", () => {
234
- const restore = mockGlobalSettings({});
235
- try {
236
- const disabled = getDisabledMcpServers(TEST_DIR);
237
- expect(disabled).toEqual([]);
238
- } finally {
239
- restore();
240
- }
241
- });
242
-
243
- it("返回禁用的 MCP 服务器", () => {
244
- const restore = mockGlobalSettings({});
245
- try {
246
- createProjectSettings(TEST_DIR, {
247
- mcp: { disabled: ["code-graph", "glm-web-search"] },
248
- });
249
-
250
- const disabled = getDisabledMcpServers(TEST_DIR);
251
- expect(disabled).toEqual(["code-graph", "glm-web-search"]);
252
- } finally {
253
- restore();
254
- }
255
- });
256
- });
257
- });
@@ -1,189 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { SETTINGS_PATH } from "../paths";
6
- import { clearProjectSettingsCache } from "../project-config";
7
- import { getDisabledTools, isToolDisabled } from "../project-tools";
8
-
9
- const TEST_DIR = join(tmpdir(), "pi-shared-utils-mcp-tools-test");
10
-
11
- // ── 测试辅助 ────────────────────────────────────────────
12
-
13
- function createProjectSettings(dir: string, content: Record<string, any>) {
14
- const piDir = join(dir, ".pi");
15
- mkdirSync(piDir, { recursive: true });
16
- writeFileSync(join(piDir, "settings.json"), JSON.stringify(content, null, 2));
17
- }
18
-
19
- function cleanupDir(dir: string) {
20
- if (existsSync(dir)) rmSync(dir, { recursive: true });
21
- }
22
-
23
- /** 临时替换全局 settings.json 内容 */
24
- function mockGlobalSettings(content: Record<string, any>) {
25
- const original = readFileSync(SETTINGS_PATH, "utf-8");
26
- writeFileSync(SETTINGS_PATH, JSON.stringify(content, null, 2));
27
- return () => writeFileSync(SETTINGS_PATH, original);
28
- }
29
-
30
- // ── getDisabledTools ────────────────────────────────────
31
-
32
- describe("getDisabledTools — 工具级禁用", () => {
33
- beforeEach(() => {
34
- cleanupDir(TEST_DIR);
35
- clearProjectSettingsCache();
36
- mkdirSync(TEST_DIR, { recursive: true });
37
- });
38
- afterEach(() => {
39
- cleanupDir(TEST_DIR);
40
- });
41
-
42
- it("无配置时返回空数组", () => {
43
- const restore = mockGlobalSettings({});
44
- try {
45
- const result = getDisabledTools(TEST_DIR);
46
- expect(result).toEqual([]);
47
- } finally {
48
- restore();
49
- }
50
- });
51
-
52
- it("全局工具级禁用", () => {
53
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["godot_export_build"] } } });
54
- try {
55
- const result = getDisabledTools(TEST_DIR);
56
- expect(result).toEqual(["godot_export_build"]);
57
- } finally {
58
- restore();
59
- }
60
- });
61
-
62
- it("全局 + 项目 concat", () => {
63
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["godot_export_build"] } } });
64
- try {
65
- createProjectSettings(TEST_DIR, { mcp: { tools: { disabled: ["godot_launch_editor"] } } });
66
- const result = getDisabledTools(TEST_DIR);
67
- expect(result).toEqual(["godot_export_build", "godot_launch_editor"]);
68
- } finally {
69
- restore();
70
- }
71
- });
72
-
73
- it("项目 enabled 白名单覆盖", () => {
74
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["godot_export_build", "godot_launch_editor"] } } });
75
- try {
76
- createProjectSettings(TEST_DIR, { mcp: { tools: { enabled: ["godot_launch_editor"] } } });
77
- const result = getDisabledTools(TEST_DIR);
78
- expect(result).toEqual(["godot_export_build"]);
79
- } finally {
80
- restore();
81
- }
82
- });
83
-
84
- it("去重", () => {
85
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["godot_export_build"] } } });
86
- try {
87
- createProjectSettings(TEST_DIR, { mcp: { tools: { disabled: ["godot_export_build"] } } });
88
- const result = getDisabledTools(TEST_DIR);
89
- expect(result).toEqual(["godot_export_build"]);
90
- } finally {
91
- restore();
92
- }
93
- });
94
-
95
- it("与 server 级 disabled 独立", () => {
96
- const restore = mockGlobalSettings({ mcp: { disabled: ["godot"], tools: { disabled: ["glm_web_search"] } } });
97
- try {
98
- const result = getDisabledTools(TEST_DIR);
99
- expect(result).toEqual(["glm_web_search"]);
100
- } finally {
101
- restore();
102
- }
103
- });
104
- });
105
-
106
- // ── isToolDisabled — glob 匹配 ──────────────────────────
107
-
108
- describe("isToolDisabled — glob 匹配", () => {
109
- beforeEach(() => {
110
- cleanupDir(TEST_DIR);
111
- clearProjectSettingsCache();
112
- mkdirSync(TEST_DIR, { recursive: true });
113
- });
114
- afterEach(() => {
115
- cleanupDir(TEST_DIR);
116
- });
117
-
118
- it("精确匹配", () => {
119
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["godot_launch_editor"] } } });
120
- try {
121
- expect(isToolDisabled("godot_launch_editor", TEST_DIR)).toBe(true);
122
- expect(isToolDisabled("godot_run_project", TEST_DIR)).toBe(false);
123
- } finally {
124
- restore();
125
- }
126
- });
127
-
128
- it("glob * 匹配", () => {
129
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["godot_export_*"] } } });
130
- try {
131
- expect(isToolDisabled("godot_export_build", TEST_DIR)).toBe(true);
132
- expect(isToolDisabled("godot_export_list_presets", TEST_DIR)).toBe(true);
133
- expect(isToolDisabled("godot_run_project", TEST_DIR)).toBe(false);
134
- } finally {
135
- restore();
136
- }
137
- });
138
-
139
- it("多个 * 通配符", () => {
140
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["godot_*_editor"] } } });
141
- try {
142
- expect(isToolDisabled("godot_launch_editor", TEST_DIR)).toBe(true);
143
- expect(isToolDisabled("godot_game_bridge_uninstall", TEST_DIR)).toBe(false);
144
- } finally {
145
- restore();
146
- }
147
- });
148
-
149
- it("混合精确和 glob", () => {
150
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["godot_export_*", "godot_launch_editor"] } } });
151
- try {
152
- expect(isToolDisabled("godot_export_build", TEST_DIR)).toBe(true);
153
- expect(isToolDisabled("godot_launch_editor", TEST_DIR)).toBe(true);
154
- expect(isToolDisabled("godot_run_project", TEST_DIR)).toBe(false);
155
- } finally {
156
- restore();
157
- }
158
- });
159
-
160
- it("无禁用时全部通过", () => {
161
- const restore = mockGlobalSettings({});
162
- try {
163
- expect(isToolDisabled("godot_anything", TEST_DIR)).toBe(false);
164
- } finally {
165
- restore();
166
- }
167
- });
168
-
169
- it("项目级 enabled 覆盖全局 disabled", () => {
170
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["godot_export_*"] } } });
171
- try {
172
- createProjectSettings(TEST_DIR, { mcp: { tools: { enabled: ["godot_export_*"] } } });
173
- expect(isToolDisabled("godot_export_build", TEST_DIR)).toBe(false);
174
- } finally {
175
- restore();
176
- }
177
- });
178
-
179
- it("特殊字符在模式中被正确转义", () => {
180
- const restore = mockGlobalSettings({ mcp: { tools: { disabled: ["code_graph_pi-core_*"] } } });
181
- try {
182
- // "-" 不是 glob 特殊字符,应该精确匹配
183
- expect(isToolDisabled("code_graph_pi-core_semantic_code_search", TEST_DIR)).toBe(true);
184
- expect(isToolDisabled("code_graph_pi_core_semantic_code_search", TEST_DIR)).toBe(false);
185
- } finally {
186
- restore();
187
- }
188
- });
189
- });