pi-plans 0.6.0 → 0.8.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/AGENTS.md +58 -0
- package/CONTRIBUTING.md +8 -15
- package/README.md +39 -37
- package/agents/execution-reviewer.md +40 -0
- package/agents/reviewer.md +12 -3
- package/index.ts +55 -58
- package/package.json +2 -1
- package/references/pi-planning-workflow.md +50 -60
- package/references/plan-artifact-template.md +81 -60
- package/references/state-and-config.md +60 -44
- 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 +39 -11
- 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 +227 -0
- package/src/auto-approve.ts +1 -1
- package/src/autocomplete.ts +19 -17
- package/src/code-graph/commands.ts +8 -3
- 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 +303 -0
- package/src/exec.ts +1185 -924
- 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 +19 -3
- package/src/resume-command.ts +45 -129
- package/src/resume.ts +5 -1
- package/src/role-panels.ts +542 -0
- package/src/run-context.ts +3 -10
- package/src/staleness.ts +53 -0
- package/src/state.ts +273 -72
- package/src/subagent.ts +19 -29
- package/src/task-tool.ts +100 -0
- package/src/tasks.ts +223 -0
- package/src/thinking-levels.ts +67 -0
- package/src/ui-language.ts +7 -54
- package/src/workflow-state.ts +76 -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 +210 -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 +402 -0
- package/tests/exec-lifecycle.test.ts +181 -115
- package/tests/exec-panel-lifecycle.test.ts +106 -251
- package/tests/exec-review-loop.test.ts +331 -0
- package/tests/exec.test.ts +771 -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 +41 -22
- package/tests/resume.test.ts +39 -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/staleness.test.ts +76 -0
- package/tests/state.test.ts +155 -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/tasks.test.ts +142 -0
- package/tests/thinking-levels.test.ts +77 -0
- package/tests/ui-language.test.ts +2 -17
- package/tests/workflow-state.test.ts +73 -90
- 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 +55 -99
- package/tools/graph-aware-file-tools.ts +4 -10
- package/tools/plans.ts +41 -67
- 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
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/** Tests for the task dashboard (v0.6.1): compact widget, expanded tree,
|
|
2
|
+
* markers, width adaptation, and the shared summary line.
|
|
3
|
+
*
|
|
4
|
+
* Width invariant: the widget host measures every rendered line with pi-tui's
|
|
5
|
+
* `visibleWidth` and throws an uncaughtException — killing the whole TUI
|
|
6
|
+
* session — when a line is wider than the terminal (real crashes:
|
|
7
|
+
* "Rendered line 157 exceeds terminal width (231 > 230)" and
|
|
8
|
+
* "(114 > 113)"). These tests therefore measure with the *host's* function,
|
|
9
|
+
* not a local reimplementation, so a local/host divergence is caught here
|
|
10
|
+
* rather than in a user's terminal.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as assert from "node:assert/strict";
|
|
14
|
+
import { describe, it } from "node:test";
|
|
15
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
16
|
+
import { parsePlanTasks } from "../src/plan.ts";
|
|
17
|
+
import { buildTaskView } from "../src/tasks.ts";
|
|
18
|
+
import { visibleWidth as localVisibleWidth } from "../src/refine-ui-helpers.ts";
|
|
19
|
+
import {
|
|
20
|
+
deriveDashboardModel,
|
|
21
|
+
formatDashboardSummaryLine,
|
|
22
|
+
openTaskSummary,
|
|
23
|
+
renderDashboardLines,
|
|
24
|
+
renderDashboardTreeLines,
|
|
25
|
+
taskMarker,
|
|
26
|
+
} from "../src/dashboard.ts";
|
|
27
|
+
|
|
28
|
+
const PLAN = `## Tasks
|
|
29
|
+
|
|
30
|
+
- Task-1: 解析器 — files: src/plan.ts; wave: 1
|
|
31
|
+
- Task-2: 工具 — files: src/task-tool.ts; wave: 1
|
|
32
|
+
- Task-3: 核心 — deps: Task-1, Task-2; files: src/exec.ts; wave: 2
|
|
33
|
+
- Task-3.1: 注入
|
|
34
|
+
- Task-3.2: 状态机
|
|
35
|
+
|
|
36
|
+
### Execution Waves
|
|
37
|
+
|
|
38
|
+
- wave 1: Task-1, Task-2 — 并行
|
|
39
|
+
- wave 2: Task-3 — 串行
|
|
40
|
+
|
|
41
|
+
## Verification Checks
|
|
42
|
+
|
|
43
|
+
- [ ] \`VC-001\` covers \`Task-1\`; pass condition: 解析单测
|
|
44
|
+
- [ ] \`VC-002\` covers \`Task-3\`; pass condition: 核心测试
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
function model(progress?: Record<string, { status: "complete" | "skipped" }>) {
|
|
48
|
+
const tasks = buildTaskView(parsePlanTasks(PLAN), progress);
|
|
49
|
+
const checklist = [
|
|
50
|
+
{ id: "VC-001", text: "`VC-001` covers `Task-1`; pass condition: x", done: progress?.["Task-1"]?.status === "complete" },
|
|
51
|
+
{ id: "VC-002", text: "`VC-002` covers `Task-3`; pass condition: y", done: false },
|
|
52
|
+
];
|
|
53
|
+
return deriveDashboardModel("demo-run", tasks, checklist, { startedAt: new Date().toISOString() });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("dashboard model", () => {
|
|
57
|
+
it("summary line carries task and VC progress", () => {
|
|
58
|
+
const line = formatDashboardSummaryLine(model({ "Task-1": { status: "complete" } }));
|
|
59
|
+
assert.match(line, /tasks 1\/5/);
|
|
60
|
+
assert.match(line, /VC 1\/2/);
|
|
61
|
+
assert.match(line, /wave 1/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("markers: ✓ complete, ~ skipped, ▸ current, · pending", () => {
|
|
65
|
+
const m = model({ "Task-1": { status: "complete" }, "Task-2": { status: "skipped" } });
|
|
66
|
+
const flat = m.tasks.flatMap(function walk(t: { id: string; children: unknown[] }) { return [t, ...t.children]; } as never) as never[];
|
|
67
|
+
const byId = new Map(flat.map((t: { id: string }) => [t.id, t]));
|
|
68
|
+
assert.equal(taskMarker(byId.get("Task-1")!, null), "✓");
|
|
69
|
+
assert.equal(taskMarker(byId.get("Task-2")!, null), "~");
|
|
70
|
+
// Task-3 is the first open task in wave order → current.
|
|
71
|
+
assert.equal(taskMarker(byId.get("Task-3")!, "Task-3"), "▸");
|
|
72
|
+
assert.equal(taskMarker(byId.get("Task-3.1")!, "Task-3"), "·");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("open task summary lists the current wave and later waves", () => {
|
|
76
|
+
const tasks = buildTaskView(parsePlanTasks(PLAN), { "Task-1": { status: "complete" } });
|
|
77
|
+
const summary = openTaskSummary(tasks);
|
|
78
|
+
assert.match(summary, /current wave 1: Task-2/);
|
|
79
|
+
assert.match(summary, /later waves: Task-3/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("compact widget rendering", () => {
|
|
84
|
+
it("renders the current task and files at wide width", () => {
|
|
85
|
+
const lines = renderDashboardLines(model(), 100);
|
|
86
|
+
assert.ok(lines.some((line) => line.includes("Task-1")));
|
|
87
|
+
assert.ok(lines.some((line) => line.includes("src/plan.ts")));
|
|
88
|
+
assert.ok(lines.some((line) => line.includes("Ctrl+Shift+T")));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("narrow width drops file lists and stays inside the width", () => {
|
|
92
|
+
const lines = renderDashboardLines(model(), 40);
|
|
93
|
+
assert.ok(!lines.some((line) => line.includes("src/plan.ts")));
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
assert.ok(visibleWidth(line) <= 40, `line too wide (${visibleWidth(line)}): ${line}`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("shows the pause state when paused", () => {
|
|
100
|
+
const m = model();
|
|
101
|
+
m.paused = true;
|
|
102
|
+
m.pausedReason = "no task-status change in 3 rounds";
|
|
103
|
+
const lines = renderDashboardLines(m, 80);
|
|
104
|
+
assert.ok(lines.some((line) => line.includes("⏸")));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("shows audit failures when present", () => {
|
|
108
|
+
const m = model();
|
|
109
|
+
m.auditRounds = 1;
|
|
110
|
+
m.auditFailed = ["VC-002"];
|
|
111
|
+
const lines = renderDashboardLines(m, 100);
|
|
112
|
+
assert.ok(lines.some((line) => line.includes("VC-002")));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("expanded tree rendering", () => {
|
|
117
|
+
it("renders the nested tree with markers, deps, and files at wide width", () => {
|
|
118
|
+
const m = model({ "Task-1": { status: "complete" }, "Task-2": { status: "complete" } });
|
|
119
|
+
const lines = renderDashboardTreeLines(m, 120);
|
|
120
|
+
assert.ok(lines.some((line) => /✓ Task-1/.test(line)));
|
|
121
|
+
assert.ok(lines.some((line) => line.trim().startsWith("· Task-3.1")));
|
|
122
|
+
assert.ok(lines.some((line) => line.includes("(deps: Task-1, Task-2)")));
|
|
123
|
+
assert.ok(lines.some((line) => line.includes("Verification checks:")));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("narrow width clips titles and drops file/dep decorations", () => {
|
|
127
|
+
const lines = renderDashboardTreeLines(model(), 60);
|
|
128
|
+
assert.ok(!lines.some((line) => line.includes("(deps:")));
|
|
129
|
+
assert.ok(!lines.some((line) => line.includes("[")));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("lists the audit state when a round has run", () => {
|
|
133
|
+
const m = model();
|
|
134
|
+
m.auditRounds = 2;
|
|
135
|
+
m.auditFailed = ["VC-001"];
|
|
136
|
+
const lines = renderDashboardTreeLines(m, 100);
|
|
137
|
+
assert.ok(lines.some((line) => line.includes("Execution review: round 2")));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/** Regression cover for the TUI crash: "Rendered line 157 exceeds terminal
|
|
142
|
+
* width (231 > 230)" (2026-09-30, v0.99.1) and the same +1 overflow at 113
|
|
143
|
+
* columns. Root cause: the box rows were built as
|
|
144
|
+
* `content.padEnd(width).slice(0, width) + border` — one column over on every
|
|
145
|
+
* row — and the width math counted UTF-16 units, so CJK titles overflowed by
|
|
146
|
+
* far more than one. */
|
|
147
|
+
describe("width invariant (TUI crash regression)", () => {
|
|
148
|
+
const WIDTHS = [1, 2, 3, 5, 8, 20, 40, 63, 64, 79, 80, 95, 96, 113, 114, 120, 200, 229, 230, 231, 300, 400];
|
|
149
|
+
|
|
150
|
+
/** Long, CJK-heavy plan: the content that triggered the crash. */
|
|
151
|
+
const STRESS_PLAN = `## Tasks
|
|
152
|
+
|
|
153
|
+
- Task-1: 移除 typebox 运行时重复声明并改为 peerDependencies 通配声明 — files: package.json, package-lock.json, src/exec.ts, tests/exec.test.ts, tests/impl-review.test.ts; wave: 1
|
|
154
|
+
- Task-2: 适配器单测覆盖 check 双分支、schema 透传与 compiled() 非空鸭子形状断言 — files: tests/compiled-schema.test.ts, src/code-graph/schema.ts; wave: 1
|
|
155
|
+
- Task-3: 全量验证与冒烟启动 — deps: Task-1, Task-2; files: scripts/validate.ts; wave: 2
|
|
156
|
+
- Task-3.1: 注入
|
|
157
|
+
|
|
158
|
+
### Execution Waves
|
|
159
|
+
|
|
160
|
+
- wave 1: Task-1, Task-2 — 并行
|
|
161
|
+
- wave 2: Task-3 — 串行
|
|
162
|
+
|
|
163
|
+
## Verification Checks
|
|
164
|
+
|
|
165
|
+
- [ ] \`VC-1\` covers \`Task-1\`; pass condition: package.json 无 typebox 残留
|
|
166
|
+
- [ ] \`VC-2\` covers \`Task-2\`; pass condition: 适配器单测全绿
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
function stressModel() {
|
|
170
|
+
const tasks = buildTaskView(parsePlanTasks(STRESS_PLAN), { "Task-1": { status: "complete" } });
|
|
171
|
+
const checklist = [
|
|
172
|
+
{ id: "VC-1", text: "`VC-1` covers `Task-1`; pass condition: package.json 无 `@sinclair/typebox` 残留", done: true },
|
|
173
|
+
{ id: "VC-2", text: "`VC-2` covers `Task-2`; pass condition: 适配器单测全绿且断言 compiled() 非空", done: false },
|
|
174
|
+
];
|
|
175
|
+
return deriveDashboardModel("20260930T051445Z-fix-binary-ref-download-with-a-very-long-topic-slug", tasks, checklist, {
|
|
176
|
+
auditRounds: 1,
|
|
177
|
+
auditFailed: ["VC-2"],
|
|
178
|
+
pausedReason: "连续 3 轮无任务状态变化自动暂停 — 完成审计已耗尽预算",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const states = (): [string, ReturnType<typeof stressModel>][] => {
|
|
183
|
+
const base = stressModel();
|
|
184
|
+
return [
|
|
185
|
+
["running", base],
|
|
186
|
+
["paused", { ...base, paused: true, pausedReason: base.pausedReason as string }],
|
|
187
|
+
["all-terminal", { ...base, tasks: base.tasks.map((t) => ({ ...t, status: "complete" as const })) }],
|
|
188
|
+
["audit-failed", { ...base, auditRounds: 3, auditFailed: ["VC-1", "VC-2"] }],
|
|
189
|
+
];
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
for (const [label, render] of [
|
|
193
|
+
["compact", renderDashboardLines],
|
|
194
|
+
["tree", renderDashboardTreeLines],
|
|
195
|
+
] as const) {
|
|
196
|
+
it(`${label} view never exceeds the terminal width at any width`, () => {
|
|
197
|
+
for (const [state, m] of states()) {
|
|
198
|
+
for (const width of WIDTHS) {
|
|
199
|
+
for (const line of render(m, width)) {
|
|
200
|
+
assert.ok(
|
|
201
|
+
visibleWidth(line) <= width,
|
|
202
|
+
`${label}/${state} @${width}: line is ${visibleWidth(line)} columns: ${line}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
it("compact box rows are exactly the terminal width (the +1 border overflow)", () => {
|
|
211
|
+
for (const [state, m] of states()) {
|
|
212
|
+
for (const width of [40, 64, 113, 230]) {
|
|
213
|
+
for (const line of renderDashboardLines(m, width)) {
|
|
214
|
+
assert.equal(visibleWidth(line), width, `${state} @${width}: ${line}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("keeps the progress bar and the right border at wide widths", () => {
|
|
221
|
+
const m = stressModel();
|
|
222
|
+
const [top, progress] = renderDashboardLines(m, 230);
|
|
223
|
+
assert.ok(top.startsWith("┌─ "), "top row opens with the box corner");
|
|
224
|
+
assert.ok(top.endsWith("┐"), "top row closes with the right border");
|
|
225
|
+
assert.ok(progress.includes("█"), "progress bar survives a long topic");
|
|
226
|
+
assert.ok(!top.includes("█"), "the bar moved to the progress row, not the title row");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("title row is π-plans: <plan>, brand accented and the rest muted", () => {
|
|
230
|
+
const m = stressModel();
|
|
231
|
+
const fg = (color: string, text: string) => `<${color}>${text}</${color}>`;
|
|
232
|
+
const [title] = renderDashboardLines(m, 100, { fg });
|
|
233
|
+
// Brand in the accent colour, plan name in muted — as sibling spans, so a
|
|
234
|
+
// closing reset cannot strip the colour off everything after it.
|
|
235
|
+
assert.ok(title.startsWith("<muted>┌─ </muted><accent>π-plans</accent>"), `brand span: ${title}`);
|
|
236
|
+
assert.ok(title.includes("<muted>: 20260930T051445Z-"), `plan name muted: ${title}`);
|
|
237
|
+
assert.ok(!/<muted>[^<]*<\/muted><accent>[^<]*<\/accent><muted>[^<]*π-plans/.test(title), "brand is not nested inside a muted span");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("progress row spells out the counts and carries elapsed time then the bar", () => {
|
|
241
|
+
const m = stressModel();
|
|
242
|
+
const [, progress] = renderDashboardLines(m, 100);
|
|
243
|
+
assert.match(progress, /│ Tasks \d+\/\d+ · Verification checks \d+\/\d+ /);
|
|
244
|
+
// Elapsed time precedes the bar, both on the progress row.
|
|
245
|
+
assert.match(progress, /\d{2}:\d{2}:\d{2} [█░]+/);
|
|
246
|
+
assert.ok(!/VC \d+\//.test(progress), "the VC abbreviation is gone");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("drops the bar, then elapsed time, when the row cannot fit them", () => {
|
|
250
|
+
const m = stressModel();
|
|
251
|
+
for (const width of [12, 20, 24, 30, 40, 64]) {
|
|
252
|
+
const [, progress] = renderDashboardLines(m, width);
|
|
253
|
+
assert.ok(visibleWidth(progress) <= width, `progress row fits at ${width}`);
|
|
254
|
+
}
|
|
255
|
+
// Where the full counts fit they always survive; the abbreviation the old
|
|
256
|
+
// layout used is never substituted back in.
|
|
257
|
+
assert.match(renderDashboardLines(m, 40)[1], /Tasks \d+\/\d+ · Verification checks \d+\/\d+/);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("local width helper agrees with the host on every glyph the dashboard emits", () => {
|
|
261
|
+
// The dashboard sizes rows with the local helper but the host measures
|
|
262
|
+
// them. A disagreement in the crash-causing direction (local < host)
|
|
263
|
+
// silently reintroduces the overflow, so pin the glyph set.
|
|
264
|
+
for (const glyph of ["┌", "┐", "└", "┘", "│", "─", "⏸", "▸", "·", "✗", "✓", "☑", "☐", "█", "░", "—", "…", "~"]) {
|
|
265
|
+
assert.equal(localVisibleWidth(glyph), visibleWidth(glyph), `glyph width mismatch for ${JSON.stringify(glyph)}`);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("rolled-back task rendering", () => {
|
|
271
|
+
const CHECKS = () => [
|
|
272
|
+
{ id: "VC-001", text: "`VC-001` covers `Task-1`; pass condition: x", done: false },
|
|
273
|
+
{ id: "VC-002", text: "`VC-002` covers `Task-3`; pass condition: y", done: false },
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
function withProgress(progress: Record<string, { status: "complete" | "skipped" | "pending"; evidence?: string }>) {
|
|
277
|
+
return deriveDashboardModel("demo-run", buildTaskView(parsePlanTasks(PLAN), progress), CHECKS(), {
|
|
278
|
+
startedAt: new Date().toISOString(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
it("marks a rolled-back task distinctly from untouched pending work", () => {
|
|
283
|
+
// A rollback keeps the task's evidence (Task-2.1), so "pending with
|
|
284
|
+
// evidence" is the observable signature of work that was reopened.
|
|
285
|
+
const tasks = withProgress({
|
|
286
|
+
"Task-1": { status: "pending" },
|
|
287
|
+
"Task-2": { status: "pending", evidence: "wired the tool" },
|
|
288
|
+
}).tasks;
|
|
289
|
+
const rolled = tasks.find((t) => t.id === "Task-2")!;
|
|
290
|
+
const untouched = tasks.find((t) => t.id === "Task-1")!;
|
|
291
|
+
assert.equal(taskMarker(rolled, null), "↺");
|
|
292
|
+
assert.equal(taskMarker(untouched, null), "·");
|
|
293
|
+
// The current-task indicator still wins over the rollback marker.
|
|
294
|
+
assert.equal(taskMarker(rolled, "Task-2"), "▸");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("shows the retained evidence on a rolled-back row in the tree view", () => {
|
|
298
|
+
const tree = renderDashboardTreeLines(
|
|
299
|
+
withProgress({
|
|
300
|
+
"Task-1": { status: "pending" },
|
|
301
|
+
"Task-2": { status: "pending", evidence: "wired the tool" },
|
|
302
|
+
}),
|
|
303
|
+
100,
|
|
304
|
+
).join("\n");
|
|
305
|
+
assert.match(tree, /↺ Task-2/, "the rolled-back row carries its own marker");
|
|
306
|
+
assert.match(tree, /wired the tool/, "the previous attempt's evidence is visible");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const terminal = (extra: { auditRounds?: number; auditFailed?: string[]; auditUndeterminable?: string[]; reviewRunning?: boolean }) => {
|
|
310
|
+
const base = withProgress({
|
|
311
|
+
"Task-1": { status: "complete", evidence: "done" },
|
|
312
|
+
"Task-2": { status: "complete", evidence: "done" },
|
|
313
|
+
"Task-3": { status: "complete", evidence: "done" },
|
|
314
|
+
"Task-3.1": { status: "complete", evidence: "done" },
|
|
315
|
+
"Task-3.2": { status: "complete", evidence: "done" },
|
|
316
|
+
});
|
|
317
|
+
return deriveDashboardModel("demo-run", base.tasks, base.checklist, { startedAt: base.startedAt, ...extra });
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
it("shows the undeterminable count instead of a completion tick", () => {
|
|
321
|
+
const owed = terminal({ auditRounds: 2, auditUndeterminable: ["VC-001", "VC-002"] });
|
|
322
|
+
const panel = renderDashboardLines(owed, 80);
|
|
323
|
+
assert.ok(panel.some((l) => /undeterminable/.test(l)), "panel reports undeterminable");
|
|
324
|
+
assert.ok(!panel.some((l) => /audit complete/.test(l)), "no false completion tick");
|
|
325
|
+
const tree = renderDashboardTreeLines(owed, 100).join("\n");
|
|
326
|
+
assert.match(tree, /undeterminable: VC-001, VC-002/);
|
|
327
|
+
assert.doesNotMatch(tree, /passed ✓/);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("shows the retained evidence for rolled-back work in the COMPACT view too", () => {
|
|
331
|
+
// VC-006 requires the evidence in both views. The first implementation
|
|
332
|
+
// only added it to the tree, which the execution review caught.
|
|
333
|
+
const m = withProgress({
|
|
334
|
+
"Task-1": { status: "pending", evidence: "rewrote src/plan.ts" },
|
|
335
|
+
"Task-2": { status: "complete", evidence: "wired the tool" },
|
|
336
|
+
});
|
|
337
|
+
const panel = renderDashboardLines(m, 90).join("\n");
|
|
338
|
+
assert.match(panel, /\u21ba Task-1/, "compact panel marks the rolled-back task");
|
|
339
|
+
assert.match(panel, /rewrote src\/plan\.ts/, "compact panel shows the retained evidence");
|
|
340
|
+
const tree = renderDashboardTreeLines(m, 100).join("\n");
|
|
341
|
+
assert.match(tree, /rewrote src\/plan\.ts/, "tree view still shows it");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("keeps every compact row inside the width budget with rollback rows present", () => {
|
|
345
|
+
const m = deriveDashboardModel(
|
|
346
|
+
"demo-run",
|
|
347
|
+
buildTaskView(parsePlanTasks(PLAN), {
|
|
348
|
+
"Task-1": { status: "pending", evidence: "a very long piece of evidence that must be clipped to fit the panel width" },
|
|
349
|
+
"Task-2": { status: "pending", evidence: "another long evidence string for the second rolled-back task" },
|
|
350
|
+
"Task-3": { status: "pending", evidence: "a third one that must be capped out of the panel" },
|
|
351
|
+
}),
|
|
352
|
+
CHECKS(),
|
|
353
|
+
{ startedAt: new Date().toISOString(), auditRounds: 1, auditFailed: ["VC-001"], auditUndeterminable: ["VC-002"] },
|
|
354
|
+
);
|
|
355
|
+
for (const width of [46, 64, 80, 120]) {
|
|
356
|
+
for (const row of renderDashboardLines(m, width)) {
|
|
357
|
+
assert.ok(visibleWidth(row) <= width, `width ${width}: ${JSON.stringify(row)}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const rows = renderDashboardLines(m, 90).filter((l) => /\u21ba /.test(l));
|
|
361
|
+
assert.equal(rows.length, 2, "rollback rows are capped so the panel height stays bounded");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("never shows the completion tick while a review round runs or checks are owed (v0.8)", () => {
|
|
365
|
+
// The v0.7 mis-cue: `audit complete ✓` rendered for the whole duration
|
|
366
|
+
// of a running round (auditRounds was pre-incremented, the model had no
|
|
367
|
+
// running field). Now the running/owed states render their round number.
|
|
368
|
+
const running = terminal({ auditRounds: 1, reviewRunning: true });
|
|
369
|
+
const runningPanel = renderDashboardLines(running, 80).join("\n");
|
|
370
|
+
assert.match(runningPanel, /review: round 2\/5 running/, "the running round is visible with its number");
|
|
371
|
+
assert.doesNotMatch(runningPanel, /audit complete/, "no tick while a round runs");
|
|
372
|
+
const runningTree = renderDashboardTreeLines(running, 100).join("\n");
|
|
373
|
+
assert.match(runningTree, /Execution review: round 2\/5 running/);
|
|
374
|
+
const owed = terminal({ auditRounds: 1 });
|
|
375
|
+
const owedPanel = renderDashboardLines(owed, 80).join("\n");
|
|
376
|
+
assert.doesNotMatch(owedPanel, /audit complete/, "no tick while checks are still owed");
|
|
377
|
+
assert.match(owedPanel, /review: round 2\/5 — verdict pending/, "the owed state shows the pending round");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("shows the completion tick only when every check is really done", () => {
|
|
381
|
+
const settled = terminal({ auditRounds: 1 });
|
|
382
|
+
const model = { ...settled, checklist: settled.checklist.map((item) => ({ ...item, done: true })) };
|
|
383
|
+
assert.ok(renderDashboardLines(model, 80).some((l) => /audit complete/.test(l)));
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("still reports real failures ahead of undeterminable ones", () => {
|
|
387
|
+
const mixed = terminal({ auditRounds: 2, auditFailed: ["VC-001"], auditUndeterminable: ["VC-002"] });
|
|
388
|
+
const tree = renderDashboardTreeLines(mixed, 100).join("\n");
|
|
389
|
+
assert.match(tree, /failed: VC-001/, "a real failure takes the tree audit line");
|
|
390
|
+
assert.doesNotMatch(tree, /undeterminable:/, "and suppresses the softer outcome there");
|
|
391
|
+
const panel = renderDashboardLines(mixed, 80);
|
|
392
|
+
assert.ok(panel.some((l) => /check\(s\) failed/.test(l)));
|
|
393
|
+
assert.ok(panel.some((l) => /\? VC-002/.test(l)), "the unreadable checks still get their own panel row");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("keeps the width invariant with the new rows", () => {
|
|
397
|
+
const rows = renderDashboardLines(terminal({ auditRounds: 2, auditFailed: ["VC-001"], auditUndeterminable: ["VC-002"] }), 46);
|
|
398
|
+
for (const row of rows) {
|
|
399
|
+
assert.ok(visibleWidth(row) <= 46, `row too wide: ${JSON.stringify(row)}`);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
});
|