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,269 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, 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
-
6
- const TEST_DIR = join(tmpdir(), `pi-settings-adv-test-${process.pid}`);
7
- const TEST_SETTINGS = join(TEST_DIR, "settings.json");
8
- const TEST_BACKUP_DIR = join(TEST_DIR, "settings-backup");
9
-
10
- const { patchSettingsSectionWithBackup, rollbackSettings, listBackups } = await import(
11
- "../settings-backup"
12
- );
13
-
14
- function writeTestSettings(content: Record<string, any>) {
15
- mkdirSync(TEST_DIR, { recursive: true });
16
- writeFileSync(TEST_SETTINGS, JSON.stringify(content, null, 2));
17
- }
18
-
19
- function readTestSettings(): Record<string, any> {
20
- if (!existsSync(TEST_SETTINGS)) return {};
21
- return JSON.parse(readFileSync(TEST_SETTINGS, "utf-8"));
22
- }
23
-
24
- function cleanupTestDir() {
25
- if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
26
- }
27
-
28
- describe("settings-backup 高级测试", () => {
29
- beforeEach(() => {
30
- cleanupTestDir();
31
- mkdirSync(TEST_DIR, { recursive: true });
32
- });
33
- afterEach(() => {
34
- cleanupTestDir();
35
- });
36
-
37
- // ── 并发写入与竞态保护 ────────────────────────
38
-
39
- describe("并发写入与竞态保护", () => {
40
- it("快速连续 patch 不同 section 不丢数据", () => {
41
- writeTestSettings({ context: { threshold: 100 }, shepherd: { maxWarnings: 5 } });
42
-
43
- patchSettingsSectionWithBackup(
44
- "context",
45
- { threshold: 200 },
46
- { threshold: 100 },
47
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, backup: false },
48
- );
49
- patchSettingsSectionWithBackup(
50
- "shepherd",
51
- { maxWarnings: 10 },
52
- { maxWarnings: 5 },
53
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, backup: false },
54
- );
55
-
56
- const settings = readTestSettings();
57
- expect(settings.context.threshold).toBe(200);
58
- expect(settings.shepherd.maxWarnings).toBe(10);
59
- });
60
-
61
- it("加锁后重读最新数据 — 保留等锁期间外部修改", () => {
62
- writeTestSettings({ context: { threshold: 100 } });
63
-
64
- // 第一次 patch
65
- patchSettingsSectionWithBackup(
66
- "context",
67
- { threshold: 200 },
68
- { threshold: 100 },
69
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, backup: false },
70
- );
71
-
72
- // 模拟外部进程修改:直接写文件加一个新 section
73
- const externalData = { ...readTestSettings(), externalSection: { addedByOther: true } };
74
- writeFileSync(TEST_SETTINGS, JSON.stringify(externalData, null, 2));
75
-
76
- // 第二次 patch 应该保留外部添加的 section
77
- patchSettingsSectionWithBackup(
78
- "shepherd",
79
- { maxWarnings: 3 },
80
- { maxWarnings: 5 },
81
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, backup: false },
82
- );
83
-
84
- const settings = readTestSettings();
85
- expect(settings.context.threshold).toBe(200);
86
- expect(settings.shepherd.maxWarnings).toBe(3);
87
- expect(settings.externalSection.addedByOther).toBe(true);
88
- });
89
-
90
- it("3 个 section 快速连续 patch 全部保留", () => {
91
- writeTestSettings({
92
- context: { threshold: 100 },
93
- shepherd: { maxWarnings: 5 },
94
- mcp: { disabled: [] },
95
- });
96
-
97
- patchSettingsSectionWithBackup(
98
- "context", { threshold: 200 }, { threshold: 100 },
99
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, backup: false },
100
- );
101
- patchSettingsSectionWithBackup(
102
- "shepherd", { maxWarnings: 10 }, { maxWarnings: 5 },
103
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, backup: false },
104
- );
105
- patchSettingsSectionWithBackup(
106
- "mcp", { disabled: ["server-a"] }, { disabled: [] },
107
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, backup: false },
108
- );
109
-
110
- const settings = readTestSettings();
111
- expect(settings.context.threshold).toBe(200);
112
- expect(settings.shepherd.maxWarnings).toBe(10);
113
- expect(settings.mcp.disabled).toEqual(["server-a"]);
114
- });
115
- });
116
-
117
- // ── 备份内容完整性 ──────────────────────────
118
-
119
- describe("备份内容完整性", () => {
120
- it("备份的 JSON 与修改前深度一致", () => {
121
- const original = {
122
- context: { threshold: 100, name: "test" },
123
- deep: { nested: { value: [1, 2, 3] } },
124
- };
125
- writeTestSettings(original);
126
-
127
- patchSettingsSectionWithBackup(
128
- "context",
129
- { threshold: 200 },
130
- { threshold: 100 },
131
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR },
132
- );
133
-
134
- const backupFiles = readdirSync(TEST_BACKUP_DIR).filter((f) => f.endsWith(".json"));
135
- const backupContent = JSON.parse(readFileSync(join(TEST_BACKUP_DIR, backupFiles[0]), "utf-8"));
136
-
137
- expect(backupContent).toEqual(original);
138
- });
139
-
140
- it("两次备份内容分别对应各自修改前的状态", async () => {
141
- writeTestSettings({ context: { threshold: 100 } });
142
-
143
- patchSettingsSectionWithBackup(
144
- "context", { threshold: 200 }, { threshold: 100 },
145
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR },
146
- );
147
- await new Promise((r) => setTimeout(r, 10));
148
- patchSettingsSectionWithBackup(
149
- "context", { threshold: 300 }, { threshold: 100 },
150
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR },
151
- );
152
-
153
- const backups = listBackups({ backupDir: TEST_BACKUP_DIR });
154
- expect(backups.length).toBe(2);
155
-
156
- // 最新备份(index 0)应该是第二次修改前的状态
157
- const backup0 = JSON.parse(readFileSync(backups[0].path, "utf-8"));
158
- expect(backup0.context.threshold).toBe(200);
159
-
160
- // 旧备份(index 1)应该是第一次修改前的状态
161
- const backup1 = JSON.parse(readFileSync(backups[1].path, "utf-8"));
162
- expect(backup1.context.threshold).toBe(100);
163
- });
164
- });
165
-
166
- // ── 写入格式验证 ──────────────────────────────
167
-
168
- describe("写入格式验证", () => {
169
- it("写入的 JSON 格式合法且包含所有字段", () => {
170
- writeTestSettings({ context: { threshold: 100 } });
171
-
172
- patchSettingsSectionWithBackup(
173
- "context",
174
- { threshold: 200, name: "测试" },
175
- { threshold: 100, name: "" },
176
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, backup: false },
177
- );
178
-
179
- const raw = readFileSync(TEST_SETTINGS, "utf-8");
180
- const parsed = JSON.parse(raw);
181
- expect(parsed.context.threshold).toBe(200);
182
- expect(parsed.context.name).toBe("测试");
183
- });
184
-
185
- it("写入后 settings.json 以换行结尾", () => {
186
- writeTestSettings({ context: { threshold: 100 } });
187
-
188
- patchSettingsSectionWithBackup(
189
- "context", { threshold: 200 }, { threshold: 100 },
190
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, backup: false },
191
- );
192
-
193
- const raw = readFileSync(TEST_SETTINGS, "utf-8");
194
- expect(raw.endsWith("\n")).toBe(true);
195
- });
196
- });
197
-
198
- // ── detectConflicts 集成 ──────────────────────
199
-
200
- describe("detectConflicts 集成", () => {
201
- it("返回全局与项目级冲突列表", async () => {
202
- const { detectConfigConflicts } = await import("../project-config");
203
-
204
- // 创建一个临时项目级配置
205
- const projectDir = join(TEST_DIR, "project");
206
- const projectSettingsDir = join(projectDir, ".pi");
207
- mkdirSync(projectSettingsDir, { recursive: true });
208
-
209
- // 项目级覆盖全局配置(同 key 不同值)
210
- writeFileSync(
211
- join(projectSettingsDir, "settings.json"),
212
- JSON.stringify({ context: { threshold: 999 } }),
213
- );
214
-
215
- // detectConfigConflicts 读全局 SETTINGS_PATH,项目级 threshold=999 和全局不同
216
- const conflicts = detectConfigConflicts(projectDir);
217
-
218
- // 可能检测到项目级的 context.threshold (999) 和全局不同
219
- // 注意:这个测试依赖真实全局配置内容,所以只验证 API 不崩溃
220
- expect(Array.isArray(conflicts)).toBe(true);
221
- });
222
- });
223
-
224
- // ── rollback 到指定备份 ────────────────────────
225
-
226
- describe("rollback 到指定备份", () => {
227
- it("默认恢复最近一份备份", async () => {
228
- writeTestSettings({ context: { threshold: 100 } });
229
-
230
- patchSettingsSectionWithBackup(
231
- "context", { threshold: 200 }, { threshold: 100 },
232
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR },
233
- );
234
- await new Promise((r) => setTimeout(r, 10));
235
- patchSettingsSectionWithBackup(
236
- "context", { threshold: 300 }, { threshold: 100 },
237
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR },
238
- );
239
-
240
- // 当前是 300,回滚应该恢复到 200(最近一份备份)
241
- rollbackSettings({ settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR });
242
- expect(readTestSettings().context.threshold).toBe(200);
243
- });
244
-
245
- it("手动指定备份文件恢复到更早的状态", async () => {
246
- writeTestSettings({ context: { threshold: 100 } });
247
-
248
- patchSettingsSectionWithBackup(
249
- "context", { threshold: 200 }, { threshold: 100 },
250
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR },
251
- );
252
- await new Promise((r) => setTimeout(r, 10));
253
- patchSettingsSectionWithBackup(
254
- "context", { threshold: 300 }, { threshold: 100 },
255
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR },
256
- );
257
-
258
- const backups = listBackups({ backupDir: TEST_BACKUP_DIR });
259
- expect(backups.length).toBe(2);
260
-
261
- // 手动恢复到最早那份(threshold = 100)
262
- const oldestBackup = backups[backups.length - 1];
263
- const content = readFileSync(oldestBackup.path, "utf-8");
264
- writeFileSync(TEST_SETTINGS, content);
265
-
266
- expect(readTestSettings().context.threshold).toBe(100);
267
- });
268
- });
269
- });
@@ -1,267 +0,0 @@
1
- /**
2
- * patchSettingsSectionWithBackup 数组操作测试
3
- *
4
- * 测试 packages/extensions/skills 等数组类型 section 的增删改操作。
5
- */
6
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
10
-
11
- const TEST_DIR = join(tmpdir(), `pi-settings-array-test-${process.pid}`);
12
- const TEST_SETTINGS = join(TEST_DIR, "settings.json");
13
- const TEST_BACKUP_DIR = join(TEST_DIR, "settings-backup");
14
-
15
- const { patchSettingsSectionWithBackup } = await import("../settings-backup");
16
-
17
- function writeTestSettings(content: Record<string, any>) {
18
- mkdirSync(TEST_DIR, { recursive: true });
19
- writeFileSync(TEST_SETTINGS, JSON.stringify(content, null, "\t"));
20
- }
21
-
22
- function readTestSettings(): Record<string, any> {
23
- return JSON.parse(readFileSync(TEST_SETTINGS, "utf-8"));
24
- }
25
-
26
- beforeEach(() => {
27
- mkdirSync(TEST_DIR, { recursive: true });
28
- mkdirSync(TEST_BACKUP_DIR, { recursive: true });
29
- });
30
-
31
- afterEach(() => {
32
- rmSync(TEST_DIR, { recursive: true, force: true });
33
- });
34
-
35
- // ── addItem:向数组添加元素 ─────────────────────────────
36
-
37
- describe("patchSettingsSectionWithBackup — 数组 addItem", () => {
38
- it("应向空数组添加元素", () => {
39
- writeTestSettings({ packages: [] });
40
-
41
- const result = patchSettingsSectionWithBackup(
42
- "packages",
43
- { addItem: { source: "git:github.com/catlain/pi-foo" } },
44
- [],
45
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
46
- );
47
-
48
- expect(result.config).toEqual([{ source: "git:github.com/catlain/pi-foo" }]);
49
- const written = readTestSettings();
50
- expect(written.packages).toEqual([{ source: "git:github.com/catlain/pi-foo" }]);
51
- });
52
-
53
- it("应向已有数组追加元素", () => {
54
- writeTestSettings({
55
- packages: [{ source: "git:github.com/catlain/pi-shepherd" }],
56
- });
57
-
58
- patchSettingsSectionWithBackup(
59
- "packages",
60
- { addItem: { source: "git:github.com/catlain/pi-foo" } },
61
- [],
62
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
63
- );
64
-
65
- const written = readTestSettings();
66
- expect(written.packages).toHaveLength(2);
67
- expect(written.packages[1]).toEqual({ source: "git:github.com/catlain/pi-foo" });
68
- });
69
-
70
- it("addItem 为对象时用 source 字段去重,已存在则不重复添加", () => {
71
- writeTestSettings({
72
- packages: [{ source: "git:github.com/catlain/pi-shepherd" }],
73
- });
74
-
75
- const result = patchSettingsSectionWithBackup(
76
- "packages",
77
- { addItem: { source: "git:github.com/catlain/pi-shepherd" } },
78
- [],
79
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
80
- );
81
-
82
- expect(result.config).toHaveLength(1);
83
- });
84
-
85
- it("addItem 为字符串时精确去重", () => {
86
- writeTestSettings({
87
- packages: ["npm:pi-tool-display"],
88
- });
89
-
90
- const result = patchSettingsSectionWithBackup(
91
- "packages",
92
- { addItem: "npm:pi-tool-display" },
93
- [],
94
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
95
- );
96
-
97
- expect(result.config).toHaveLength(1);
98
- });
99
- });
100
-
101
- // ── removeItem:从数组删除元素 ─────────────────────────────
102
-
103
- describe("patchSettingsSectionWithBackup — 数组 removeItem", () => {
104
- it("应按 source 字段匹配删除对象元素", () => {
105
- writeTestSettings({
106
- packages: [
107
- { source: "git:github.com/catlain/pi-shepherd" },
108
- { source: "git:github.com/catlain/pi-foo" },
109
- ],
110
- });
111
-
112
- const result = patchSettingsSectionWithBackup(
113
- "packages",
114
- { removeItem: { source: "git:github.com/catlain/pi-foo" } },
115
- [],
116
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
117
- );
118
-
119
- expect(result.config).toEqual([{ source: "git:github.com/catlain/pi-shepherd" }]);
120
- });
121
-
122
- it("应按字符串精确匹配删除", () => {
123
- writeTestSettings({
124
- packages: ["npm:pi-tool-display", "npm:pi-foo"],
125
- });
126
-
127
- const result = patchSettingsSectionWithBackup(
128
- "packages",
129
- { removeItem: "npm:pi-foo" },
130
- [],
131
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
132
- );
133
-
134
- expect(result.config).toEqual(["npm:pi-tool-display"]);
135
- });
136
-
137
- it("removeItem 匹配不到时不应修改数组", () => {
138
- writeTestSettings({
139
- packages: [{ source: "git:github.com/catlain/pi-shepherd" }],
140
- });
141
-
142
- const result = patchSettingsSectionWithBackup(
143
- "packages",
144
- { removeItem: { source: "git:github.com/catlain/nonexistent" } },
145
- [],
146
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
147
- );
148
-
149
- expect(result.config).toHaveLength(1);
150
- });
151
- });
152
-
153
- // ── replaceItem:替换数组中匹配的元素 ─────────────────────────
154
-
155
- describe("patchSettingsSectionWithBackup — 数组 replaceItem", () => {
156
- it("应按 source 匹配并替换整个对象元素", () => {
157
- writeTestSettings({
158
- packages: [
159
- { source: "git:github.com/catlain/pi-usage-stats" }, // 旧格式(字符串被修前)
160
- ],
161
- });
162
-
163
- const result = patchSettingsSectionWithBackup(
164
- "packages",
165
- {
166
- replaceItem: {
167
- match: { source: "git:github.com/catlain/pi-usage-stats" },
168
- replacement: { source: "git:github.com/catlain/pi-usage-stats", extensions: ["+index.ts"] },
169
- },
170
- },
171
- [],
172
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
173
- );
174
-
175
- expect(result.config).toEqual([
176
- { source: "git:github.com/catlain/pi-usage-stats", extensions: ["+index.ts"] },
177
- ]);
178
- });
179
-
180
- it("应按字符串精确匹配并替换", () => {
181
- writeTestSettings({
182
- packages: ["npm:old-package"],
183
- });
184
-
185
- const result = patchSettingsSectionWithBackup(
186
- "packages",
187
- {
188
- replaceItem: {
189
- match: "npm:old-package",
190
- replacement: { source: "npm:new-package" },
191
- },
192
- },
193
- [],
194
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
195
- );
196
-
197
- expect(result.config).toEqual([{ source: "npm:new-package" }]);
198
- });
199
-
200
- it("replaceItem 匹配不到时不应修改数组", () => {
201
- writeTestSettings({
202
- packages: [{ source: "git:github.com/catlain/pi-shepherd" }],
203
- });
204
-
205
- const result = patchSettingsSectionWithBackup(
206
- "packages",
207
- {
208
- replaceItem: {
209
- match: { source: "git:github.com/catlain/nonexistent" },
210
- replacement: { source: "git:github.com/catlain/something" },
211
- },
212
- },
213
- [],
214
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
215
- );
216
-
217
- expect(result.config).toEqual([{ source: "git:github.com/catlain/pi-shepherd" }]);
218
- });
219
- });
220
-
221
- // ── 创建备份 ─────────────────────────────────────────
222
-
223
- describe("patchSettingsSectionWithBackup — 数组操作应创建备份", () => {
224
- it("addItem 应创建备份", () => {
225
- writeTestSettings({ packages: [] });
226
-
227
- const result = patchSettingsSectionWithBackup(
228
- "packages",
229
- { addItem: { source: "git:github.com/catlain/pi-foo" } },
230
- [],
231
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR },
232
- );
233
-
234
- expect(result.backupPath).toBeDefined();
235
- expect(existsSync(result.backupPath!)).toBe(true);
236
- });
237
-
238
- it("removeItem 应创建备份", () => {
239
- writeTestSettings({ packages: [{ source: "git:github.com/catlain/pi-foo" }] });
240
-
241
- const result = patchSettingsSectionWithBackup(
242
- "packages",
243
- { removeItem: { source: "git:github.com/catlain/pi-foo" } },
244
- [],
245
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR },
246
- );
247
-
248
- expect(result.backupPath).toBeDefined();
249
- });
250
- });
251
-
252
- // ── 对象 section 保持不变 ─────────────────────────────────
253
-
254
- describe("patchSettingsSectionWithBackup — 对象操作不受影响", () => {
255
- it("对象 merge 仍然正常工作", () => {
256
- writeTestSettings({ context: { distillThreshold: 5000 } });
257
-
258
- const result = patchSettingsSectionWithBackup(
259
- "context",
260
- { processorThreshold: 3000 },
261
- { distillThreshold: 8000, processorThreshold: 5000 },
262
- { settingsPath: TEST_SETTINGS, backupDir: TEST_BACKUP_DIR, validate: false },
263
- );
264
-
265
- expect(result.config).toEqual({ distillThreshold: 5000, processorThreshold: 3000 });
266
- });
267
- });