@ilya-lesikov/pi-pi 0.5.0 → 0.6.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.
- package/3p/pi-ask-user/index.ts +65 -49
- package/3p/pi-subagents/src/agent-manager.ts +8 -0
- package/3p/pi-subagents/src/agent-runner.ts +112 -19
- package/3p/pi-subagents/src/index.ts +3 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +32 -19
- package/extensions/orchestrator/agents/code-reviewer.ts +31 -17
- package/extensions/orchestrator/agents/constraints.ts +55 -0
- package/extensions/orchestrator/agents/explore.ts +13 -13
- package/extensions/orchestrator/agents/librarian.ts +12 -9
- package/extensions/orchestrator/agents/plan-reviewer.ts +31 -19
- package/extensions/orchestrator/agents/planner.ts +28 -23
- package/extensions/orchestrator/agents/registry.ts +2 -1
- package/extensions/orchestrator/agents/repo-context.ts +11 -0
- package/extensions/orchestrator/agents/task.ts +14 -11
- package/extensions/orchestrator/agents/tool-routing.ts +17 -32
- package/extensions/orchestrator/ast-search.ts +2 -1
- package/extensions/orchestrator/cbm.test.ts +35 -0
- package/extensions/orchestrator/cbm.ts +43 -13
- package/extensions/orchestrator/command-handlers.test.ts +390 -19
- package/extensions/orchestrator/command-handlers.ts +68 -28
- package/extensions/orchestrator/commands.test.ts +255 -2
- package/extensions/orchestrator/commands.ts +108 -10
- package/extensions/orchestrator/config.test.ts +289 -68
- package/extensions/orchestrator/config.ts +630 -121
- package/extensions/orchestrator/context.test.ts +177 -10
- package/extensions/orchestrator/context.ts +115 -14
- package/extensions/orchestrator/custom-footer.ts +2 -1
- package/extensions/orchestrator/doctor.test.ts +559 -0
- package/extensions/orchestrator/doctor.ts +664 -0
- package/extensions/orchestrator/event-handlers.test.ts +84 -22
- package/extensions/orchestrator/event-handlers.ts +1177 -341
- package/extensions/orchestrator/exa.test.ts +46 -0
- package/extensions/orchestrator/exa.ts +16 -10
- package/extensions/orchestrator/flant-infra.test.ts +224 -0
- package/extensions/orchestrator/flant-infra.ts +110 -41
- package/extensions/orchestrator/index.ts +13 -2
- package/extensions/orchestrator/integration.test.ts +2866 -118
- package/extensions/orchestrator/log.test.ts +219 -0
- package/extensions/orchestrator/log.ts +153 -0
- package/extensions/orchestrator/model-registry.test.ts +238 -0
- package/extensions/orchestrator/model-registry.ts +282 -0
- package/extensions/orchestrator/model-version.test.ts +27 -0
- package/extensions/orchestrator/model-version.ts +19 -0
- package/extensions/orchestrator/orchestrator.test.ts +206 -56
- package/extensions/orchestrator/orchestrator.ts +287 -140
- package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
- package/extensions/orchestrator/phases/brainstorm.ts +41 -36
- package/extensions/orchestrator/phases/implementation.ts +7 -11
- package/extensions/orchestrator/phases/machine.test.ts +27 -8
- package/extensions/orchestrator/phases/machine.ts +13 -0
- package/extensions/orchestrator/phases/planning.ts +57 -31
- package/extensions/orchestrator/phases/review-task.ts +3 -7
- package/extensions/orchestrator/phases/review.ts +38 -39
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +69 -0
- package/extensions/orchestrator/phases/verdict.test.ts +139 -0
- package/extensions/orchestrator/phases/verdict.ts +82 -0
- package/extensions/orchestrator/plannotator.test.ts +85 -0
- package/extensions/orchestrator/plannotator.ts +24 -6
- package/extensions/orchestrator/pp-menu.test.ts +134 -0
- package/extensions/orchestrator/pp-menu.ts +2557 -347
- package/extensions/orchestrator/repo-utils.test.ts +151 -0
- package/extensions/orchestrator/repo-utils.ts +67 -0
- package/extensions/orchestrator/spawn-cleanup.test.ts +57 -0
- package/extensions/orchestrator/spawn-cleanup.ts +35 -0
- package/extensions/orchestrator/state.test.ts +76 -6
- package/extensions/orchestrator/state.ts +89 -26
- package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
- package/extensions/orchestrator/test-helpers.ts +217 -0
- package/extensions/orchestrator/tracer.test.ts +132 -0
- package/extensions/orchestrator/tracer.ts +127 -0
- package/extensions/orchestrator/transition-controller.test.ts +207 -0
- package/extensions/orchestrator/transition-controller.ts +259 -0
- package/extensions/orchestrator/usage-tracker.test.ts +435 -0
- package/extensions/orchestrator/usage-tracker.ts +23 -2
- package/extensions/orchestrator/validate-artifacts.test.ts +83 -1
- package/package.json +2 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import { deepMerge, loadConfig, validateConfig } from "./config.js";
|
|
5
|
+
import { deepMerge, getDefaultConfig, loadConfig, readRawConfig, removeConfigValue, resolvePreset, validateConfig, writeConfigValue } from "./config.js";
|
|
6
6
|
|
|
7
7
|
const tempDirs: string[] = [];
|
|
8
8
|
|
|
@@ -46,63 +46,91 @@ describe("deepMerge", () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
describe("validateConfig", () => {
|
|
49
|
-
it("throws for empty
|
|
50
|
-
expect(() =>
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
it("throws for empty orchestrator model string", () => {
|
|
50
|
+
expect(() =>
|
|
51
|
+
validateConfig({
|
|
52
|
+
agents: {
|
|
53
|
+
orchestrators: {
|
|
54
|
+
implement: { model: "", thinking: "high" },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
).toThrow("config.agents.orchestrators.implement.model must be a non-empty string");
|
|
53
59
|
});
|
|
54
60
|
|
|
55
|
-
it("throws for
|
|
56
|
-
expect(() =>
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
it("throws for invalid preset names", () => {
|
|
62
|
+
expect(() =>
|
|
63
|
+
validateConfig({
|
|
64
|
+
agents: {
|
|
65
|
+
subagents: {
|
|
66
|
+
presetGroups: {
|
|
67
|
+
planners: {
|
|
68
|
+
presets: {
|
|
69
|
+
"bad name": { agents: {} },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
).toThrow("config.agents.subagents.presetGroups.planners.presets.bad name has invalid name");
|
|
59
77
|
});
|
|
60
78
|
|
|
61
|
-
it("throws
|
|
62
|
-
expect(() =>
|
|
63
|
-
|
|
79
|
+
it("throws for invalid variant names", () => {
|
|
80
|
+
expect(() =>
|
|
81
|
+
validateConfig({
|
|
82
|
+
agents: {
|
|
83
|
+
subagents: {
|
|
84
|
+
presetGroups: {
|
|
85
|
+
planners: {
|
|
86
|
+
presets: {
|
|
87
|
+
regular: {
|
|
88
|
+
agents: {
|
|
89
|
+
"bad name": { enabled: true, model: "provider/model", thinking: "high" },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
).toThrow("config.agents.subagents.presetGroups.planners.presets.regular.agents.bad name has invalid name");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("throws when commands.afterEdit is not an object", () => {
|
|
102
|
+
expect(() => validateConfig({ commands: { afterEdit: [] } as any })).toThrow(
|
|
103
|
+
"config.commands.afterEdit must be an object",
|
|
64
104
|
);
|
|
65
105
|
});
|
|
66
106
|
|
|
67
107
|
it("throws when commands.afterEdit entry has no run", () => {
|
|
68
|
-
expect(() => validateConfig({ commands: { afterEdit:
|
|
69
|
-
"config.commands.afterEdit
|
|
108
|
+
expect(() => validateConfig({ commands: { afterEdit: { cmd: { run: "", globs: ["*.ts"] } } } })).toThrow(
|
|
109
|
+
"config.commands.afterEdit.cmd.run must be a non-empty string",
|
|
70
110
|
);
|
|
71
111
|
});
|
|
72
112
|
|
|
73
113
|
it("throws when commands.afterImplement entry has no run", () => {
|
|
74
|
-
expect(() => validateConfig({ commands: { afterImplement:
|
|
75
|
-
"config.commands.afterImplement
|
|
114
|
+
expect(() => validateConfig({ commands: { afterImplement: { cmd: { run: "" } } } })).toThrow(
|
|
115
|
+
"config.commands.afterImplement.cmd.run must be a non-empty string",
|
|
76
116
|
);
|
|
77
117
|
});
|
|
78
118
|
|
|
79
|
-
it("throws for
|
|
80
|
-
expect(() => validateConfig({
|
|
81
|
-
"config.
|
|
119
|
+
it("throws for invalid duration values", () => {
|
|
120
|
+
expect(() => validateConfig({ performance: { commands: { afterEdit: -1 } } })).toThrow(
|
|
121
|
+
"config.performance.commands.afterEdit must be a valid duration",
|
|
82
122
|
);
|
|
83
123
|
});
|
|
84
124
|
|
|
85
|
-
it("accepts valid config", () => {
|
|
125
|
+
it("accepts valid partial config", () => {
|
|
86
126
|
expect(() =>
|
|
87
127
|
validateConfig({
|
|
88
|
-
|
|
89
|
-
implement: { model: "provider/model-1" },
|
|
90
|
-
debug: { model: "provider/model-2" },
|
|
91
|
-
brainstorm: { model: "provider/model-3" },
|
|
92
|
-
review: { model: "provider/model-4" },
|
|
93
|
-
},
|
|
94
|
-
planners: {
|
|
95
|
-
good: { enabled: true, model: "provider/model-4" },
|
|
96
|
-
disabled: { enabled: false },
|
|
97
|
-
},
|
|
98
|
-
brainstormReviewers: {
|
|
99
|
-
good: { enabled: true, model: "provider/model-5" },
|
|
100
|
-
},
|
|
128
|
+
general: { autoCommit: false },
|
|
101
129
|
commands: {
|
|
102
|
-
afterEdit:
|
|
103
|
-
afterImplement:
|
|
130
|
+
afterEdit: { fmt: { run: "npm run fmt", globs: ["**/*.ts"] } },
|
|
131
|
+
afterImplement: { test: { run: "npm test" } },
|
|
104
132
|
},
|
|
105
|
-
|
|
133
|
+
performance: { commands: { afterEdit: "30s" } },
|
|
106
134
|
}),
|
|
107
135
|
).not.toThrow();
|
|
108
136
|
});
|
|
@@ -118,34 +146,52 @@ describe("loadConfig", () => {
|
|
|
118
146
|
writeFileSync(
|
|
119
147
|
configPath,
|
|
120
148
|
JSON.stringify({
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
agents: {
|
|
150
|
+
orchestrators: {
|
|
151
|
+
implement: { model: "custom/implement", thinking: "low" },
|
|
152
|
+
},
|
|
153
|
+
subagents: {
|
|
154
|
+
presetGroups: {
|
|
155
|
+
planners: {
|
|
156
|
+
presets: {
|
|
157
|
+
regular: {
|
|
158
|
+
agents: {
|
|
159
|
+
opus: { enabled: false, model: "anthropic/claude-opus-latest", thinking: "high" },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
126
166
|
},
|
|
127
167
|
commands: {
|
|
128
|
-
afterImplement:
|
|
168
|
+
afterImplement: { lint: { run: "npm run lint" } },
|
|
129
169
|
},
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
performance: {
|
|
171
|
+
commands: {
|
|
172
|
+
afterEdit: 1234,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
general: {
|
|
176
|
+
autoCommit: false,
|
|
132
177
|
},
|
|
133
|
-
autoCommit: false,
|
|
134
178
|
}),
|
|
135
179
|
"utf-8",
|
|
136
180
|
);
|
|
137
181
|
|
|
138
182
|
const config = loadConfig(cwd, "/nonexistent/global/config.json");
|
|
139
183
|
|
|
140
|
-
expect(config.
|
|
141
|
-
expect(config.
|
|
142
|
-
expect(config.
|
|
143
|
-
|
|
144
|
-
expect(
|
|
145
|
-
expect(
|
|
146
|
-
expect(config.
|
|
147
|
-
expect(config.
|
|
148
|
-
expect(config.
|
|
184
|
+
expect(config.agents.orchestrators.implement.model).toBe("custom/implement");
|
|
185
|
+
expect(config.agents.orchestrators.plan.model).toBe("anthropic/claude-opus-latest");
|
|
186
|
+
expect(config.agents.orchestrators.debug.model).toBe("openai/gpt-latest");
|
|
187
|
+
const planners = resolvePreset(config, "planners");
|
|
188
|
+
expect(planners.opus.enabled).toBe(false);
|
|
189
|
+
expect(planners.opus.model).toBe("anthropic/claude-opus-latest");
|
|
190
|
+
expect(config.commands.afterEdit).toEqual({});
|
|
191
|
+
expect(config.commands.afterImplement).toEqual({ lint: { run: "npm run lint" } });
|
|
192
|
+
expect(config.performance.commands.afterEdit).toBe(1234);
|
|
193
|
+
expect(config.performance.commands.afterImplement).toBe(300000);
|
|
194
|
+
expect(config.general.autoCommit).toBe(false);
|
|
149
195
|
});
|
|
150
196
|
|
|
151
197
|
it("creates default config when config.json does not exist", () => {
|
|
@@ -154,10 +200,9 @@ describe("loadConfig", () => {
|
|
|
154
200
|
|
|
155
201
|
const config = loadConfig(cwd, "/nonexistent/global/config.json");
|
|
156
202
|
|
|
157
|
-
expect(existsSync(configPath)).toBe(
|
|
158
|
-
|
|
159
|
-
expect(
|
|
160
|
-
expect(config.mainModel.implement.model).toBe("anthropic/claude-opus-4-6");
|
|
203
|
+
expect(existsSync(configPath)).toBe(false);
|
|
204
|
+
expect(config.agents.orchestrators.implement.model).toBe("anthropic/claude-opus-latest");
|
|
205
|
+
expect(config.agents.orchestrators.plan.model).toBe("anthropic/claude-opus-latest");
|
|
161
206
|
});
|
|
162
207
|
|
|
163
208
|
it("throws parse errors with config file path", () => {
|
|
@@ -177,9 +222,110 @@ describe("loadConfig", () => {
|
|
|
177
222
|
const configPath = join(ppDir, "config.json");
|
|
178
223
|
|
|
179
224
|
mkdirSync(ppDir, { recursive: true });
|
|
180
|
-
writeFileSync(configPath, JSON.stringify({
|
|
225
|
+
writeFileSync(configPath, JSON.stringify({ performance: { commands: { afterEdit: -1 } } }), "utf-8");
|
|
226
|
+
|
|
227
|
+
expect(() => loadConfig(cwd, "/nonexistent/global/config.json")).toThrow("config.performance.commands.afterEdit must be a valid duration");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("allows project default preset that exists in global presets", () => {
|
|
231
|
+
const cwd = makeTempDir();
|
|
232
|
+
const ppDir = join(cwd, ".pp");
|
|
233
|
+
const projectConfigPath = join(ppDir, "config.json");
|
|
234
|
+
const globalConfigPath = join(cwd, "global-config.json");
|
|
235
|
+
|
|
236
|
+
mkdirSync(ppDir, { recursive: true });
|
|
237
|
+
writeFileSync(
|
|
238
|
+
globalConfigPath,
|
|
239
|
+
JSON.stringify({
|
|
240
|
+
agents: {
|
|
241
|
+
subagents: {
|
|
242
|
+
presetGroups: {
|
|
243
|
+
planners: {
|
|
244
|
+
presets: {
|
|
245
|
+
deep: {
|
|
246
|
+
enabled: true,
|
|
247
|
+
agents: {
|
|
248
|
+
custom: { enabled: true, model: "provider/model-deep", thinking: "high" },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
"utf-8",
|
|
258
|
+
);
|
|
259
|
+
writeFileSync(
|
|
260
|
+
projectConfigPath,
|
|
261
|
+
JSON.stringify({
|
|
262
|
+
agents: {
|
|
263
|
+
subagents: {
|
|
264
|
+
presetGroups: {
|
|
265
|
+
planners: {
|
|
266
|
+
default: "deep",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
"utf-8",
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const config = loadConfig(cwd, globalConfigPath);
|
|
181
276
|
|
|
182
|
-
expect(
|
|
277
|
+
expect(config.agents.subagents.presetGroups.planners.default).toBe("deep");
|
|
278
|
+
expect(resolvePreset(config, "planners")).toEqual(config.agents.subagents.presetGroups.planners.presets.deep.agents);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("throws when merged default preset points to missing preset", () => {
|
|
282
|
+
const cwd = makeTempDir();
|
|
283
|
+
const ppDir = join(cwd, ".pp");
|
|
284
|
+
const projectConfigPath = join(ppDir, "config.json");
|
|
285
|
+
const globalConfigPath = join(cwd, "global-config.json");
|
|
286
|
+
|
|
287
|
+
mkdirSync(ppDir, { recursive: true });
|
|
288
|
+
writeFileSync(
|
|
289
|
+
globalConfigPath,
|
|
290
|
+
JSON.stringify({
|
|
291
|
+
agents: {
|
|
292
|
+
subagents: {
|
|
293
|
+
presetGroups: {
|
|
294
|
+
planners: {
|
|
295
|
+
presets: {
|
|
296
|
+
regular: {
|
|
297
|
+
enabled: true,
|
|
298
|
+
agents: {
|
|
299
|
+
custom: { enabled: true, model: "provider/model-regular", thinking: "high" },
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
"utf-8",
|
|
309
|
+
);
|
|
310
|
+
writeFileSync(
|
|
311
|
+
projectConfigPath,
|
|
312
|
+
JSON.stringify({
|
|
313
|
+
agents: {
|
|
314
|
+
subagents: {
|
|
315
|
+
presetGroups: {
|
|
316
|
+
planners: {
|
|
317
|
+
default: "missing",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
}),
|
|
323
|
+
"utf-8",
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(() => loadConfig(cwd, globalConfigPath)).toThrow(
|
|
327
|
+
'config.agents.subagents.presetGroups.planners.default "missing" does not exist',
|
|
328
|
+
);
|
|
183
329
|
});
|
|
184
330
|
});
|
|
185
331
|
|
|
@@ -204,19 +350,94 @@ describe("config regressions", () => {
|
|
|
204
350
|
expect(merged.items[0].a).toBe(1);
|
|
205
351
|
});
|
|
206
352
|
|
|
207
|
-
it("rejects empty
|
|
208
|
-
expect(() =>
|
|
209
|
-
|
|
210
|
-
|
|
353
|
+
it("rejects empty simple subagent model and accepts valid simple subagent config", () => {
|
|
354
|
+
expect(() =>
|
|
355
|
+
validateConfig({
|
|
356
|
+
agents: {
|
|
357
|
+
subagents: {
|
|
358
|
+
simple: {
|
|
359
|
+
explore: { model: "", thinking: "low" },
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
}),
|
|
364
|
+
).toThrow("config.agents.subagents.simple.explore.model must be a non-empty string");
|
|
211
365
|
|
|
212
366
|
expect(() =>
|
|
213
367
|
validateConfig({
|
|
214
368
|
agents: {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
369
|
+
subagents: {
|
|
370
|
+
simple: {
|
|
371
|
+
explore: { model: "google/gemini-flash-latest", thinking: "low" },
|
|
372
|
+
librarian: { model: "google/gemini-flash-latest", thinking: "medium" },
|
|
373
|
+
task: { model: "anthropic/claude-opus-latest", thinking: "medium" },
|
|
374
|
+
},
|
|
375
|
+
},
|
|
218
376
|
},
|
|
219
377
|
}),
|
|
220
378
|
).not.toThrow();
|
|
221
379
|
});
|
|
222
380
|
});
|
|
381
|
+
|
|
382
|
+
describe("config write helpers", () => {
|
|
383
|
+
it("getDefaultConfig returns deep clones", () => {
|
|
384
|
+
const first = getDefaultConfig();
|
|
385
|
+
const second = getDefaultConfig();
|
|
386
|
+
|
|
387
|
+
first.agents.orchestrators.implement.model = "custom/model";
|
|
388
|
+
first.commands.afterEdit["cmd-1"] = { run: "echo test", globs: ["*.ts"] };
|
|
389
|
+
|
|
390
|
+
expect(second.agents.orchestrators.implement.model).toBe("anthropic/claude-opus-latest");
|
|
391
|
+
expect(second.commands.afterEdit).toEqual({});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("readRawConfig returns empty object when file does not exist", () => {
|
|
395
|
+
const filePath = join(makeTempDir(), ".pp", "config.json");
|
|
396
|
+
expect(readRawConfig(filePath)).toEqual({});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("writeConfigValue creates parent dirs and writes nested key", () => {
|
|
400
|
+
const filePath = join(makeTempDir(), ".pp", "config.json");
|
|
401
|
+
writeConfigValue(filePath, ["agents", "subagents", "presetGroups", "planners", "presets", "regular", "agents", "custom"], {
|
|
402
|
+
enabled: true,
|
|
403
|
+
model: "x/y",
|
|
404
|
+
thinking: "high",
|
|
405
|
+
});
|
|
406
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
407
|
+
expect(raw.agents.subagents.presetGroups.planners.presets.regular.agents.custom).toEqual({
|
|
408
|
+
enabled: true,
|
|
409
|
+
model: "x/y",
|
|
410
|
+
thinking: "high",
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("removeConfigValue removes nested key, prunes empty parents, and keeps file", () => {
|
|
415
|
+
const filePath = join(makeTempDir(), ".pp", "config.json");
|
|
416
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
417
|
+
writeFileSync(
|
|
418
|
+
filePath,
|
|
419
|
+
JSON.stringify({
|
|
420
|
+
agents: {
|
|
421
|
+
subagents: {
|
|
422
|
+
presetGroups: {
|
|
423
|
+
planners: {
|
|
424
|
+
presets: {
|
|
425
|
+
regular: {
|
|
426
|
+
agents: {
|
|
427
|
+
a: { enabled: true },
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
}),
|
|
436
|
+
"utf-8",
|
|
437
|
+
);
|
|
438
|
+
removeConfigValue(filePath, ["agents", "subagents", "presetGroups", "planners", "presets", "regular", "agents", "a"]);
|
|
439
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
440
|
+
expect(raw).toEqual({});
|
|
441
|
+
expect(existsSync(filePath)).toBe(true);
|
|
442
|
+
});
|
|
443
|
+
});
|