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,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
- });
@@ -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
- });