pi-plans 0.6.0 → 0.7.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/CONTRIBUTING.md +3 -3
- package/README.md +39 -37
- package/agents/reviewer.md +12 -3
- package/index.ts +42 -35
- package/package.json +1 -1
- package/references/pi-planning-workflow.md +44 -60
- package/references/plan-artifact-template.md +71 -60
- package/references/state-and-config.md +59 -43
- package/scripts/bench/pi-adapter/pi_plans_bench.py +33 -22
- package/scripts/bench/pi-adapter/rpc_driver.mjs +4 -4
- package/scripts/run-tests.ts +12 -1
- package/scripts/validate.ts +20 -9
- package/skills/debug-and-plan/SKILL.md +3 -3
- package/skills/plan-big/SKILL.md +3 -3
- package/skills/plan-normal/SKILL.md +3 -3
- package/skills/plan-small/SKILL.md +4 -4
- package/skills/plan-with-refs/SKILL.md +6 -6
- package/skills/planning/SKILL.md +1 -1
- package/src/ask-form.ts +4 -4
- package/src/auditor.ts +126 -0
- package/src/auto-approve.ts +1 -1
- package/src/autocomplete.ts +19 -17
- package/src/code-graph/commands.ts +2 -2
- package/src/code-graph/community.ts +1 -1
- package/src/code-graph/paths.ts +1 -1
- package/src/code-graph/watch.ts +2 -2
- package/src/compaction.ts +3 -3
- package/src/config-command.ts +146 -73
- package/src/dashboard.ts +257 -0
- package/src/exec.ts +692 -919
- package/src/global-state.ts +304 -0
- package/src/guard.ts +18 -19
- package/src/messaging.ts +44 -0
- package/src/plan.ts +421 -112
- package/src/query-hook.ts +4 -4
- package/src/refine-prompts.ts +12 -70
- package/src/refine-ui-helpers.ts +24 -5
- package/src/refine-ui-state.ts +1 -1
- package/src/refine-ui.ts +1 -1
- package/src/resume-command.ts +34 -128
- package/src/role-panels.ts +542 -0
- package/src/run-context.ts +3 -10
- package/src/state.ts +272 -72
- package/src/subagent.ts +19 -29
- package/src/task-tool.ts +100 -0
- package/src/tasks.ts +189 -0
- package/src/thinking-levels.ts +67 -0
- package/src/ui-language.ts +3 -54
- package/src/workflow-state.ts +63 -58
- package/tests/analyze-refs.test.ts +35 -18
- package/tests/ask-choice-schema.test.ts +0 -12
- package/tests/ask-choice.test.ts +2 -49
- package/tests/ask-form-tool.test.ts +4 -5
- package/tests/ask-form.test.ts +2 -2
- package/tests/auditor.test.ts +111 -0
- package/tests/auto-approve.test.ts +7 -10
- package/tests/autocomplete.test.ts +8 -11
- package/tests/code-graph-apply-action.test.ts +2 -2
- package/tests/code-graph-commands.test.ts +2 -2
- package/tests/code-graph-index.test.ts +2 -2
- package/tests/code-graph-loop.e2e.test.ts +1 -1
- package/tests/code-graph-mutations.test.ts +1 -1
- package/tests/code-graph-rollback.test.ts +1 -1
- package/tests/code-graph-v05.test.ts +2 -2
- package/tests/compaction.test.ts +1 -1
- package/tests/config-command.test.ts +103 -100
- package/tests/dashboard.test.ts +268 -0
- package/tests/exec-lifecycle.test.ts +181 -115
- package/tests/exec-panel-lifecycle.test.ts +106 -251
- package/tests/exec.test.ts +617 -1706
- package/tests/execute-plan.test.ts +44 -19
- package/tests/extension-load.test.ts +48 -0
- package/tests/global-state.test.ts +371 -0
- package/tests/graph-aware-file-tools.test.ts +5 -5
- package/tests/guard.test.ts +1 -1
- package/tests/multi-run.test.ts +3 -103
- package/tests/plan.test.ts +139 -62
- package/tests/plans.test.ts +7 -79
- package/tests/refine-prompts.test.ts +20 -71
- package/tests/refine-resume.test.ts +27 -22
- package/tests/refine-ui.test.ts +6 -15
- package/tests/resume-lifecycle.test.ts +37 -22
- package/tests/resume.test.ts +33 -81
- package/tests/role-panels.test.ts +391 -0
- package/tests/run-context.test.ts +1 -1
- package/tests/run-ownership.test.ts +1 -1
- package/tests/stale-ctx.test.ts +218 -0
- package/tests/state.test.ts +151 -32
- package/tests/subagent-thinking.test.ts +65 -0
- package/tests/subagent-usage.test.ts +1 -1
- package/tests/task-tool.test.ts +61 -0
- package/tests/thinking-levels.test.ts +77 -0
- package/tests/ui-language.test.ts +2 -17
- package/tests/workflow-state.test.ts +17 -99
- package/tools/analyze-refs.ts +67 -32
- package/tools/ask-choice.ts +7 -53
- package/tools/code-graph.ts +2 -2
- package/tools/execute-plan.ts +48 -99
- package/tools/graph-aware-file-tools.ts +4 -10
- package/tools/plans.ts +40 -66
- package/tools/refine.ts +101 -164
- package/agents/criticizer.md +0 -18
- package/agents/executor.md +0 -26
- package/scripts/bench/pi-adapter/__pycache__/pi_plans_bench.cpython-312.pyc +0 -0
- package/src/panel.ts +0 -473
- package/src/termination-prompt.ts +0 -73
- package/tests/goal-wait.test.ts +0 -269
- package/tests/panel-i-zero.test.ts +0 -420
- package/tests/panel.test.ts +0 -355
package/tests/panel.test.ts
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fixed tasks' status panel model + rendering tests (0.4.0 feature 2).
|
|
3
|
-
* The panel is the single source of truth for the aboveEditor widget, the
|
|
4
|
-
* status-bar summary line and the execution injection text (D-014/R-011).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { after, before, describe, it } from "node:test";
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import * as fs from "node:fs";
|
|
10
|
-
import * as path from "node:path";
|
|
11
|
-
import {
|
|
12
|
-
BADGE_ROW_COUNT,
|
|
13
|
-
MIN_PANEL_WIDTH,
|
|
14
|
-
PANEL_ROW_COUNT,
|
|
15
|
-
deriveImplReviewLoopModel,
|
|
16
|
-
deriveNextAction,
|
|
17
|
-
derivePanelModel,
|
|
18
|
-
formatActivityTime,
|
|
19
|
-
formatImplReviewLoopSummaryLine,
|
|
20
|
-
formatPanelSummaryLine,
|
|
21
|
-
renderImplReviewLoopLines,
|
|
22
|
-
renderPanelLines,
|
|
23
|
-
} from "../src/panel.ts";
|
|
24
|
-
import { visibleWidth } from "../src/refine-ui-helpers.ts";
|
|
25
|
-
|
|
26
|
-
function exec(over: Record<string, unknown> = {}) {
|
|
27
|
-
return {
|
|
28
|
-
items: [
|
|
29
|
-
{ id: "VC-001", text: "`VC-001` panel works", done: false },
|
|
30
|
-
{ id: "VC-002", text: "`VC-002` no regressions", done: false },
|
|
31
|
-
{ id: "VC-003", text: "`VC-003` docs updated", done: false },
|
|
32
|
-
],
|
|
33
|
-
implItems: [
|
|
34
|
-
{ id: "I-001", text: "Add batch form." },
|
|
35
|
-
{ id: "I-002", text: "Add fixed panel." },
|
|
36
|
-
{ id: "I-003", text: "Bump version." },
|
|
37
|
-
],
|
|
38
|
-
implStatus: {},
|
|
39
|
-
currentI: undefined,
|
|
40
|
-
goalWait: { noProgressRounds: 0, waitRounds: 0, lastMarkers: null, paused: false },
|
|
41
|
-
...over,
|
|
42
|
-
} as never;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
describe("derivePanelModel", () => {
|
|
46
|
-
it("derives remaining I, current I (inferred) and next action from fresh state", () => {
|
|
47
|
-
const model = derivePanelModel(exec(), "my-topic", false);
|
|
48
|
-
assert.equal(model.topic, "my-topic");
|
|
49
|
-
assert.equal(model.remainingI, 3);
|
|
50
|
-
assert.equal(model.totalI, 3);
|
|
51
|
-
assert.equal(model.vcDone, 0);
|
|
52
|
-
assert.equal(model.vcTotal, 3);
|
|
53
|
-
assert.equal(model.currentI, "I-001");
|
|
54
|
-
assert.equal(model.currentInferred, true);
|
|
55
|
-
assert.match(model.nextAction, /^Implement I-001/);
|
|
56
|
-
assert.equal(model.phase, "executing");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("prefers the marker-backed current I and never persists the inferred fallback", () => {
|
|
60
|
-
const model = derivePanelModel(exec({ currentI: "I-003" }), "t", false);
|
|
61
|
-
assert.equal(model.currentI, "I-003");
|
|
62
|
-
assert.equal(model.currentInferred, false);
|
|
63
|
-
assert.match(model.nextAction, /^Implement I-003/);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("drops a stale marker current I that points at an already-passed item", () => {
|
|
67
|
-
const withCoverage = {
|
|
68
|
-
items: [
|
|
69
|
-
{ id: "VC-001", text: "`VC-001` covers `I-001`; pass", done: true },
|
|
70
|
-
{ id: "VC-002", text: "`VC-002` covers `I-002`; pass", done: false },
|
|
71
|
-
],
|
|
72
|
-
implItems: [
|
|
73
|
-
{ id: "I-001", text: "One." },
|
|
74
|
-
{ id: "I-002", text: "Two." },
|
|
75
|
-
],
|
|
76
|
-
};
|
|
77
|
-
const model = derivePanelModel(exec({ ...withCoverage, currentI: "I-001" }), "t", false);
|
|
78
|
-
assert.notEqual(model.currentI, "I-001");
|
|
79
|
-
assert.equal(model.currentI, "I-002");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("marks goal-wait paused with a user-action next action", () => {
|
|
83
|
-
const model = derivePanelModel(
|
|
84
|
-
exec({
|
|
85
|
-
goalWait: { noProgressRounds: 3, waitRounds: 6, lastMarkers: null, paused: true, pausedReason: "no progress" },
|
|
86
|
-
}),
|
|
87
|
-
"t",
|
|
88
|
-
false,
|
|
89
|
-
);
|
|
90
|
-
assert.equal(model.phase, "paused");
|
|
91
|
-
assert.match(model.nextAction, /paused/);
|
|
92
|
-
assert.match(model.nextAction, /plans-execute/);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("marks waiting state from the explicit approximation signal", () => {
|
|
96
|
-
const model = derivePanelModel(
|
|
97
|
-
exec({ goalWait: { noProgressRounds: 0, waitRounds: 2, lastMarkers: null, paused: false } }),
|
|
98
|
-
"t",
|
|
99
|
-
true,
|
|
100
|
-
);
|
|
101
|
-
assert.equal(model.phase, "goal-wait");
|
|
102
|
-
assert.match(model.nextAction, /waiting for a subprocess/);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("reports completion when nothing remains", () => {
|
|
106
|
-
const model = derivePanelModel(
|
|
107
|
-
exec({
|
|
108
|
-
items: [{ id: "VC-001", text: "`VC-001` covers `I-001`, `I-002`, `I-003`; pass", done: true }],
|
|
109
|
-
implStatus: { "I-001": "implemented", "I-002": "implemented", "I-003": "implemented" },
|
|
110
|
-
}),
|
|
111
|
-
"t",
|
|
112
|
-
false,
|
|
113
|
-
);
|
|
114
|
-
assert.equal(model.remainingI, 0);
|
|
115
|
-
assert.match(model.nextAction, /Report completion/);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe("renderPanelLines", () => {
|
|
120
|
-
const WIDTHS = [20, 24, 30, 39, 60, 80, 100, 140];
|
|
121
|
-
|
|
122
|
-
it("renders exactly PANEL_ROW_COUNT rows at >= MIN_PANEL_WIDTH and BADGE_ROW_COUNT below", () => {
|
|
123
|
-
for (const width of WIDTHS) {
|
|
124
|
-
const lines = renderPanelLines(derivePanelModel(exec({ currentI: "I-002" }), "v0-4-0-表单-执行面板", false), width);
|
|
125
|
-
assert.equal(
|
|
126
|
-
lines.length,
|
|
127
|
-
width >= MIN_PANEL_WIDTH ? PANEL_ROW_COUNT : BADGE_ROW_COUNT,
|
|
128
|
-
`width ${width}`,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("never emits a line wider than the terminal width, including CJK topics", () => {
|
|
134
|
-
for (const width of WIDTHS) {
|
|
135
|
-
const lines = renderPanelLines(
|
|
136
|
-
derivePanelModel(exec({ currentI: "I-002" }), "升级-0.4.0-批量表单与固定状态面板-专题", false),
|
|
137
|
-
width,
|
|
138
|
-
);
|
|
139
|
-
for (const line of lines) {
|
|
140
|
-
assert.ok(
|
|
141
|
-
visibleWidth(line) <= width,
|
|
142
|
-
`width ${width}: ${JSON.stringify(line)} is ${visibleWidth(line)} wide`,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("keeps box borders intact and pads every content row to the fixed width", () => {
|
|
149
|
-
for (const width of WIDTHS) {
|
|
150
|
-
const lines = renderPanelLines(derivePanelModel(exec({ currentI: "I-002" }), "t", false), width);
|
|
151
|
-
if (lines.length === PANEL_ROW_COUNT) {
|
|
152
|
-
assert.ok(lines[0].startsWith("╭"));
|
|
153
|
-
assert.ok(lines[0].endsWith("╮"));
|
|
154
|
-
assert.ok(lines.at(-1)!.startsWith("╰"));
|
|
155
|
-
assert.ok(lines.at(-1)!.endsWith("╯"));
|
|
156
|
-
} else {
|
|
157
|
-
assert.ok(lines[0].startsWith("╭"));
|
|
158
|
-
assert.ok(lines.at(-1)!.endsWith("╯"));
|
|
159
|
-
}
|
|
160
|
-
for (const line of lines) {
|
|
161
|
-
assert.equal(visibleWidth(line), width, `width ${width} pads to fixed width`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("shows the current I and next action prominently", () => {
|
|
167
|
-
const lines = renderPanelLines(derivePanelModel(exec({ currentI: "I-002" }), "t", false), 100);
|
|
168
|
-
assert.ok(lines.join("\n").includes("I-002"));
|
|
169
|
-
assert.ok(lines.join("\n").includes("Next: Implement I-002"));
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("renders the goal-wait paused fixture", () => {
|
|
173
|
-
const model = derivePanelModel(
|
|
174
|
-
exec({ goalWait: { noProgressRounds: 3, waitRounds: 6, lastMarkers: null, paused: true, pausedReason: "no progress" } }),
|
|
175
|
-
"t",
|
|
176
|
-
false,
|
|
177
|
-
);
|
|
178
|
-
const lines = renderPanelLines(model, 100);
|
|
179
|
-
assert.ok(lines.join("\n").includes("paused"));
|
|
180
|
-
assert.ok(lines.join("\n").includes("plans-execute"));
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe("formatPanelSummaryLine", () => {
|
|
185
|
-
it("derives the status line from the same model as the panel", () => {
|
|
186
|
-
const model = derivePanelModel(exec({ currentI: "I-002" }), "my-topic", false);
|
|
187
|
-
const summary = formatPanelSummaryLine(model);
|
|
188
|
-
assert.match(summary, /^plans: my-topic ▸ exec/);
|
|
189
|
-
assert.match(summary, /I 0\/3/);
|
|
190
|
-
assert.match(summary, /VC 0\/3/);
|
|
191
|
-
assert.match(summary, /next: Implement I-002/);
|
|
192
|
-
assert.ok(summary.includes(model.nextAction));
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("keeps goal-wait paused readable in one line", () => {
|
|
196
|
-
const model = derivePanelModel(
|
|
197
|
-
exec({ goalWait: { noProgressRounds: 3, waitRounds: 6, lastMarkers: null, paused: true, pausedReason: "no progress" } }),
|
|
198
|
-
"t",
|
|
199
|
-
false,
|
|
200
|
-
);
|
|
201
|
-
assert.match(formatPanelSummaryLine(model), /goal-wait paused/);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe("deriveNextAction", () => {
|
|
206
|
-
it("prefers Verify wording once an item is implemented", () => {
|
|
207
|
-
const statuses = { "I-001": "implementing" as const, "I-002": "implemented" as const, "I-003": "pending" as const };
|
|
208
|
-
const next = deriveNextAction(exec(), false, statuses, [{ id: "VC-001", text: "x", done: false }], "I-002");
|
|
209
|
-
assert.match(next, /^Verify I-002/);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("falls back to the verifier list without implementation items", () => {
|
|
213
|
-
const noImpl = { items: exec().items, implItems: [], implStatus: {}, currentI: undefined, goalWait: undefined };
|
|
214
|
-
const next = deriveNextAction(noImpl, false, {}, noImpl.items, undefined);
|
|
215
|
-
assert.match(next, /^Verify VC-001/);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("suggests reporting completion only when nothing remains", () => {
|
|
219
|
-
const none = {
|
|
220
|
-
items: [{ id: "VC-001", text: "`VC-001` covers `I-001`; pass", done: true }],
|
|
221
|
-
implItems: [{ id: "I-001", text: "One." }],
|
|
222
|
-
implStatus: { "I-001": "implemented" as const },
|
|
223
|
-
currentI: undefined,
|
|
224
|
-
goalWait: undefined,
|
|
225
|
-
};
|
|
226
|
-
assert.match(deriveNextAction(none, false, { "I-001": "vc-passed" }, []), /Report completion/);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
describe("activity line (0.5.2 legend dedupe)", () => {
|
|
231
|
-
const origTZ = process.env.TZ;
|
|
232
|
-
before(() => {
|
|
233
|
-
process.env.TZ = "UTC";
|
|
234
|
-
});
|
|
235
|
-
after(() => {
|
|
236
|
-
if (origTZ === undefined) delete process.env.TZ;
|
|
237
|
-
else process.env.TZ = origTZ;
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("formatActivityTime formats a valid ISO stamp as MM-DD HH:mm", () => {
|
|
241
|
-
assert.equal(formatActivityTime("2026-09-18T15:02:00Z"), "09-18 15:02");
|
|
242
|
-
// UTC boundary: zero-padded hour + local-date roll (F-003, impl-review r1).
|
|
243
|
-
assert.equal(formatActivityTime("2026-10-01T00:00:30Z"), "10-01 00:00");
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("formatActivityTime degrades empty and invalid input to --", () => {
|
|
247
|
-
assert.equal(formatActivityTime(null), "--");
|
|
248
|
-
assert.equal(formatActivityTime(undefined), "--");
|
|
249
|
-
assert.equal(formatActivityTime(""), "--");
|
|
250
|
-
assert.equal(formatActivityTime("not-a-timestamp"), "--");
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("derives activity from the run record (status · since time)", () => {
|
|
254
|
-
const model = derivePanelModel(exec(), "t", false, {
|
|
255
|
-
status: "executing",
|
|
256
|
-
created_at: "2026-09-18T10:00:00Z",
|
|
257
|
-
updated_at: "2026-09-18T15:02:00Z",
|
|
258
|
-
});
|
|
259
|
-
assert.equal(model.activity, "executing · since 09-18 15:02");
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("falls back to created_at when updated_at is empty", () => {
|
|
263
|
-
const model = derivePanelModel(exec(), "t", false, {
|
|
264
|
-
status: "executing",
|
|
265
|
-
created_at: "2026-09-18T10:00:00Z",
|
|
266
|
-
updated_at: "",
|
|
267
|
-
});
|
|
268
|
-
assert.equal(model.activity, "executing · since 09-18 10:00");
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("falls back to the phase word without a run record", () => {
|
|
272
|
-
assert.equal(derivePanelModel(exec(), "t", false, null).activity, "executing");
|
|
273
|
-
assert.equal(derivePanelModel(exec(), "t", false, undefined).activity, "executing");
|
|
274
|
-
const paused = derivePanelModel(
|
|
275
|
-
{ items: [], goalWait: { paused: true, pausedReason: "waiting for user" } },
|
|
276
|
-
"t",
|
|
277
|
-
false,
|
|
278
|
-
null,
|
|
279
|
-
);
|
|
280
|
-
assert.equal(paused.activity, "paused");
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("renders the activity row plus a legend-free pure bottom border", () => {
|
|
284
|
-
const model = derivePanelModel(exec(), "t", false, {
|
|
285
|
-
status: "executing",
|
|
286
|
-
created_at: "2026-09-18T10:00:00Z",
|
|
287
|
-
updated_at: "2026-09-18T15:02:00Z",
|
|
288
|
-
});
|
|
289
|
-
const lines = renderPanelLines(model, 100);
|
|
290
|
-
assert.equal(lines.length, PANEL_ROW_COUNT);
|
|
291
|
-
assert.match(lines[5]!, /executing · since 09-18 15:02/);
|
|
292
|
-
const last = lines[6]!;
|
|
293
|
-
assert.ok(last.startsWith("╰"), "bottom border preserved");
|
|
294
|
-
assert.ok(!last.includes("▸"), "no embedded I-state legend");
|
|
295
|
-
assert.ok(!last.includes("[I-"), "no marker syntax in the border");
|
|
296
|
-
const all = lines.join("\n");
|
|
297
|
-
assert.ok(!all.includes("markers:"), "markers legend row removed");
|
|
298
|
-
assert.ok(!all.includes("[DONE:VC-"), "DONE marker legend removed from the panel");
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
describe("no-timer discipline", () => {
|
|
303
|
-
it("the panel module never starts periodic timers", () => {
|
|
304
|
-
const source = fs.readFileSync(path.join(process.cwd(), "src", "panel.ts"), "utf8");
|
|
305
|
-
assert.doesNotMatch(source, /setInterval|setTimeout/);
|
|
306
|
-
assert.doesNotMatch(source, /requestAnimationFrame/);
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
describe("implementation-review loop box (0.5.4, D-3)", () => {
|
|
311
|
-
it("derives the loop model from configured implementationReview state", () => {
|
|
312
|
-
const model = deriveImplReviewLoopModel("demo", {
|
|
313
|
-
terminationCondition: "goal wait: continue until no unpassed VCs remain (auto-continue each round)",
|
|
314
|
-
reviewerCount: 3,
|
|
315
|
-
completedRounds: 1,
|
|
316
|
-
});
|
|
317
|
-
assert.equal(model.roundLabel, "round 2");
|
|
318
|
-
assert.equal(model.reviewerLabel, "3 reviewers");
|
|
319
|
-
assert.match(model.conditionLabel, /goal wait/);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it("single reviewer never pluralizes; unconfigured state reads config pending", () => {
|
|
323
|
-
assert.equal(
|
|
324
|
-
deriveImplReviewLoopModel("t", { terminationCondition: "1 round", reviewerCount: 1, completedRounds: 0 }).reviewerLabel,
|
|
325
|
-
"1 reviewer",
|
|
326
|
-
);
|
|
327
|
-
const pending = deriveImplReviewLoopModel("t", { completedRounds: 0 });
|
|
328
|
-
assert.equal(pending.reviewerLabel, "reviewers config pending");
|
|
329
|
-
assert.equal(pending.conditionLabel, "termination config pending");
|
|
330
|
-
assert.equal(pending.roundLabel, "round 1");
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it("renders a bounded 4-line box that survives narrow widths", () => {
|
|
334
|
-
const model = deriveImplReviewLoopModel("a-very-long-topic-name", {
|
|
335
|
-
terminationCondition: "goal wait",
|
|
336
|
-
reviewerCount: 2,
|
|
337
|
-
completedRounds: 0,
|
|
338
|
-
});
|
|
339
|
-
for (const width of [80, 40, 24]) {
|
|
340
|
-
const lines = renderImplReviewLoopLines(model, width);
|
|
341
|
-
assert.equal(lines.length, 4, `4 lines at width ${width}`);
|
|
342
|
-
for (const line of lines) {
|
|
343
|
-
assert.ok(visibleWidth(line) <= width, `line fits width ${width}: ${line}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// Full content only at comfortable widths; narrow widths degrade by
|
|
347
|
-
// truncation (asserted above by the width bound).
|
|
348
|
-
assert.match(renderImplReviewLoopLines(model, 80)[1], /impl-review . round 1 . 2 reviewers/);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("summary line mirrors the box model (D-015)", () => {
|
|
352
|
-
const model = deriveImplReviewLoopModel("t", { terminationCondition: "1 round", reviewerCount: 3, completedRounds: 4 });
|
|
353
|
-
assert.equal(formatImplReviewLoopSummaryLine(model), "\ud83d\udd01 plans: impl-review round 5 \u00b7 3 reviewers");
|
|
354
|
-
});
|
|
355
|
-
});
|