pi-shepherd 0.1.1 → 0.1.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.
Files changed (48) hide show
  1. package/README.en.md +2 -0
  2. package/README.md +2 -0
  3. package/index.ts +1 -1
  4. package/package.json +1 -4
  5. package/rules.json +22 -206
  6. package/shepherd/index.ts +1 -0
  7. package/shepherd/rules-editor.ts +80 -0
  8. package/shepherd/rules-tool-helpers.ts +120 -0
  9. package/shepherd/rules-tool-list.ts +126 -0
  10. package/shepherd/rules-tool.ts +74 -31
  11. package/shepherd/rules.ts +15 -3
  12. package/shepherd/tool-hooks.ts +3 -2
  13. package/node_modules/@pi-atelier/shared-utils/README.en.md +0 -182
  14. package/node_modules/@pi-atelier/shared-utils/README.md +0 -182
  15. package/node_modules/@pi-atelier/shared-utils/package.json +0 -51
  16. package/node_modules/@pi-atelier/shared-utils/src/__tests__/agents.test.ts +0 -120
  17. package/node_modules/@pi-atelier/shared-utils/src/__tests__/ephemeral.test.ts +0 -100
  18. package/node_modules/@pi-atelier/shared-utils/src/__tests__/file-lock.test.ts +0 -152
  19. package/node_modules/@pi-atelier/shared-utils/src/__tests__/filter-match.test.ts +0 -187
  20. package/node_modules/@pi-atelier/shared-utils/src/__tests__/memory-parser.test.ts +0 -170
  21. package/node_modules/@pi-atelier/shared-utils/src/__tests__/paths.test.ts +0 -126
  22. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config-edge.test.ts +0 -138
  23. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config.test.ts +0 -257
  24. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools-mcp.test.ts +0 -189
  25. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools.test.ts +0 -204
  26. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-advanced.test.ts +0 -269
  27. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-array.test.ts +0 -267
  28. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup.test.ts +0 -520
  29. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-read.test.ts +0 -116
  30. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-write.test.ts +0 -119
  31. package/node_modules/@pi-atelier/shared-utils/src/__tests__/tool-output.test.ts +0 -145
  32. package/node_modules/@pi-atelier/shared-utils/src/agents.ts +0 -39
  33. package/node_modules/@pi-atelier/shared-utils/src/ephemeral.ts +0 -42
  34. package/node_modules/@pi-atelier/shared-utils/src/file-lock.ts +0 -62
  35. package/node_modules/@pi-atelier/shared-utils/src/filter-match.ts +0 -100
  36. package/node_modules/@pi-atelier/shared-utils/src/index.ts +0 -71
  37. package/node_modules/@pi-atelier/shared-utils/src/memory-parser.ts +0 -96
  38. package/node_modules/@pi-atelier/shared-utils/src/paths.ts +0 -23
  39. package/node_modules/@pi-atelier/shared-utils/src/project-config.ts +0 -241
  40. package/node_modules/@pi-atelier/shared-utils/src/project-tools.ts +0 -191
  41. package/node_modules/@pi-atelier/shared-utils/src/settings-array.ts +0 -73
  42. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-rollback.ts +0 -104
  43. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-utils.ts +0 -75
  44. package/node_modules/@pi-atelier/shared-utils/src/settings-backup.ts +0 -172
  45. package/node_modules/@pi-atelier/shared-utils/src/settings.ts +0 -104
  46. package/node_modules/@pi-atelier/shared-utils/src/tool-output.ts +0 -149
  47. package/node_modules/@pi-atelier/shared-utils/tsconfig.json +0 -9
  48. package/node_modules/@pi-atelier/shared-utils/vitest.config.ts +0 -24
@@ -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
- });
@@ -1,204 +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 {
8
- getDisabledMcpServers,
9
- getDisabledTools,
10
- getEffectiveToolFilter,
11
- getEnabledTools,
12
- isToolDisabled,
13
- } from "../project-tools";
14
-
15
- const TEST_DIR = join(tmpdir(), "pi-shared-utils-tools-test");
16
-
17
- // ── 测试辅助 ────────────────────────────────────────────
18
-
19
- function createProjectSettings(dir: string, content: Record<string, any>) {
20
- const piDir = join(dir, ".pi");
21
- mkdirSync(piDir, { recursive: true });
22
- writeFileSync(join(piDir, "settings.json"), JSON.stringify(content, null, 2));
23
- }
24
-
25
- function cleanupDir(dir: string) {
26
- if (existsSync(dir)) rmSync(dir, { recursive: true });
27
- }
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("getDisabledMcpServers — 三层合并", () => {
39
- beforeEach(() => {
40
- cleanupDir(TEST_DIR);
41
- clearProjectSettingsCache();
42
- mkdirSync(TEST_DIR, { recursive: true });
43
- });
44
- afterEach(() => {
45
- cleanupDir(TEST_DIR);
46
- });
47
-
48
- it("无项目配置时,只返回全局禁用列表", () => {
49
- const restore = mockGlobalSettings({ mcp: { disabled: ["global-server"] } });
50
- try {
51
- const result = getDisabledMcpServers(TEST_DIR);
52
- expect(result).toEqual(["global-server"]);
53
- } finally {
54
- restore();
55
- }
56
- });
57
-
58
- it("无全局无项目配置时,返回空数组", () => {
59
- const restore = mockGlobalSettings({});
60
- try {
61
- const result = getDisabledMcpServers(TEST_DIR);
62
- expect(result).toEqual([]);
63
- } finally {
64
- restore();
65
- }
66
- });
67
-
68
- it("全局 + 项目 concat 合并", () => {
69
- const restore = mockGlobalSettings({ mcp: { disabled: ["unstable"] } });
70
- try {
71
- createProjectSettings(TEST_DIR, { mcp: { disabled: ["godot"] } });
72
- const result = getDisabledMcpServers(TEST_DIR);
73
- expect(result).toEqual(["unstable", "godot"]);
74
- } finally {
75
- restore();
76
- }
77
- });
78
-
79
- it("全局和项目有重复时去重", () => {
80
- const restore = mockGlobalSettings({ mcp: { disabled: ["shared-server"] } });
81
- try {
82
- createProjectSettings(TEST_DIR, { mcp: { disabled: ["shared-server", "extra"] } });
83
- const result = getDisabledMcpServers(TEST_DIR);
84
- expect(result).toEqual(["shared-server", "extra"]);
85
- } finally {
86
- restore();
87
- }
88
- });
89
-
90
- it("只有项目级配置时", () => {
91
- const restore = mockGlobalSettings({});
92
- try {
93
- createProjectSettings(TEST_DIR, { mcp: { disabled: ["godot", "glm"] } });
94
- const result = getDisabledMcpServers(TEST_DIR);
95
- expect(result).toEqual(["godot", "glm"]);
96
- } finally {
97
- restore();
98
- }
99
- });
100
-
101
- it("全局禁用 + 项目 enabled 白名单覆盖", () => {
102
- const restore = mockGlobalSettings({ mcp: { disabled: ["godot", "code-graph", "glm-web-search"] } });
103
- try {
104
- // 项目启用 godot,其他保持禁用
105
- createProjectSettings(TEST_DIR, { mcp: { enabled: ["godot"] } });
106
- const result = getDisabledMcpServers(TEST_DIR);
107
- expect(result).toEqual(["code-graph", "glm-web-search"]);
108
- expect(result).not.toContain("godot");
109
- } finally {
110
- restore();
111
- }
112
- });
113
-
114
- it("全局禁用 + 项目 enabled + 项目 disabled 同时生效", () => {
115
- const restore = mockGlobalSettings({ mcp: { disabled: ["godot", "code-graph"] } });
116
- try {
117
- createProjectSettings(TEST_DIR, {
118
- mcp: {
119
- enabled: ["godot"], // 启用 godot(从 disabled 中移除)
120
- disabled: ["glm-web-search"], // 额外禁用
121
- },
122
- });
123
- const result = getDisabledMcpServers(TEST_DIR);
124
- expect(result).toEqual(["code-graph", "glm-web-search"]);
125
- expect(result).not.toContain("godot");
126
- } finally {
127
- restore();
128
- }
129
- });
130
- });
131
-
132
- describe("getEffectiveToolFilter — 三层合并", () => {
133
- beforeEach(() => {
134
- cleanupDir(TEST_DIR);
135
- clearProjectSettingsCache();
136
- mkdirSync(TEST_DIR, { recursive: true });
137
- });
138
- afterEach(() => {
139
- cleanupDir(TEST_DIR);
140
- });
141
-
142
- it("无配置时返回空 filter", () => {
143
- const filter = getEffectiveToolFilter(TEST_DIR);
144
- expect(filter.disabled).toBeUndefined();
145
- expect(filter.enabled).toBeUndefined();
146
- });
147
-
148
- it("全局 + 项目 concat", () => {
149
- const restore = mockGlobalSettings({ tools: { disabled: ["global-tool"] } });
150
- try {
151
- createProjectSettings(TEST_DIR, { tools: { disabled: ["project-tool"] } });
152
- const filter = getEffectiveToolFilter(TEST_DIR);
153
- expect(filter.disabled).toEqual(["global-tool", "project-tool"]);
154
- } finally {
155
- restore();
156
- }
157
- });
158
- });
159
-
160
- describe("getEnabledTools — 三层合并", () => {
161
- beforeEach(() => {
162
- cleanupDir(TEST_DIR);
163
- clearProjectSettingsCache();
164
- mkdirSync(TEST_DIR, { recursive: true });
165
- });
166
- afterEach(() => {
167
- cleanupDir(TEST_DIR);
168
- });
169
-
170
- const ALL_TOOLS = ["read", "write", "edit", "bash", "grep", "find"];
171
-
172
- it("无过滤时返回全部工具", () => {
173
- const result = getEnabledTools(ALL_TOOLS, TEST_DIR);
174
- expect(result).toEqual(ALL_TOOLS);
175
- });
176
-
177
- it("全局禁用 + 项目禁用 concat", () => {
178
- const restore = mockGlobalSettings({ tools: { disabled: ["bash"] } });
179
- try {
180
- createProjectSettings(TEST_DIR, { tools: { disabled: ["grep"] } });
181
- const result = getEnabledTools(ALL_TOOLS, TEST_DIR);
182
- expect(result).toEqual(["read", "write", "edit", "find"]);
183
- } finally {
184
- restore();
185
- }
186
- });
187
-
188
- it("全局和项目都禁用同一个工具时去重", () => {
189
- const restore = mockGlobalSettings({ tools: { disabled: ["bash"] } });
190
- try {
191
- createProjectSettings(TEST_DIR, { tools: { disabled: ["bash", "edit"] } });
192
- const result = getEnabledTools(ALL_TOOLS, TEST_DIR);
193
- expect(result).toEqual(["read", "write", "grep", "find"]);
194
- } finally {
195
- restore();
196
- }
197
- });
198
-
199
- it("enabled 补充不在 allTools 里的工具", () => {
200
- createProjectSettings(TEST_DIR, { tools: { enabled: ["custom-tool"] } });
201
- const result = getEnabledTools(ALL_TOOLS, TEST_DIR);
202
- expect(result).toContain("custom-tool");
203
- });
204
- });