infinity-harness 2.8.9 → 2.8.11
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/CHANGELOG.md +18 -0
- package/dist/extensions/infinity-harness/index.js +3614 -0
- package/dist/extensions/infinity-harness/index.js.map +1 -0
- package/dist/src/approval.js +154 -0
- package/dist/src/approval.js.map +1 -0
- package/dist/src/core/brief.js +309 -0
- package/dist/src/core/brief.js.map +1 -0
- package/dist/src/core/config.js +539 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/exec.js +106 -0
- package/dist/src/core/exec.js.map +1 -0
- package/dist/src/core/featureList.js +365 -0
- package/dist/src/core/featureList.js.map +1 -0
- package/dist/src/core/fsx.js +123 -0
- package/dist/src/core/fsx.js.map +1 -0
- package/dist/src/core/gates.js +490 -0
- package/dist/src/core/gates.js.map +1 -0
- package/dist/src/core/init.js +480 -0
- package/dist/src/core/init.js.map +1 -0
- package/dist/src/core/lock.js +183 -0
- package/dist/src/core/lock.js.map +1 -0
- package/dist/src/core/modelRouter.js +185 -0
- package/dist/src/core/modelRouter.js.map +1 -0
- package/dist/src/core/paths.js +129 -0
- package/dist/src/core/paths.js.map +1 -0
- package/dist/src/core/phases.js +254 -0
- package/dist/src/core/phases.js.map +1 -0
- package/dist/src/core/plan.js +15 -0
- package/dist/src/core/plan.js.map +1 -0
- package/dist/src/core/runState.js +126 -0
- package/dist/src/core/runState.js.map +1 -0
- package/dist/src/core/settings.js +877 -0
- package/dist/src/core/settings.js.map +1 -0
- package/dist/src/core/skills.js +319 -0
- package/dist/src/core/skills.js.map +1 -0
- package/dist/src/core/skillsAudit.js +194 -0
- package/dist/src/core/skillsAudit.js.map +1 -0
- package/dist/src/core/types.js +77 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/daemon/budget.js +85 -0
- package/dist/src/daemon/budget.js.map +1 -0
- package/dist/src/daemon/guard.js +122 -0
- package/dist/src/daemon/guard.js.map +1 -0
- package/dist/src/daemon/index.js +461 -0
- package/dist/src/daemon/index.js.map +1 -0
- package/dist/src/daemon/isolation.js +84 -0
- package/dist/src/daemon/isolation.js.map +1 -0
- package/dist/src/daemon/preflight.js +131 -0
- package/dist/src/daemon/preflight.js.map +1 -0
- package/dist/src/daemon/server.js +174 -0
- package/dist/src/daemon/server.js.map +1 -0
- package/dist/src/daemon/supervisorState.js +42 -0
- package/dist/src/daemon/supervisorState.js.map +1 -0
- package/dist/src/daemon/worker.js +239 -0
- package/dist/src/daemon/worker.js.map +1 -0
- package/dist/src/daemon/worktree.js +102 -0
- package/dist/src/daemon/worktree.js.map +1 -0
- package/dist/src/escalate.js +312 -0
- package/dist/src/escalate.js.map +1 -0
- package/dist/src/exec/piWorker.js +630 -0
- package/dist/src/exec/piWorker.js.map +1 -0
- package/dist/src/goal.js +317 -0
- package/dist/src/goal.js.map +1 -0
- package/dist/src/goalLoop.js +374 -0
- package/dist/src/goalLoop.js.map +1 -0
- package/dist/src/goalSpec.js +397 -0
- package/dist/src/goalSpec.js.map +1 -0
- package/dist/src/goalState.js +255 -0
- package/dist/src/goalState.js.map +1 -0
- package/dist/src/handoff.js +205 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/intake.js +203 -0
- package/dist/src/intake.js.map +1 -0
- package/dist/src/loop.js +592 -0
- package/dist/src/loop.js.map +1 -0
- package/dist/src/modelRouter.js +216 -0
- package/dist/src/modelRouter.js.map +1 -0
- package/dist/src/remote.js +246 -0
- package/dist/src/remote.js.map +1 -0
- package/dist/src/replan.js +262 -0
- package/dist/src/replan.js.map +1 -0
- package/dist/src/review.js +54 -0
- package/dist/src/review.js.map +1 -0
- package/dist/src/rework.js +241 -0
- package/dist/src/rework.js.map +1 -0
- package/dist/src/runState.js +4 -0
- package/dist/src/runState.js.map +1 -0
- package/dist/src/scheduler.js +270 -0
- package/dist/src/scheduler.js.map +1 -0
- package/dist/src/supervisor.js +787 -0
- package/dist/src/supervisor.js.map +1 -0
- package/dist/src/taskList.js +430 -0
- package/dist/src/taskList.js.map +1 -0
- package/dist/src/ui/config.js +268 -0
- package/dist/src/ui/config.js.map +1 -0
- package/dist/src/ui/dashboard.js +1288 -0
- package/dist/src/ui/dashboard.js.map +1 -0
- package/dist/src/ui/display.js +229 -0
- package/dist/src/ui/display.js.map +1 -0
- package/dist/src/ui/planTree.js +219 -0
- package/dist/src/ui/planTree.js.map +1 -0
- package/dist/src/ui/theme.js +298 -0
- package/dist/src/ui/theme.js.map +1 -0
- package/dist/src/ui/viewState.js +65 -0
- package/dist/src/ui/viewState.js.map +1 -0
- package/dist/src/ui/widget.js +721 -0
- package/dist/src/ui/widget.js.map +1 -0
- package/dist/src/ui/wizard.js +540 -0
- package/dist/src/ui/wizard.js.map +1 -0
- package/dist/src/unstuck.js +210 -0
- package/dist/src/unstuck.js.map +1 -0
- package/dist/src/worker.js +267 -0
- package/dist/src/worker.js.map +1 -0
- package/dist/src/workflow.js +250 -0
- package/dist/src/workflow.js.map +1 -0
- package/dist/tests/approval.test.js +144 -0
- package/dist/tests/approval.test.js.map +1 -0
- package/dist/tests/brief.test.js +271 -0
- package/dist/tests/brief.test.js.map +1 -0
- package/dist/tests/config.test.js +320 -0
- package/dist/tests/config.test.js.map +1 -0
- package/dist/tests/dashboard.test.js +277 -0
- package/dist/tests/dashboard.test.js.map +1 -0
- package/dist/tests/display.test.js +229 -0
- package/dist/tests/display.test.js.map +1 -0
- package/dist/tests/escalate.test.js +272 -0
- package/dist/tests/escalate.test.js.map +1 -0
- package/dist/tests/featureList.test.js +429 -0
- package/dist/tests/featureList.test.js.map +1 -0
- package/dist/tests/gates.test.js +333 -0
- package/dist/tests/gates.test.js.map +1 -0
- package/dist/tests/goal.test.js +229 -0
- package/dist/tests/goal.test.js.map +1 -0
- package/dist/tests/goalLoop.test.js +170 -0
- package/dist/tests/goalLoop.test.js.map +1 -0
- package/dist/tests/goalSpec.test.js +146 -0
- package/dist/tests/goalSpec.test.js.map +1 -0
- package/dist/tests/goalState.test.js +202 -0
- package/dist/tests/goalState.test.js.map +1 -0
- package/dist/tests/handoff.test.js +127 -0
- package/dist/tests/handoff.test.js.map +1 -0
- package/dist/tests/init.test.js +264 -0
- package/dist/tests/init.test.js.map +1 -0
- package/dist/tests/intake.test.js +288 -0
- package/dist/tests/intake.test.js.map +1 -0
- package/dist/tests/loop.test.js +453 -0
- package/dist/tests/loop.test.js.map +1 -0
- package/dist/tests/modelRouter.test.js +131 -0
- package/dist/tests/modelRouter.test.js.map +1 -0
- package/dist/tests/phases.test.js +244 -0
- package/dist/tests/phases.test.js.map +1 -0
- package/dist/tests/planTree.test.js +190 -0
- package/dist/tests/planTree.test.js.map +1 -0
- package/dist/tests/remote.test.js +380 -0
- package/dist/tests/remote.test.js.map +1 -0
- package/dist/tests/replan.test.js +113 -0
- package/dist/tests/replan.test.js.map +1 -0
- package/dist/tests/review.test.js +77 -0
- package/dist/tests/review.test.js.map +1 -0
- package/dist/tests/rework.test.js +109 -0
- package/dist/tests/rework.test.js.map +1 -0
- package/dist/tests/routing-live.test.js +121 -0
- package/dist/tests/routing-live.test.js.map +1 -0
- package/dist/tests/runState.test.js +73 -0
- package/dist/tests/runState.test.js.map +1 -0
- package/dist/tests/settings.test.js +311 -0
- package/dist/tests/settings.test.js.map +1 -0
- package/dist/tests/skills.test.js +387 -0
- package/dist/tests/skills.test.js.map +1 -0
- package/dist/tests/supervisor.test.js +437 -0
- package/dist/tests/supervisor.test.js.map +1 -0
- package/dist/tests/surface.test.js +203 -0
- package/dist/tests/surface.test.js.map +1 -0
- package/dist/tests/taskList.test.js +376 -0
- package/dist/tests/taskList.test.js.map +1 -0
- package/dist/tests/theme.test.js +149 -0
- package/dist/tests/theme.test.js.map +1 -0
- package/dist/tests/unstuck.test.js +268 -0
- package/dist/tests/unstuck.test.js.map +1 -0
- package/dist/tests/widget.test.js +424 -0
- package/dist/tests/widget.test.js.map +1 -0
- package/dist/tests/worker.test.js +306 -0
- package/dist/tests/worker.test.js.map +1 -0
- package/dist/tests/workflow.test.js +187 -0
- package/dist/tests/workflow.test.js.map +1 -0
- package/extensions/infinity-harness/index.ts +39 -12
- package/package.json +5 -2
- package/src/core/modelRouter.ts +23 -1
- package/src/daemon/index.ts +3 -3
- package/src/daemon/worker.ts +28 -5
- package/src/ui/config.ts +25 -1
- package/src/ui/wizard.ts +10 -5
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { buildBrief, renderBrief, renderBriefLine, referencedDocs } from "../src/core/brief.js";
|
|
6
|
+
import { defaultConfig } from "../src/core/config.js";
|
|
7
|
+
function mkTask(id, status, extra = {}) {
|
|
8
|
+
return { id, description: `Do ${id}`, status, dependsOn: [], subtasks: [], ...extra };
|
|
9
|
+
}
|
|
10
|
+
function plan(tasks, featureCriteria = ["the feature works end to end"]) {
|
|
11
|
+
return {
|
|
12
|
+
version: "2.0",
|
|
13
|
+
baseRevision: 3,
|
|
14
|
+
goals: [{ id: "goal-001", title: "Ship the infinity harness" }],
|
|
15
|
+
sprints: [],
|
|
16
|
+
features: [{ id: "feature-001", name: "The first feature", criteria: featureCriteria, tasks }],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function tmpProject(mutate, list) {
|
|
20
|
+
const d = mkdtempSync(join(tmpdir(), "pi-brief-"));
|
|
21
|
+
mkdirSync(join(d, "harness", "features"), { recursive: true });
|
|
22
|
+
const c = defaultConfig();
|
|
23
|
+
mutate(c);
|
|
24
|
+
writeFileSync(join(d, "harness", "config.json"), JSON.stringify(c, null, 2), "utf-8");
|
|
25
|
+
writeFileSync(join(d, "harness", "features", "feature-list.json"), JSON.stringify(list, null, 2), "utf-8");
|
|
26
|
+
return d;
|
|
27
|
+
}
|
|
28
|
+
// ── buildBrief surfaces the state that answers "what now?" ─────────────────
|
|
29
|
+
{
|
|
30
|
+
const dir = tmpProject((c) => {
|
|
31
|
+
c.currentPhase = "build";
|
|
32
|
+
c.currentRole = "generator";
|
|
33
|
+
c.taskRetryCount = 1;
|
|
34
|
+
c.phaseRetryCount = 2;
|
|
35
|
+
}, plan([
|
|
36
|
+
mkTask("task-1", "complete"),
|
|
37
|
+
mkTask("task-2", "in_progress", { criteria: ["the task's own criterion"] }),
|
|
38
|
+
mkTask("task-3", "pending"),
|
|
39
|
+
mkTask("task-4", "blocked"),
|
|
40
|
+
]));
|
|
41
|
+
try {
|
|
42
|
+
const brief = await buildBrief(dir);
|
|
43
|
+
assert.equal(brief.phase, "build");
|
|
44
|
+
assert.equal(brief.role, "generator", "the role is derived from the phase");
|
|
45
|
+
assert.equal(brief.paused, false);
|
|
46
|
+
assert.equal(brief.complete, false);
|
|
47
|
+
assert.equal(brief.goal, "Ship the infinity harness");
|
|
48
|
+
assert.deepEqual(brief.feature, { id: "feature-001", name: "The first feature" });
|
|
49
|
+
assert.ok(brief.task, "there is work to point at");
|
|
50
|
+
assert.equal(brief.task.id, "task-2", "the in-progress task is the one to work on");
|
|
51
|
+
assert.equal(brief.task.key, "feature-001/task-2");
|
|
52
|
+
assert.equal(brief.task.description, "Do task-2");
|
|
53
|
+
assert.equal(brief.task.status, "in_progress");
|
|
54
|
+
assert.deepEqual(brief.criteria, ["the task's own criterion"], "task criteria beat feature criteria");
|
|
55
|
+
assert.deepEqual(brief.progress, { tasksDone: 1, tasksTotal: 4, featuresDone: 0, featuresTotal: 1 });
|
|
56
|
+
assert.deepEqual(brief.retries, { task: 1, feature: 0, phase: 2, max: 10 });
|
|
57
|
+
assert.equal(brief.gate, null, "the gate is not run unless asked — it costs a lint/test run");
|
|
58
|
+
assert.equal(brief.validateCommand, "infinity_validate", "the brief names a tool that exists");
|
|
59
|
+
// A blocked task is something the human needs to know about.
|
|
60
|
+
assert.equal(brief.notes.length, 1, brief.notes.join(" | "));
|
|
61
|
+
assert.match(brief.notes[0], /1 task\(s\) are blocked: feature-001\/task-4/);
|
|
62
|
+
console.log("✓ buildBrief surfaces phase, role, task and progress");
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
rmSync(dir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── buildBrief falls back to feature criteria, and reports an empty plan ───
|
|
69
|
+
{
|
|
70
|
+
const dir = tmpProject((c) => {
|
|
71
|
+
c.currentPhase = "build";
|
|
72
|
+
}, plan([mkTask("task-1", "pending")]));
|
|
73
|
+
try {
|
|
74
|
+
const brief = await buildBrief(dir);
|
|
75
|
+
assert.deepEqual(brief.criteria, ["the feature works end to end"], "feature criteria fill in for a task with none");
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
rmSync(dir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
const bare = mkdtempSync(join(tmpdir(), "pi-brief-bare-"));
|
|
81
|
+
try {
|
|
82
|
+
const brief = await buildBrief(bare);
|
|
83
|
+
assert.equal(brief.phase, null);
|
|
84
|
+
assert.equal(brief.role, null);
|
|
85
|
+
assert.equal(brief.task, null);
|
|
86
|
+
assert.equal(brief.feature, null);
|
|
87
|
+
assert.equal(brief.goal, null);
|
|
88
|
+
assert.deepEqual(brief.progress, { tasksDone: 0, tasksTotal: 0, featuresDone: 0, featuresTotal: 0 });
|
|
89
|
+
assert.ok(brief.notes.some((n) => /config.json is missing/.test(n)), "an uninitialised project says so instead of pretending");
|
|
90
|
+
const text = renderBrief(brief);
|
|
91
|
+
assert.match(text, /NEXT STEP · NOT STARTED/);
|
|
92
|
+
assert.match(text, /TASK {5}\(none actionable/);
|
|
93
|
+
console.log("✓ buildBrief on an empty and an uninitialised project");
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
rmSync(bare, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ── renderBrief: the working brief ─────────────────────────────────────────
|
|
100
|
+
{
|
|
101
|
+
const dir = tmpProject((c) => {
|
|
102
|
+
c.currentPhase = "build";
|
|
103
|
+
}, plan([mkTask("task-1", "complete"), mkTask("task-2", "in_progress")]));
|
|
104
|
+
try {
|
|
105
|
+
const brief = await buildBrief(dir);
|
|
106
|
+
const config = defaultConfig();
|
|
107
|
+
config.currentPhase = "build";
|
|
108
|
+
const text = renderBrief(brief, config);
|
|
109
|
+
assert.match(text, /NEXT STEP · BUILD · feature-001 · feature-001\/task-2/, "the header names the phase and task");
|
|
110
|
+
assert.match(text, /GOAL {5}Ship the infinity harness/);
|
|
111
|
+
assert.match(text, /ROLE {5}generator — Implement the current task/, "the role comes with its intent");
|
|
112
|
+
assert.match(text, /FEATURE {2}feature-001 · The first feature/);
|
|
113
|
+
assert.match(text, /TASK {5}feature-001\/task-2 \[in_progress\]/);
|
|
114
|
+
assert.match(text, /Do task-2/);
|
|
115
|
+
assert.match(text, /PROGRESS 1\/2 tasks · 0\/1 features · retries 0\/10/);
|
|
116
|
+
assert.match(text, /PIPELINE define → plan → \[BUILD\] → verify → review → ship/);
|
|
117
|
+
assert.match(text, /ACCEPTANCE CRITERIA\n {2}- the feature works end to end/);
|
|
118
|
+
// The loop instructions are the whole contract with the agent.
|
|
119
|
+
assert.match(text, /THE LOOP/);
|
|
120
|
+
assert.match(text, /1\. Do the work described above\./);
|
|
121
|
+
assert.match(text, /2\. Call the infinity_validate tool/);
|
|
122
|
+
assert.match(text, /3\. FAIL → fix the listed checks and validate again\./);
|
|
123
|
+
assert.match(text, /4\. PASS → the harness advances the phase and issues the next brief\./);
|
|
124
|
+
assert.match(text, /Do not edit harness\/config.json by hand/);
|
|
125
|
+
assert.match(text, /The gate is the only referee\./);
|
|
126
|
+
// Without a config there is no pipeline line, but everything else stands.
|
|
127
|
+
const noConfig = renderBrief(brief);
|
|
128
|
+
assert.ok(!noConfig.includes("PIPELINE"), "the pipeline rail needs the config to know what is enabled");
|
|
129
|
+
assert.match(noConfig, /THE LOOP/);
|
|
130
|
+
console.log("✓ renderBrief contains the phase, the task key and the loop instructions");
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
rmSync(dir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ── renderBrief: the paused path renders the paused text and nothing else ──
|
|
137
|
+
{
|
|
138
|
+
const dir = tmpProject((c) => {
|
|
139
|
+
c.currentPhase = "build";
|
|
140
|
+
c.paused = true;
|
|
141
|
+
}, plan([mkTask("task-1", "in_progress")]));
|
|
142
|
+
try {
|
|
143
|
+
const brief = await buildBrief(dir);
|
|
144
|
+
assert.equal(brief.paused, true);
|
|
145
|
+
const text = renderBrief(brief, defaultConfig());
|
|
146
|
+
assert.match(text, /HARNESS PAUSED/);
|
|
147
|
+
assert.match(text, /Do not continue autonomously — tell the human and stop\./);
|
|
148
|
+
for (const leak of ["NEXT STEP", "THE LOOP", "PROGRESS", "TASK", "PIPELINE", "GOAL"]) {
|
|
149
|
+
assert.ok(!text.includes(leak), `the paused brief must not also print ${leak}`);
|
|
150
|
+
}
|
|
151
|
+
assert.equal(renderBriefLine(brief), "paused");
|
|
152
|
+
console.log("✓ the paused path renders the paused text and nothing else");
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
rmSync(dir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// ── renderBrief: the complete path ─────────────────────────────────────────
|
|
159
|
+
{
|
|
160
|
+
const dir = tmpProject((c) => {
|
|
161
|
+
c.currentPhase = "ship";
|
|
162
|
+
}, plan([mkTask("task-1", "complete"), mkTask("task-2", "complete")]));
|
|
163
|
+
try {
|
|
164
|
+
const brief = await buildBrief(dir);
|
|
165
|
+
assert.equal(brief.complete, true, "the final phase with every task done is a finished run");
|
|
166
|
+
const text = renderBrief(brief, defaultConfig());
|
|
167
|
+
assert.match(text, /PIPELINE COMPLETE/);
|
|
168
|
+
assert.match(text, /All 2 task\(s\) across 1 feature\(s\) are done/);
|
|
169
|
+
assert.match(text, /Report to the human; do not start new work\./);
|
|
170
|
+
assert.ok(!text.includes("THE LOOP"), "there is no next step to describe");
|
|
171
|
+
assert.equal(renderBriefLine(brief), "complete");
|
|
172
|
+
// The same plan mid-pipeline is not complete.
|
|
173
|
+
const midDir = tmpProject((c) => {
|
|
174
|
+
c.currentPhase = "verify";
|
|
175
|
+
}, plan([mkTask("task-1", "complete"), mkTask("task-2", "complete")]));
|
|
176
|
+
try {
|
|
177
|
+
const mid = await buildBrief(midDir);
|
|
178
|
+
assert.equal(mid.complete, false, "all tasks done is not enough — the pipeline must be finished too");
|
|
179
|
+
assert.match(renderBrief(mid), /THE LOOP/);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
rmSync(midDir, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
console.log("✓ the complete path renders the completion text");
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
rmSync(dir, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// ── retry exhaustion adds an escalation note ───────────────────────────────
|
|
191
|
+
{
|
|
192
|
+
const dir = tmpProject((c) => {
|
|
193
|
+
c.currentPhase = "build";
|
|
194
|
+
c.maxRetries = 4;
|
|
195
|
+
c.taskRetryCount = 4;
|
|
196
|
+
}, plan([mkTask("task-1", "in_progress")]));
|
|
197
|
+
try {
|
|
198
|
+
const brief = await buildBrief(dir);
|
|
199
|
+
assert.ok(brief.notes.some((n) => /Retry budget for task is exhausted/.test(n)), `expected an exhaustion note, got: ${JSON.stringify(brief.notes)}`);
|
|
200
|
+
assert.ok(brief.notes.some((n) => /escalate to the human/.test(n)));
|
|
201
|
+
const text = renderBrief(brief, defaultConfig());
|
|
202
|
+
assert.match(text, /ATTENTION/);
|
|
203
|
+
assert.match(text, /! Retry budget for task is exhausted/);
|
|
204
|
+
assert.match(text, /retries 4\/4/);
|
|
205
|
+
// One retry short of the budget, there is nothing to escalate.
|
|
206
|
+
const okDir = tmpProject((c) => {
|
|
207
|
+
c.currentPhase = "build";
|
|
208
|
+
c.maxRetries = 4;
|
|
209
|
+
c.taskRetryCount = 3;
|
|
210
|
+
}, plan([mkTask("task-1", "in_progress")]));
|
|
211
|
+
try {
|
|
212
|
+
const ok = await buildBrief(okDir);
|
|
213
|
+
assert.deepEqual(ok.notes, [], "under budget, no note");
|
|
214
|
+
assert.ok(!renderBrief(ok).includes("ATTENTION"));
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
rmSync(okDir, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
console.log("✓ retry exhaustion adds an escalation note");
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
rmSync(dir, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ── includeGate embeds a live verdict ──────────────────────────────────────
|
|
226
|
+
{
|
|
227
|
+
const dir = tmpProject((c) => {
|
|
228
|
+
c.currentPhase = "build";
|
|
229
|
+
}, plan([mkTask("task-1", "pending")]));
|
|
230
|
+
try {
|
|
231
|
+
const brief = await buildBrief(dir, { includeGate: true });
|
|
232
|
+
assert.ok(brief.gate, "a gate verdict was requested");
|
|
233
|
+
assert.equal(brief.gate.phase, "build");
|
|
234
|
+
assert.equal(brief.gate.overall, false, "one task is still open");
|
|
235
|
+
assert.ok(brief.gate.failures.includes("tasks-complete"));
|
|
236
|
+
const text = renderBrief(brief);
|
|
237
|
+
assert.match(text, /GATE {5}FAIL/);
|
|
238
|
+
assert.match(text, /x tasks-complete: /, "failing checks are marked x");
|
|
239
|
+
assert.match(text, /· lint: no lint command configured/, "advisory checks are marked ·");
|
|
240
|
+
console.log("✓ includeGate embeds a live gate verdict");
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
rmSync(dir, { recursive: true, force: true });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// ── renderBriefLine and referencedDocs ─────────────────────────────────────
|
|
247
|
+
{
|
|
248
|
+
const dir = tmpProject((c) => {
|
|
249
|
+
c.currentPhase = "build";
|
|
250
|
+
}, plan([mkTask("task-1", "complete"), mkTask("task-2", "in_progress"), mkTask("task-3", "pending")]));
|
|
251
|
+
try {
|
|
252
|
+
const brief = await buildBrief(dir);
|
|
253
|
+
assert.equal(renderBriefLine(brief), "build · feature-001/task-2 · 1/3");
|
|
254
|
+
assert.deepEqual(referencedDocs(dir, null), [], "no phase, no docs");
|
|
255
|
+
assert.deepEqual(referencedDocs(dir, "build"), [], "nothing to point at yet");
|
|
256
|
+
mkdirSync(join(dir, "harness", "docs", "phases"), { recursive: true });
|
|
257
|
+
mkdirSync(join(dir, "harness", "docs", "agents"), { recursive: true });
|
|
258
|
+
writeFileSync(join(dir, "harness", "docs", "phases", "build.md"), "# build", "utf-8");
|
|
259
|
+
writeFileSync(join(dir, "harness", "docs", "agents", "generator.md"), "# generator", "utf-8");
|
|
260
|
+
const docs = referencedDocs(dir, "build");
|
|
261
|
+
assert.equal(docs.length, 2);
|
|
262
|
+
assert.ok(docs[0].endsWith(join("docs", "phases", "build.md")));
|
|
263
|
+
assert.ok(docs[1].endsWith(join("docs", "agents", "generator.md")), "the doc follows the phase's role");
|
|
264
|
+
console.log("✓ renderBriefLine and referencedDocs");
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
rmSync(dir, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
console.log("All brief tests PASS");
|
|
271
|
+
//# sourceMappingURL=brief.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brief.test.js","sourceRoot":"","sources":["../../tests/brief.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { COVERAGE_THRESHOLD_DEFAULT, DEFAULT_FEATURE_RETRIES, DEFAULT_MAX_RETRIES, DEFAULT_PHASE_RETRIES, GATE_HISTORY_LIMIT, currentRoleFor, defaultConfig, getKey, getRetryConfig, incrementFeatureRetry, incrementPhaseRetry, incrementTaskRetry, isHarnessProject, isRetryExhausted, loadConfig, recordGate, resetFeatureRetry, resetPhaseRetry, resetTaskRetry, saveConfig, setKey, trimGateHistory, validateConfig, } from "../src/core/config.js";
|
|
6
|
+
import { DEFAULT_ENABLED_PHASES } from "../src/core/types.js";
|
|
7
|
+
function tmpProject() {
|
|
8
|
+
const d = mkdtempSync(join(tmpdir(), "pi-config-"));
|
|
9
|
+
mkdirSync(join(d, "harness"), { recursive: true });
|
|
10
|
+
return d;
|
|
11
|
+
}
|
|
12
|
+
function configFile(dir) {
|
|
13
|
+
return join(dir, "harness", "config.json");
|
|
14
|
+
}
|
|
15
|
+
function writeRaw(dir, text) {
|
|
16
|
+
writeFileSync(configFile(dir), text, "utf-8");
|
|
17
|
+
}
|
|
18
|
+
// ── defaults ───────────────────────────────────────────────────────────────
|
|
19
|
+
{
|
|
20
|
+
const c = defaultConfig();
|
|
21
|
+
assert.equal(c.version, "2.0");
|
|
22
|
+
assert.equal(c.mode, "copilot");
|
|
23
|
+
assert.equal(c.currentPhase, null, "a fresh project has not entered a phase");
|
|
24
|
+
assert.equal(c.currentRole, null);
|
|
25
|
+
assert.equal(c.paused, false);
|
|
26
|
+
assert.equal(c.gates.enabled, true, "gates are on by default — that is the whole point");
|
|
27
|
+
assert.deepEqual(c.gates.checks, ["all"]);
|
|
28
|
+
assert.equal(c.gates.coverage.enabled, false);
|
|
29
|
+
assert.equal(c.gates.coverage.threshold, COVERAGE_THRESHOLD_DEFAULT);
|
|
30
|
+
assert.equal(c.gates.antiPlaceholder.enabled, true);
|
|
31
|
+
assert.deepEqual(c.phases.enabled, DEFAULT_ENABLED_PHASES);
|
|
32
|
+
assert.ok(!c.phases.enabled.includes("simplify"), "SIMPLIFY is opt-in");
|
|
33
|
+
assert.equal(c.maxRetries, DEFAULT_MAX_RETRIES);
|
|
34
|
+
assert.deepEqual(c.gateHistory, []);
|
|
35
|
+
assert.deepEqual(validateConfig(c), [], "the defaults are themselves valid");
|
|
36
|
+
// Each call is a fresh object — one caller's mutation cannot leak into another's.
|
|
37
|
+
const other = defaultConfig();
|
|
38
|
+
other.gates.checks.push("lint");
|
|
39
|
+
assert.deepEqual(defaultConfig().gates.checks, ["all"]);
|
|
40
|
+
assert.notEqual(c.phases.enabled, other.phases.enabled);
|
|
41
|
+
console.log("✓ defaults");
|
|
42
|
+
}
|
|
43
|
+
// ── deep merge over defaults ───────────────────────────────────────────────
|
|
44
|
+
{
|
|
45
|
+
const dir = tmpProject();
|
|
46
|
+
try {
|
|
47
|
+
writeRaw(dir, JSON.stringify({
|
|
48
|
+
mode: "autopilot",
|
|
49
|
+
currentPhase: "build",
|
|
50
|
+
gates: { coverage: { threshold: 95 }, checks: ["lint", "tests"] },
|
|
51
|
+
phases: { enabled: ["define", "build"] },
|
|
52
|
+
retry: { features: { enabled: true } },
|
|
53
|
+
customFutureKey: { nested: true },
|
|
54
|
+
}));
|
|
55
|
+
const { ok, config, seeded, error } = loadConfig(dir);
|
|
56
|
+
assert.equal(ok, true);
|
|
57
|
+
assert.equal(seeded, false);
|
|
58
|
+
assert.equal(error, null);
|
|
59
|
+
// Supplied values win.
|
|
60
|
+
assert.equal(config.mode, "autopilot");
|
|
61
|
+
assert.equal(config.currentPhase, "build");
|
|
62
|
+
assert.equal(config.gates.coverage.threshold, 95);
|
|
63
|
+
// Nested defaults the partial never mentioned survive the merge.
|
|
64
|
+
assert.equal(config.gates.enabled, true, "gates.enabled survives a partial gates block");
|
|
65
|
+
assert.equal(config.gates.coverage.enabled, false, "sibling key inside a nested object survives");
|
|
66
|
+
assert.equal(config.gates.antiPlaceholder.enabled, true);
|
|
67
|
+
assert.equal(config.retry.features.maxRetries, DEFAULT_FEATURE_RETRIES, "sibling of a nested override survives");
|
|
68
|
+
assert.equal(config.retry.tasks.enabled, true, "untouched nested branches survive");
|
|
69
|
+
assert.equal(config.maxRetries, DEFAULT_MAX_RETRIES);
|
|
70
|
+
// Arrays are replaced wholesale, never element-merged.
|
|
71
|
+
assert.deepEqual(config.gates.checks, ["lint", "tests"]);
|
|
72
|
+
assert.deepEqual(config.phases.enabled, ["define", "build"]);
|
|
73
|
+
// Keys the harness does not know about are carried through untouched.
|
|
74
|
+
assert.deepEqual(config.customFutureKey, { nested: true });
|
|
75
|
+
console.log("✓ deep merge keeps nested defaults and replaces arrays");
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
rmSync(dir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ── loadConfig: missing vs empty vs corrupt ────────────────────────────────
|
|
82
|
+
{
|
|
83
|
+
const dir = tmpProject();
|
|
84
|
+
try {
|
|
85
|
+
assert.equal(isHarnessProject(dir), false);
|
|
86
|
+
const missing = loadConfig(dir);
|
|
87
|
+
assert.equal(missing.ok, false, "an uninitialised project is not ok");
|
|
88
|
+
assert.equal(missing.seeded, true, "…but it is seeded with defaults so callers can still read state");
|
|
89
|
+
assert.match(missing.error ?? "", /no harness\/config.json/);
|
|
90
|
+
assert.deepEqual(missing.config, defaultConfig());
|
|
91
|
+
writeRaw(dir, "");
|
|
92
|
+
assert.equal(isHarnessProject(dir), true, "the file exists even though it says nothing");
|
|
93
|
+
const empty = loadConfig(dir);
|
|
94
|
+
assert.equal(empty.ok, false);
|
|
95
|
+
assert.equal(empty.seeded, true);
|
|
96
|
+
assert.match(empty.error ?? "", /empty/);
|
|
97
|
+
// A corrupt file must never throw: every lifecycle hook calls loadConfig,
|
|
98
|
+
// and a throw there kills the session.
|
|
99
|
+
writeRaw(dir, '{ "mode": "autopilot", ');
|
|
100
|
+
let corrupt;
|
|
101
|
+
assert.doesNotThrow(() => {
|
|
102
|
+
corrupt = loadConfig(dir);
|
|
103
|
+
}, "loadConfig must not throw on a corrupt file");
|
|
104
|
+
assert.equal(corrupt.ok, false);
|
|
105
|
+
assert.equal(corrupt.seeded, false, "corrupt is not the same as absent — do not claim it was seeded");
|
|
106
|
+
assert.ok(corrupt.error && corrupt.error.length > 0, "the parse error is reported");
|
|
107
|
+
assert.match(corrupt.error, /not valid JSON/);
|
|
108
|
+
assert.deepEqual(corrupt.config, defaultConfig(), "callers still get a usable config object");
|
|
109
|
+
// The corrupt file is left alone rather than clobbered with defaults.
|
|
110
|
+
assert.equal(readFileSync(configFile(dir), "utf-8"), '{ "mode": "autopilot", ');
|
|
111
|
+
console.log("✓ loadConfig: missing, empty and corrupt all degrade without throwing");
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
rmSync(dir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ── saveConfig round-trip ──────────────────────────────────────────────────
|
|
118
|
+
{
|
|
119
|
+
const dir = tmpProject();
|
|
120
|
+
try {
|
|
121
|
+
const c = defaultConfig();
|
|
122
|
+
c.currentPhase = "verify";
|
|
123
|
+
c.currentRole = currentRoleFor("verify");
|
|
124
|
+
c.currentFeature = "feature-002";
|
|
125
|
+
c.currentTask = "feature-002/task-3";
|
|
126
|
+
c.taskRetryCount = 4;
|
|
127
|
+
c.gates.coverage.threshold = 91;
|
|
128
|
+
c.commands.test = "npm test";
|
|
129
|
+
recordGate(c, "build", "pass", { feature: "feature-001", task: "task-1" });
|
|
130
|
+
const saved = saveConfig(dir, c);
|
|
131
|
+
assert.equal(saved.ok, true);
|
|
132
|
+
assert.equal(saved.error, null);
|
|
133
|
+
assert.ok(existsSync(configFile(dir)));
|
|
134
|
+
const round = loadConfig(dir);
|
|
135
|
+
assert.equal(round.ok, true);
|
|
136
|
+
assert.deepEqual(round.config, c, "everything written comes back identical");
|
|
137
|
+
assert.equal(round.config.currentRole, "evaluator");
|
|
138
|
+
assert.equal(round.config.gateHistory.length, 1);
|
|
139
|
+
assert.equal(round.config.gateHistory[0].feature, "feature-001");
|
|
140
|
+
// The previous revision is kept beside the file before it is overwritten.
|
|
141
|
+
assert.equal(existsSync(configFile(dir) + ".bak"), false, "nothing to back up on the first write");
|
|
142
|
+
round.config.currentPhase = "review";
|
|
143
|
+
saveConfig(dir, round.config);
|
|
144
|
+
assert.equal(existsSync(configFile(dir) + ".bak"), true, "the prior revision is kept");
|
|
145
|
+
assert.equal(JSON.parse(readFileSync(configFile(dir) + ".bak", "utf-8")).currentPhase, "verify");
|
|
146
|
+
assert.equal(loadConfig(dir).config.currentPhase, "review");
|
|
147
|
+
console.log("✓ saveConfig round-trip and backup");
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
rmSync(dir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// ── gate history ───────────────────────────────────────────────────────────
|
|
154
|
+
{
|
|
155
|
+
const c = defaultConfig();
|
|
156
|
+
recordGate(c, "build", "fail");
|
|
157
|
+
assert.equal(c.gateHistory.length, 1);
|
|
158
|
+
const entry = c.gateHistory[0];
|
|
159
|
+
assert.equal(entry.phase, "build");
|
|
160
|
+
assert.equal(entry.result, "fail");
|
|
161
|
+
assert.ok(!Number.isNaN(Date.parse(entry.timestamp)), "the timestamp is an ISO instant");
|
|
162
|
+
assert.equal("feature" in entry, false, "scope keys are omitted, not written as undefined");
|
|
163
|
+
assert.equal("task" in entry, false);
|
|
164
|
+
recordGate(c, "verify", "pass", { feature: "feature-001", task: "task-2" });
|
|
165
|
+
assert.equal(c.gateHistory[1].feature, "feature-001");
|
|
166
|
+
assert.equal(c.gateHistory[1].task, "task-2");
|
|
167
|
+
// A corrupted gateHistory is repaired rather than allowed to throw.
|
|
168
|
+
c.gateHistory = "not an array";
|
|
169
|
+
trimGateHistory(c);
|
|
170
|
+
assert.deepEqual(c.gateHistory, []);
|
|
171
|
+
c.gateHistory = null;
|
|
172
|
+
recordGate(c, "ship", "pass");
|
|
173
|
+
assert.equal(c.gateHistory.length, 1);
|
|
174
|
+
// Unbounded growth is a real problem on multi-day runs: the newest entries win.
|
|
175
|
+
const big = defaultConfig();
|
|
176
|
+
const overflow = GATE_HISTORY_LIMIT + 100;
|
|
177
|
+
for (let i = 0; i < overflow; i++)
|
|
178
|
+
recordGate(big, `p${i}`, i % 2 === 0 ? "pass" : "fail");
|
|
179
|
+
assert.equal(big.gateHistory.length, GATE_HISTORY_LIMIT, "history is capped");
|
|
180
|
+
assert.equal(big.gateHistory[GATE_HISTORY_LIMIT - 1].phase, `p${overflow - 1}`, "the newest entry is kept");
|
|
181
|
+
assert.equal(big.gateHistory[0].phase, `p${overflow - GATE_HISTORY_LIMIT}`, "the oldest entries are dropped");
|
|
182
|
+
// trimGateHistory is idempotent and leaves an under-cap history alone.
|
|
183
|
+
const before = JSON.stringify(big.gateHistory);
|
|
184
|
+
trimGateHistory(big);
|
|
185
|
+
assert.equal(JSON.stringify(big.gateHistory), before);
|
|
186
|
+
console.log("✓ recordGate and trimGateHistory cap the history at GATE_HISTORY_LIMIT");
|
|
187
|
+
}
|
|
188
|
+
// ── retry budgets ──────────────────────────────────────────────────────────
|
|
189
|
+
{
|
|
190
|
+
const c = defaultConfig();
|
|
191
|
+
const r = getRetryConfig(c);
|
|
192
|
+
assert.equal(r.tasks.enabled, true);
|
|
193
|
+
assert.equal(r.tasks.max, DEFAULT_MAX_RETRIES);
|
|
194
|
+
assert.equal(r.features.enabled, false);
|
|
195
|
+
assert.equal(r.features.max, DEFAULT_FEATURE_RETRIES);
|
|
196
|
+
assert.equal(r.phases.enabled, false);
|
|
197
|
+
assert.equal(r.phases.max, DEFAULT_PHASE_RETRIES);
|
|
198
|
+
assert.equal(r.levels.task.enabled, true);
|
|
199
|
+
assert.equal(r.levels.subtask.max, 3);
|
|
200
|
+
assert.equal(r.levels.goal.max, 2);
|
|
201
|
+
// A null task budget inherits the legacy `maxRetries` knob.
|
|
202
|
+
c.maxRetries = 3;
|
|
203
|
+
assert.equal(getRetryConfig(c).tasks.max, 3, "tasks.maxRetries=null falls back to maxRetries");
|
|
204
|
+
c.retry.tasks.maxRetries = 7;
|
|
205
|
+
assert.equal(getRetryConfig(c).tasks.max, 7, "an explicit budget wins over the legacy knob");
|
|
206
|
+
// Counters.
|
|
207
|
+
const k = defaultConfig();
|
|
208
|
+
assert.equal(incrementTaskRetry(k), 1);
|
|
209
|
+
assert.equal(incrementTaskRetry(k), 2);
|
|
210
|
+
resetTaskRetry(k);
|
|
211
|
+
assert.equal(k.taskRetryCount, 0);
|
|
212
|
+
assert.equal(incrementFeatureRetry(k), 1);
|
|
213
|
+
resetFeatureRetry(k);
|
|
214
|
+
assert.equal(k.featureRetryCount, 0);
|
|
215
|
+
assert.equal(incrementPhaseRetry(k), 1);
|
|
216
|
+
assert.equal(k.retryCount, 1, "the phase counter keeps the legacy retryCount in step");
|
|
217
|
+
incrementPhaseRetry(k);
|
|
218
|
+
assert.equal(k.phaseRetryCount, 2);
|
|
219
|
+
assert.equal(k.retryCount, 2);
|
|
220
|
+
resetPhaseRetry(k);
|
|
221
|
+
assert.equal(k.phaseRetryCount, 0);
|
|
222
|
+
assert.equal(k.retryCount, 0);
|
|
223
|
+
// Counters survive an absent field (an older config file).
|
|
224
|
+
const legacy = defaultConfig();
|
|
225
|
+
delete legacy.taskRetryCount;
|
|
226
|
+
assert.equal(incrementTaskRetry(legacy), 1);
|
|
227
|
+
console.log("✓ retry counters");
|
|
228
|
+
}
|
|
229
|
+
// ── isRetryExhausted respects the enabled flags ────────────────────────────
|
|
230
|
+
{
|
|
231
|
+
const c = defaultConfig();
|
|
232
|
+
assert.deepEqual(isRetryExhausted(c), { exhausted: false, which: null });
|
|
233
|
+
c.taskRetryCount = DEFAULT_MAX_RETRIES;
|
|
234
|
+
assert.deepEqual(isRetryExhausted(c), { exhausted: true, which: "task" }, "task budget is enabled by default");
|
|
235
|
+
// A disabled bucket is never exhausted, however high the counter goes.
|
|
236
|
+
c.retry.tasks.enabled = false;
|
|
237
|
+
c.taskRetryCount = 9999;
|
|
238
|
+
assert.deepEqual(isRetryExhausted(c), { exhausted: false, which: null }, "a disabled budget cannot stop the run");
|
|
239
|
+
c.featureRetryCount = 99;
|
|
240
|
+
assert.equal(isRetryExhausted(c).exhausted, false, "the feature budget is off by default too");
|
|
241
|
+
c.retry.features.enabled = true;
|
|
242
|
+
assert.deepEqual(isRetryExhausted(c), { exhausted: true, which: "feature" });
|
|
243
|
+
const p = defaultConfig();
|
|
244
|
+
p.retry.tasks.enabled = false;
|
|
245
|
+
p.retry.phases.enabled = true;
|
|
246
|
+
p.phaseRetryCount = DEFAULT_PHASE_RETRIES - 1;
|
|
247
|
+
assert.equal(isRetryExhausted(p).exhausted, false, "one under the limit is not exhausted");
|
|
248
|
+
p.phaseRetryCount = DEFAULT_PHASE_RETRIES;
|
|
249
|
+
assert.deepEqual(isRetryExhausted(p), { exhausted: true, which: "phase" }, "the limit itself is exhausted");
|
|
250
|
+
// Reporting order: the most specific budget is named first.
|
|
251
|
+
const all = defaultConfig();
|
|
252
|
+
all.retry.features.enabled = true;
|
|
253
|
+
all.retry.phases.enabled = true;
|
|
254
|
+
all.taskRetryCount = 999;
|
|
255
|
+
all.featureRetryCount = 999;
|
|
256
|
+
all.phaseRetryCount = 999;
|
|
257
|
+
assert.equal(isRetryExhausted(all).which, "task");
|
|
258
|
+
console.log("✓ isRetryExhausted respects the enabled flags");
|
|
259
|
+
}
|
|
260
|
+
// ── dotted get/set ─────────────────────────────────────────────────────────
|
|
261
|
+
{
|
|
262
|
+
const c = defaultConfig();
|
|
263
|
+
assert.equal(getKey(c, "mode"), "copilot");
|
|
264
|
+
assert.equal(getKey(c, "gates.coverage.threshold"), COVERAGE_THRESHOLD_DEFAULT);
|
|
265
|
+
assert.equal(getKey(c, "retry.tasks.enabled"), true);
|
|
266
|
+
assert.deepEqual(getKey(c, "gates.checks"), ["all"]);
|
|
267
|
+
assert.equal(getKey(c, "gates.nope"), undefined, "an unknown leaf is undefined");
|
|
268
|
+
assert.equal(getKey(c, "nope.nope.nope"), undefined, "an unknown branch is undefined, not a throw");
|
|
269
|
+
assert.equal(getKey(c, "maxRetries.deeper"), undefined, "descending into a scalar is undefined");
|
|
270
|
+
setKey(c, "gates.coverage.threshold", 65);
|
|
271
|
+
assert.equal(c.gates.coverage.threshold, 65);
|
|
272
|
+
setKey(c, "mode", "autopilot");
|
|
273
|
+
assert.equal(c.mode, "autopilot");
|
|
274
|
+
setKey(c, "gates.checks", ["lint"]);
|
|
275
|
+
assert.deepEqual(c.gates.checks, ["lint"]);
|
|
276
|
+
// Setting one key in an existing section leaves its siblings alone — the
|
|
277
|
+
// config TUI writes a single setting at a time and must not blank the rest.
|
|
278
|
+
setKey(c, "loop.maxIterations", 25);
|
|
279
|
+
assert.equal(c.loop.maxIterations, 25);
|
|
280
|
+
assert.equal(c.loop.noProgressLimit, 3, "a sibling in the same section survives");
|
|
281
|
+
// Missing intermediates are created all the way down.
|
|
282
|
+
setKey(c, "custom.nested.deep.value", "x");
|
|
283
|
+
assert.equal(getKey(c, "custom.nested.deep.value"), "x");
|
|
284
|
+
setKey(c, "custom.nested.other", 1);
|
|
285
|
+
assert.equal(getKey(c, "custom.nested.deep.value"), "x", "creating a sibling does not wipe the branch");
|
|
286
|
+
// A scalar standing where an object is needed is replaced.
|
|
287
|
+
setKey(c, "maxRetries.deeper", 1);
|
|
288
|
+
assert.deepEqual(c.maxRetries, { deeper: 1 });
|
|
289
|
+
// Dotted writes survive a save/load round-trip.
|
|
290
|
+
const dir = tmpProject();
|
|
291
|
+
try {
|
|
292
|
+
const fresh = defaultConfig();
|
|
293
|
+
setKey(fresh, "loop.maxWallClockMs", 1234);
|
|
294
|
+
saveConfig(dir, fresh);
|
|
295
|
+
assert.equal(getKey(loadConfig(dir).config, "loop.maxWallClockMs"), 1234);
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
rmSync(dir, { recursive: true, force: true });
|
|
299
|
+
}
|
|
300
|
+
console.log("✓ getKey/setKey on dotted paths");
|
|
301
|
+
}
|
|
302
|
+
// ── validateConfig / currentRoleFor ────────────────────────────────────────
|
|
303
|
+
{
|
|
304
|
+
const c = defaultConfig();
|
|
305
|
+
delete c.gates;
|
|
306
|
+
c.mode = null;
|
|
307
|
+
const missing = validateConfig(c);
|
|
308
|
+
assert.ok(missing.includes("gates"));
|
|
309
|
+
assert.ok(missing.includes("mode"));
|
|
310
|
+
assert.ok(!missing.includes("currentPhase"), "currentPhase is legitimately null before INIT");
|
|
311
|
+
assert.equal(currentRoleFor(null), null);
|
|
312
|
+
assert.equal(currentRoleFor("plan"), "planner");
|
|
313
|
+
assert.equal(currentRoleFor("build"), "generator");
|
|
314
|
+
assert.equal(currentRoleFor("verify"), "evaluator");
|
|
315
|
+
assert.equal(currentRoleFor("simplify"), "simplifier");
|
|
316
|
+
assert.equal(currentRoleFor("ship"), "evaluator");
|
|
317
|
+
console.log("✓ validateConfig and currentRoleFor");
|
|
318
|
+
}
|
|
319
|
+
console.log("All config tests PASS");
|
|
320
|
+
//# sourceMappingURL=config.test.js.map
|