pi-harness-runtime 1.1.37 → 1.1.39
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/harness/langchain/daemon.ts +91 -15
- package/harness/langchain/graph.ts +3 -0
- package/harness/langchain/widget.test.ts +244 -0
- package/harness/langchain/widget.ts +463 -0
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -27,12 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { randomUUID } from "node:crypto";
|
|
30
|
-
import {
|
|
31
|
-
existsSync,
|
|
32
|
-
readdirSync,
|
|
33
|
-
readFileSync,
|
|
34
|
-
writeFileSync,
|
|
35
|
-
} from "node:fs";
|
|
30
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
36
31
|
import { join } from "node:path";
|
|
37
32
|
|
|
38
33
|
import { LeaseManager } from "../../packages/autonomous-runtime/src/lease.js";
|
|
@@ -53,6 +48,7 @@ import {
|
|
|
53
48
|
import { NotificationCenter } from "../../packages/notification/dist/notification-center.js";
|
|
54
49
|
|
|
55
50
|
import { invokeWithSurgeRetry, type SurgePolicy } from "./surge.js";
|
|
51
|
+
import type { LoopWidget } from "./widget.js";
|
|
56
52
|
import { createLoopCheckpointer } from "./checkpointer.js";
|
|
57
53
|
|
|
58
54
|
import {
|
|
@@ -116,6 +112,12 @@ export interface DaemonConfig {
|
|
|
116
112
|
* Default: none (no tunnel management)
|
|
117
113
|
*/
|
|
118
114
|
tunnelCommand?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Optional widget for TUI / status-line display.
|
|
117
|
+
* When set, the daemon updates the widget at each loop transition
|
|
118
|
+
* (mirrors pi-lens footer style: !NW ●ME).
|
|
119
|
+
*/
|
|
120
|
+
widget?: LoopWidget;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
// ─── TriggeredTask ──────────────────────────────────────────────────────────
|
|
@@ -535,11 +537,14 @@ class TunnelMonitor {
|
|
|
535
537
|
private consecutiveFailures = 0;
|
|
536
538
|
private readonly maxConsecutiveFailures = 2;
|
|
537
539
|
|
|
538
|
-
constructor(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
540
|
+
constructor(
|
|
541
|
+
scriptPath: string,
|
|
542
|
+
opts: {
|
|
543
|
+
checkIntervalMs?: number;
|
|
544
|
+
targetHost?: string;
|
|
545
|
+
targetPort?: number;
|
|
546
|
+
} = {},
|
|
547
|
+
) {
|
|
543
548
|
this.scriptPath = scriptPath;
|
|
544
549
|
this.checkIntervalMs = opts.checkIntervalMs ?? 30_000; // 30s default
|
|
545
550
|
this.targetHost = opts.targetHost ?? "127.0.0.1";
|
|
@@ -635,7 +640,9 @@ class TunnelMonitor {
|
|
|
635
640
|
`bash "${this.scriptPath}" start 2>&1`,
|
|
636
641
|
{ timeout: 30_000 },
|
|
637
642
|
);
|
|
638
|
-
console.log(
|
|
643
|
+
console.log(
|
|
644
|
+
`[tunnel-monitor] Restart output: ${stdout.trim() || stderr.trim() || "ok"}`,
|
|
645
|
+
);
|
|
639
646
|
} catch (err) {
|
|
640
647
|
console.error(
|
|
641
648
|
`[tunnel-monitor] Restart failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -668,6 +675,7 @@ export class LoopDaemon {
|
|
|
668
675
|
private readonly busWatcher: BusWatcher;
|
|
669
676
|
private readonly bdWatcher: BdTasksWatcher;
|
|
670
677
|
private readonly tunnelMonitor: TunnelMonitor | null;
|
|
678
|
+
private readonly widget: LoopWidget | null;
|
|
671
679
|
|
|
672
680
|
/** Currently running tasks (taskId → RunningTask) */
|
|
673
681
|
private readonly running = new Map<string, RunningTask>();
|
|
@@ -691,6 +699,7 @@ export class LoopDaemon {
|
|
|
691
699
|
workspace: config.workspace ?? getHerdrWorkspace(),
|
|
692
700
|
notificationConfig: config.notificationConfig ?? undefined,
|
|
693
701
|
tunnelCommand: config.tunnelCommand,
|
|
702
|
+
widget: config.widget,
|
|
694
703
|
} as Required<DaemonConfig>;
|
|
695
704
|
|
|
696
705
|
this.agentId = this.config.agentId;
|
|
@@ -718,6 +727,7 @@ export class LoopDaemon {
|
|
|
718
727
|
}
|
|
719
728
|
|
|
720
729
|
this.inboxWatcher = new InboxWatcher(this.inboxDir, this.config.pollMs);
|
|
730
|
+
this.widget = this.config.widget ?? null;
|
|
721
731
|
this.busWatcher = new BusWatcher(this.bus, this.config.pollMs);
|
|
722
732
|
this.bdWatcher = new BdTasksWatcher(this.config.pollMs);
|
|
723
733
|
|
|
@@ -879,11 +889,12 @@ export class LoopDaemon {
|
|
|
879
889
|
maxIterations: this.config.maxIterations,
|
|
880
890
|
}));
|
|
881
891
|
|
|
882
|
-
// Inject transition publishing into onStep
|
|
892
|
+
// Inject transition publishing into onStep (also drives the widget)
|
|
883
893
|
const originalOnStep = deps.onStep;
|
|
884
894
|
deps.onStep = (step, state) => {
|
|
885
895
|
originalOnStep?.(step, state);
|
|
886
896
|
this._publishTransition(loopId, step, state);
|
|
897
|
+
this._updateWidget(step, state);
|
|
887
898
|
};
|
|
888
899
|
|
|
889
900
|
// ── Run the graph (surge-aware: 529/overloaded pauses auto-resume) ──
|
|
@@ -892,11 +903,14 @@ export class LoopDaemon {
|
|
|
892
903
|
);
|
|
893
904
|
const checkpointer = (() => {
|
|
894
905
|
if (this.config.checkpointer === false) return false;
|
|
895
|
-
if (
|
|
906
|
+
if (
|
|
907
|
+
this.config.checkpointer === true ||
|
|
908
|
+
typeof this.config.checkpointer === "string"
|
|
909
|
+
) {
|
|
896
910
|
return createLoopCheckpointer(
|
|
897
911
|
this.config.checkpointer === true
|
|
898
912
|
? this.bus.getWorkspace()
|
|
899
|
-
|
|
913
|
+
: (this.config.checkpointer as string),
|
|
900
914
|
);
|
|
901
915
|
}
|
|
902
916
|
return undefined; // default MemorySaver
|
|
@@ -949,6 +963,17 @@ export class LoopDaemon {
|
|
|
949
963
|
|
|
950
964
|
log(`Loop finished: ${verdict} after ${iterations} iteration(s)`);
|
|
951
965
|
|
|
966
|
+
// Update widget on loop completion
|
|
967
|
+
if (this.widget) {
|
|
968
|
+
this.widget.setComplete(
|
|
969
|
+
verdict === "approved"
|
|
970
|
+
? "approved"
|
|
971
|
+
: verdict === "blocked"
|
|
972
|
+
? "rejected"
|
|
973
|
+
: "max_iterations",
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
952
977
|
// ── Gate check after blocked verdict ────────────────────────────────
|
|
953
978
|
if (
|
|
954
979
|
verdict === "blocked" &&
|
|
@@ -1031,6 +1056,57 @@ export class LoopDaemon {
|
|
|
1031
1056
|
}
|
|
1032
1057
|
}
|
|
1033
1058
|
|
|
1059
|
+
/**
|
|
1060
|
+
* Drive the TUI widget through loop transitions.
|
|
1061
|
+
* Called from onStep within _runTask.
|
|
1062
|
+
*/
|
|
1063
|
+
private _updateWidget(step: string, state: LoopState): void {
|
|
1064
|
+
const w = this.widget;
|
|
1065
|
+
if (!w) return;
|
|
1066
|
+
|
|
1067
|
+
switch (step) {
|
|
1068
|
+
case "plan": {
|
|
1069
|
+
w.setPhase("planning", "GPT planning");
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
case "write": {
|
|
1073
|
+
w.setPhase("writing", "MiniMax coding");
|
|
1074
|
+
// After a write step, bump the iteration counter
|
|
1075
|
+
w.startIteration(state.iteration, this.config.maxIterations);
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
case "review": {
|
|
1079
|
+
w.setPhase("reviewing", "GPT reviewing");
|
|
1080
|
+
// Process review comments — one record per file
|
|
1081
|
+
const comments = state.review?.comments ?? [];
|
|
1082
|
+
if (comments.length === 0) break;
|
|
1083
|
+
|
|
1084
|
+
// Group blocking comments by file (severity: "critical" = blocking)
|
|
1085
|
+
const byFile = new Map<string, number>();
|
|
1086
|
+
for (const c of comments) {
|
|
1087
|
+
if (c.severity === "critical") {
|
|
1088
|
+
const prev = byFile.get(c.file ?? "") ?? 0;
|
|
1089
|
+
byFile.set(c.file ?? "", prev + 1);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
for (const [file, count] of byFile) {
|
|
1093
|
+
if (count > 0) {
|
|
1094
|
+
w.recordBlockers(file, count);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
// Files with no blocking comments pass
|
|
1098
|
+
const blockingFiles = new Set(byFile.keys());
|
|
1099
|
+
for (const c of comments) {
|
|
1100
|
+
const f = c.file ?? "";
|
|
1101
|
+
if (!blockingFiles.has(f) && f) {
|
|
1102
|
+
w.recordReviewPass(f);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1034
1110
|
private _reviewReportPath(loopId: string, iteration: number): string {
|
|
1035
1111
|
return `${ensureHerdrWorkspace().reviews}/${loopId}-review-${iteration}.md`;
|
|
1036
1112
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type ReviewVerdict,
|
|
25
25
|
ReviewVerdictSchema,
|
|
26
26
|
} from "./agents.js";
|
|
27
|
+
import type { LoopWidget } from "./widget.js";
|
|
27
28
|
|
|
28
29
|
// ─── State ──────────────────────────────────────────────────────────────────
|
|
29
30
|
|
|
@@ -58,6 +59,8 @@ export interface LoopDeps {
|
|
|
58
59
|
review: (plan: string, code: string) => Promise<ReviewVerdict>;
|
|
59
60
|
maxIterations: number;
|
|
60
61
|
onStep?: (step: string, state: LoopState) => void;
|
|
62
|
+
/** Optional widget for TUI / status-line display (mirrors pi-lens footer style). */
|
|
63
|
+
widget?: LoopWidget;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
// ─── Node implementations ───────────────────────────────────────────────────
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoopWidget tests — T14
|
|
3
|
+
*
|
|
4
|
+
* Run:
|
|
5
|
+
* bun test harness/langchain/widget.test.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, beforeEach } from "node:test";
|
|
9
|
+
import assert from "node:assert";
|
|
10
|
+
import { LoopWidget } from "./widget.js";
|
|
11
|
+
|
|
12
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const stubTheme = {
|
|
15
|
+
fg: (color: string, s: string): string => {
|
|
16
|
+
const codes: Record<string, string> = {
|
|
17
|
+
dim: `\x1b[2m${s}\x1b[0m`,
|
|
18
|
+
error: `\x1b[31m${s}\x1b[0m`,
|
|
19
|
+
warning: `\x1b[33m${s}\x1b[0m`,
|
|
20
|
+
success: `\x1b[32m${s}\x1b[0m`,
|
|
21
|
+
accent: `\x1b[36m${s}\x1b[0m`,
|
|
22
|
+
};
|
|
23
|
+
return codes[color] ?? s;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("T14 — LoopWidget", () => {
|
|
28
|
+
|
|
29
|
+
let widget: LoopWidget;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
widget = new LoopWidget();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ─── Idle state ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
it("starts in idle phase", () => {
|
|
38
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
39
|
+
assert.match(lines[0], /idle/);
|
|
40
|
+
assert.match(lines[0], /harness/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ─── Phase transitions ──────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
it("setPhase changes the phase label", () => {
|
|
46
|
+
widget.setPhase("writing");
|
|
47
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
48
|
+
assert.match(lines[0], /coding/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("setPhase with custom label uses the label", () => {
|
|
52
|
+
widget.setPhase("planning", "GPT planning");
|
|
53
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
54
|
+
assert.match(lines[0], /GPT planning/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("surge-pause shows detail line", () => {
|
|
58
|
+
widget.setSurgePause(new Date(Date.now() + 30 * 60_000)); // 30 min
|
|
59
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
60
|
+
// Label is "surge (in 30m)"
|
|
61
|
+
assert.ok(lines.some(l => l.includes("surge") && l.includes("in 30m")));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("setError shows error detail", () => {
|
|
65
|
+
widget.setError("model timeout");
|
|
66
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
67
|
+
assert.ok(lines.some(l => l.includes("error")));
|
|
68
|
+
assert.ok(lines.some(l => l.includes("model timeout")));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("setComplete approved shows ✓ loop complete", () => {
|
|
72
|
+
widget.setComplete("approved");
|
|
73
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
74
|
+
assert.match(lines[0], /loop complete/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("setComplete rejected shows ✗ loop rejected", () => {
|
|
78
|
+
widget.setComplete("rejected");
|
|
79
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
80
|
+
assert.match(lines[0], /loop rejected/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("setComplete max_iterations shows iteration count", () => {
|
|
84
|
+
widget.setComplete("max_iterations");
|
|
85
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
86
|
+
assert.match(lines[0], /max iterations/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ─── Iteration tracking ────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
it("startIteration sets iteration and max", () => {
|
|
92
|
+
widget.startIteration(3, 5);
|
|
93
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
94
|
+
assert.match(lines[0], /iter 3\/5/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("startIteration resets per-file counters but keeps file records", () => {
|
|
98
|
+
widget.recordWrite("src/a.ts");
|
|
99
|
+
widget.recordBlockers("src/a.ts", 2);
|
|
100
|
+
widget.startIteration(2, 5);
|
|
101
|
+
// Files accumulate across iterations (same codebase); only counters reset.
|
|
102
|
+
// After reset, a.ts shows with 0 write steps and 0 blockers.
|
|
103
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
104
|
+
const fileLines = lines.filter(l => l.includes("a.ts"));
|
|
105
|
+
assert.ok(fileLines.length > 0, "file should still appear");
|
|
106
|
+
// No blocker count in the file row (reset to 0)
|
|
107
|
+
assert.ok(!fileLines.some(l => l.includes("!2")), "blocker count should be reset");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ─── File record tracking ──────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
it("recordWrite shows file in widget", () => {
|
|
113
|
+
widget.recordWrite("src/foo.ts");
|
|
114
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
115
|
+
assert.ok(lines.some(l => l.includes("foo.ts")));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("recordWrite accumulates across calls", () => {
|
|
119
|
+
widget.recordWrite("src/bar.ts");
|
|
120
|
+
widget.recordWrite("src/bar.ts");
|
|
121
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
122
|
+
assert.ok(lines.some(l => l.includes("bar.ts") && l.includes("✍2")));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("recordBlockers shows blocker count", () => {
|
|
126
|
+
widget.recordBlockers("src/bad.ts", 2);
|
|
127
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
128
|
+
assert.ok(lines.some(l => l.includes("bad.ts") && l.includes("!2")));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("recordReviewPass clears blockers and increments passes", () => {
|
|
132
|
+
widget.recordBlockers("src/ok.ts", 1);
|
|
133
|
+
widget.recordReviewPass("src/ok.ts");
|
|
134
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
135
|
+
assert.ok(lines.some(l => l.includes("ok.ts") && l.includes("🔍1")));
|
|
136
|
+
// No blocker count
|
|
137
|
+
assert.ok(!lines.some(l => l.includes("ok.ts") && l.includes("!") && !l.includes("🔍")));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ─── Summary counts ────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
it("no errors/warnings shows ✓", () => {
|
|
143
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
144
|
+
assert.ok(lines[0].includes("✓") || lines[0].includes("harness"));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("errors shown as ●NE", () => {
|
|
148
|
+
widget.recordBlockers("src/err.ts", 3);
|
|
149
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
150
|
+
assert.ok(lines.some(l => l.includes("●3E")));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ─── Surge pause ──────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
it("setSurgePause shows reset time in minutes/hours", () => {
|
|
156
|
+
const resetAt = new Date(Date.now() + 90 * 60_000); // 90 minutes → "in 1h 30m"
|
|
157
|
+
widget.setSurgePause(resetAt);
|
|
158
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
159
|
+
const surgeLine = lines.find(l => l.includes("surge"));
|
|
160
|
+
assert.ok(surgeLine !== undefined);
|
|
161
|
+
assert.match(surgeLine, /\(in \d+h/); // "in 1h 30m" matches
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("setSurgePause shows 'now' for past dates", () => {
|
|
165
|
+
const resetAt = new Date(Date.now() - 1000);
|
|
166
|
+
widget.setSurgePause(resetAt);
|
|
167
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
168
|
+
// Label format: "surge (now)"
|
|
169
|
+
assert.ok(lines.some(l => l.includes("surge") && l.includes("(now)")));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ─── Console fallback ─────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
it("toConsoleString returns human-readable output", () => {
|
|
175
|
+
widget.startIteration(2, 5);
|
|
176
|
+
widget.setPhase("writing", "coding");
|
|
177
|
+
widget.recordWrite("src/test.ts");
|
|
178
|
+
const output = widget.toConsoleString();
|
|
179
|
+
assert.match(output, /harness/);
|
|
180
|
+
assert.match(output, /iter 2\/5/);
|
|
181
|
+
assert.match(output, /coding/);
|
|
182
|
+
assert.match(output, /test\.ts/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ─── Width / truncation ───────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
it("truncates long file names with …", () => {
|
|
188
|
+
widget.recordWrite("/very/long/path/to/a/very/long/file/name/that/exceeds/width.ts");
|
|
189
|
+
const lines = widget.renderWidget(40, stubTheme);
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
192
|
+
assert.ok(stripped.length <= 41, `line too long: ${stripped}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ─── reset() ─────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
it("reset() clears all state", () => {
|
|
199
|
+
widget.startIteration(2, 5);
|
|
200
|
+
widget.setPhase("writing");
|
|
201
|
+
widget.recordWrite("src/a.ts");
|
|
202
|
+
widget.reset();
|
|
203
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
204
|
+
assert.match(lines[0], /idle/);
|
|
205
|
+
assert.ok(!lines.some(l => l.includes("a.ts")));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ─── makeRenderer ─────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
it("makeRenderer returns factory and invalidate", () => {
|
|
211
|
+
const { factory, invalidate } = widget.makeRenderer();
|
|
212
|
+
assert.equal(typeof factory, "function");
|
|
213
|
+
assert.equal(typeof invalidate, "function");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("factory returns render + invalidate + dispose", () => {
|
|
217
|
+
const { factory } = widget.makeRenderer();
|
|
218
|
+
const comp = factory({} as any, stubTheme as any);
|
|
219
|
+
assert.equal(typeof comp.render, "function");
|
|
220
|
+
assert.equal(typeof comp.invalidate, "function");
|
|
221
|
+
assert.equal(typeof comp.dispose, "function");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("factory render() returns widget lines", () => {
|
|
225
|
+
widget.setPhase("planning");
|
|
226
|
+
const { factory } = widget.makeRenderer();
|
|
227
|
+
const comp = factory({} as any, stubTheme as any);
|
|
228
|
+
const lines = comp.render(80);
|
|
229
|
+
assert.ok(Array.isArray(lines));
|
|
230
|
+
assert.ok(lines.length > 0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ─── getLoopWidget singleton ────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
it("getLoopWidget returns the same instance", () => {
|
|
236
|
+
// Dynamic import to avoid module caching issues
|
|
237
|
+
import("./widget.js").then(m => {
|
|
238
|
+
const a = m.getLoopWidget();
|
|
239
|
+
const b = m.getLoopWidget();
|
|
240
|
+
assert.strictEqual(a, b);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
});
|