@hasna/terminal 0.7.1 → 0.7.3
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/dist/cli.js +40 -2
- package/dist/parsers/git.js +13 -0
- package/dist/test-watchlist.js +127 -0
- package/package.json +1 -1
- package/src/cli.tsx +35 -2
- package/src/parsers/git.ts +15 -0
- package/src/test-watchlist.ts +156 -0
package/dist/cli.js
CHANGED
|
@@ -82,6 +82,7 @@ if (args[0] === "exec") {
|
|
|
82
82
|
const { shouldBeLazy, toLazy, getSlice } = await import("./lazy-executor.js");
|
|
83
83
|
const { parseOutput, estimateTokens } = await import("./parsers/index.js");
|
|
84
84
|
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
85
|
+
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
85
86
|
// Rewrite command if possible
|
|
86
87
|
const rw = rewriteCommand(command);
|
|
87
88
|
const actualCmd = rw.changed ? rw.rewritten : command;
|
|
@@ -123,6 +124,23 @@ if (args[0] === "exec") {
|
|
|
123
124
|
console.error(`[open-terminal] showing ${slice.lines.length}/${slice.total}, ${slice.total - (offset ?? 0) - slice.lines.length} remaining`);
|
|
124
125
|
process.exit(0);
|
|
125
126
|
}
|
|
127
|
+
// Test output detection — use watchlist for structured test tracking
|
|
128
|
+
if (isTestOutput(clean)) {
|
|
129
|
+
const result = trackTests(process.cwd(), clean);
|
|
130
|
+
const formatted = formatWatchResult(result);
|
|
131
|
+
const savedTokens = rawTokens - estimateTokens(formatted);
|
|
132
|
+
if (savedTokens > 20)
|
|
133
|
+
recordSaving("structured", savedTokens);
|
|
134
|
+
if (jsonMode) {
|
|
135
|
+
console.log(JSON.stringify({ exitCode: 0, type: "test-results", ...result, duration: Date.now() - start }));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(formatted);
|
|
139
|
+
}
|
|
140
|
+
if (savedTokens > 10)
|
|
141
|
+
console.error(`[open-terminal] test watchlist: saved ${savedTokens} tokens`);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
126
144
|
// Lazy mode for huge output (threshold 200, skip cat/summary commands)
|
|
127
145
|
if (shouldBeLazy(clean, actualCmd)) {
|
|
128
146
|
const lazy = toLazy(clean, actualCmd);
|
|
@@ -151,10 +169,30 @@ if (args[0] === "exec") {
|
|
|
151
169
|
}
|
|
152
170
|
}
|
|
153
171
|
catch (e) {
|
|
154
|
-
// Command failed —
|
|
172
|
+
// Command failed — parse error output for structured diagnosis
|
|
155
173
|
const stderr = e.stderr?.toString() ?? "";
|
|
156
174
|
const stdout = e.stdout?.toString() ?? "";
|
|
157
|
-
|
|
175
|
+
const errorOutput = stripNoise(stripAnsi(stdout + stderr)).cleaned;
|
|
176
|
+
// Try structured error parsing
|
|
177
|
+
const { errorParser } = await import("./parsers/errors.js");
|
|
178
|
+
if (errorOutput.length > 200 && errorParser.detect(actualCmd, errorOutput)) {
|
|
179
|
+
const info = errorParser.parse(actualCmd, errorOutput);
|
|
180
|
+
if (jsonMode) {
|
|
181
|
+
console.log(JSON.stringify({ exitCode: e.status ?? 1, error: info }));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(`Error: ${info.type}`);
|
|
185
|
+
console.log(` ${info.message}`);
|
|
186
|
+
if (info.file)
|
|
187
|
+
console.log(` File: ${info.file}${info.line ? `:${info.line}` : ""}`);
|
|
188
|
+
if (info.suggestion)
|
|
189
|
+
console.log(` Fix: ${info.suggestion}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Short error or no parser match — pass through cleaned
|
|
194
|
+
console.log(errorOutput);
|
|
195
|
+
}
|
|
158
196
|
process.exit(e.status ?? 1);
|
|
159
197
|
}
|
|
160
198
|
process.exit(0);
|
package/dist/parsers/git.js
CHANGED
|
@@ -7,6 +7,19 @@ export const gitLogParser = {
|
|
|
7
7
|
parse(_command, output) {
|
|
8
8
|
const entries = [];
|
|
9
9
|
const lines = output.split("\n");
|
|
10
|
+
// Detect oneline format: "abc1234 commit message"
|
|
11
|
+
const firstLine = lines[0]?.trim() ?? "";
|
|
12
|
+
const isOneline = /^[a-f0-9]{7,12}\s+/.test(firstLine) && !firstLine.startsWith("commit ");
|
|
13
|
+
if (isOneline) {
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
const match = line.trim().match(/^([a-f0-9]{7,12})\s+(.+)$/);
|
|
16
|
+
if (match) {
|
|
17
|
+
entries.push({ hash: match[1], author: "", date: "", message: match[2] });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return entries;
|
|
21
|
+
}
|
|
22
|
+
// Verbose format
|
|
10
23
|
let hash = "", author = "", date = "", message = [];
|
|
11
24
|
for (const line of lines) {
|
|
12
25
|
const commitMatch = line.match(/^commit\s+([a-f0-9]+)/);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Test focus tracker — tracks test status across runs, only reports changes
|
|
2
|
+
// Instead of showing "248 passed, 2 failed" every time, shows:
|
|
3
|
+
// "auth.login: FIXED, auth.logout: STILL FAILING, 246 unchanged"
|
|
4
|
+
// Per-cwd watchlist
|
|
5
|
+
const watchlists = new Map();
|
|
6
|
+
/** Extract test names and status from test runner output (any runner) */
|
|
7
|
+
function extractTests(output) {
|
|
8
|
+
const tests = [];
|
|
9
|
+
const lines = output.split("\n");
|
|
10
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11
|
+
const line = lines[i];
|
|
12
|
+
// PASS/FAIL with test name: "PASS src/auth.test.ts" or "✓ login works" or "✗ logout fails"
|
|
13
|
+
const passMatch = line.match(/(?:PASS|✓|✔|✅)\s+(.+)/);
|
|
14
|
+
if (passMatch) {
|
|
15
|
+
tests.push({ name: passMatch[1].trim(), status: "pass" });
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const failMatch = line.match(/(?:FAIL|✗|✕|❌|×)\s+(.+)/);
|
|
19
|
+
if (failMatch) {
|
|
20
|
+
// Capture error from next few lines
|
|
21
|
+
const errorLines = [];
|
|
22
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
23
|
+
if (lines[j].match(/(?:PASS|FAIL|✓|✗|✔|✕|Tests:|^\s*$)/))
|
|
24
|
+
break;
|
|
25
|
+
errorLines.push(lines[j].trim());
|
|
26
|
+
}
|
|
27
|
+
tests.push({ name: failMatch[1].trim(), status: "fail", error: errorLines.join(" ").slice(0, 200) });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// Jest/vitest style: " ● test name" for failures
|
|
31
|
+
const jestFail = line.match(/^\s*●\s+(.+)/);
|
|
32
|
+
if (jestFail) {
|
|
33
|
+
tests.push({ name: jestFail[1].trim(), status: "fail" });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return tests;
|
|
38
|
+
}
|
|
39
|
+
/** Detect if output looks like test runner output */
|
|
40
|
+
export function isTestOutput(output) {
|
|
41
|
+
const markers = /(?:Tests?:|PASS|FAIL|✓|✗|passed|failed|\d+\s+pass|\d+\s+fail)/i;
|
|
42
|
+
const lines = output.split("\n");
|
|
43
|
+
return lines.filter(l => markers.test(l)).length >= 2;
|
|
44
|
+
}
|
|
45
|
+
/** Track test results and return only changes */
|
|
46
|
+
export function trackTests(cwd, output) {
|
|
47
|
+
const current = extractTests(output);
|
|
48
|
+
const prev = watchlists.get(cwd);
|
|
49
|
+
// Count totals from raw output (more reliable than extracted tests)
|
|
50
|
+
let totalPassed = 0, totalFailed = 0;
|
|
51
|
+
const summaryMatch = output.match(/(\d+)\s+pass/i);
|
|
52
|
+
const failMatch = output.match(/(\d+)\s+fail/i);
|
|
53
|
+
if (summaryMatch)
|
|
54
|
+
totalPassed = parseInt(summaryMatch[1]);
|
|
55
|
+
if (failMatch)
|
|
56
|
+
totalFailed = parseInt(failMatch[1]);
|
|
57
|
+
// Fallback to extracted counts
|
|
58
|
+
if (totalPassed === 0)
|
|
59
|
+
totalPassed = current.filter(t => t.status === "pass").length;
|
|
60
|
+
if (totalFailed === 0)
|
|
61
|
+
totalFailed = current.filter(t => t.status === "fail").length;
|
|
62
|
+
// Store current for next comparison
|
|
63
|
+
const currentMap = new Map();
|
|
64
|
+
for (const t of current)
|
|
65
|
+
currentMap.set(t.name, t);
|
|
66
|
+
watchlists.set(cwd, currentMap);
|
|
67
|
+
// First run — no comparison possible
|
|
68
|
+
if (!prev) {
|
|
69
|
+
return {
|
|
70
|
+
changed: [],
|
|
71
|
+
newTests: current.filter(t => t.status === "fail"), // only show failures on first run
|
|
72
|
+
totalPassed,
|
|
73
|
+
totalFailed,
|
|
74
|
+
unchangedCount: 0,
|
|
75
|
+
firstRun: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Compare with previous
|
|
79
|
+
const changed = [];
|
|
80
|
+
const newTests = [];
|
|
81
|
+
let unchangedCount = 0;
|
|
82
|
+
for (const [name, test] of currentMap) {
|
|
83
|
+
const prevTest = prev.get(name);
|
|
84
|
+
if (!prevTest) {
|
|
85
|
+
newTests.push(test);
|
|
86
|
+
}
|
|
87
|
+
else if (prevTest.status !== test.status) {
|
|
88
|
+
changed.push({ name, from: prevTest.status, to: test.status, error: test.error });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
unchangedCount++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { changed, newTests, totalPassed, totalFailed, unchangedCount, firstRun: false };
|
|
95
|
+
}
|
|
96
|
+
/** Format watchlist result for display */
|
|
97
|
+
export function formatWatchResult(result) {
|
|
98
|
+
const lines = [];
|
|
99
|
+
if (result.firstRun) {
|
|
100
|
+
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed`);
|
|
101
|
+
if (result.newTests.length > 0) {
|
|
102
|
+
for (const t of result.newTests) {
|
|
103
|
+
lines.push(` ✗ ${t.name}${t.error ? `: ${t.error}` : ""}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
// Status changes
|
|
109
|
+
for (const c of result.changed) {
|
|
110
|
+
if (c.to === "pass")
|
|
111
|
+
lines.push(` ✓ FIXED: ${c.name}`);
|
|
112
|
+
else
|
|
113
|
+
lines.push(` ✗ BROKE: ${c.name}${c.error ? ` — ${c.error}` : ""}`);
|
|
114
|
+
}
|
|
115
|
+
// New failures
|
|
116
|
+
for (const t of result.newTests.filter(t => t.status === "fail")) {
|
|
117
|
+
lines.push(` ✗ NEW FAIL: ${t.name}${t.error ? ` — ${t.error}` : ""}`);
|
|
118
|
+
}
|
|
119
|
+
// Summary
|
|
120
|
+
if (result.changed.length === 0 && result.newTests.filter(t => t.status === "fail").length === 0) {
|
|
121
|
+
lines.push(`✓ ${result.totalPassed} passed, ${result.totalFailed} failed (no changes)`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed, ${result.unchangedCount} unchanged`);
|
|
125
|
+
}
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
package/package.json
CHANGED
package/src/cli.tsx
CHANGED
|
@@ -86,6 +86,7 @@ if (args[0] === "exec") {
|
|
|
86
86
|
const { shouldBeLazy, toLazy, getSlice } = await import("./lazy-executor.js");
|
|
87
87
|
const { parseOutput, estimateTokens } = await import("./parsers/index.js");
|
|
88
88
|
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
89
|
+
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
89
90
|
|
|
90
91
|
// Rewrite command if possible
|
|
91
92
|
const rw = rewriteCommand(command);
|
|
@@ -127,6 +128,21 @@ if (args[0] === "exec") {
|
|
|
127
128
|
process.exit(0);
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
// Test output detection — use watchlist for structured test tracking
|
|
132
|
+
if (isTestOutput(clean)) {
|
|
133
|
+
const result = trackTests(process.cwd(), clean);
|
|
134
|
+
const formatted = formatWatchResult(result);
|
|
135
|
+
const savedTokens = rawTokens - estimateTokens(formatted);
|
|
136
|
+
if (savedTokens > 20) recordSaving("structured", savedTokens);
|
|
137
|
+
if (jsonMode) {
|
|
138
|
+
console.log(JSON.stringify({ exitCode: 0, type: "test-results", ...result, duration: Date.now() - start }));
|
|
139
|
+
} else {
|
|
140
|
+
console.log(formatted);
|
|
141
|
+
}
|
|
142
|
+
if (savedTokens > 10) console.error(`[open-terminal] test watchlist: saved ${savedTokens} tokens`);
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
130
146
|
// Lazy mode for huge output (threshold 200, skip cat/summary commands)
|
|
131
147
|
if (shouldBeLazy(clean, actualCmd)) {
|
|
132
148
|
const lazy = toLazy(clean, actualCmd);
|
|
@@ -155,10 +171,27 @@ if (args[0] === "exec") {
|
|
|
155
171
|
console.error(`[open-terminal] saved ${savedTokens} tokens (noise filter)`);
|
|
156
172
|
}
|
|
157
173
|
} catch (e: any) {
|
|
158
|
-
// Command failed —
|
|
174
|
+
// Command failed — parse error output for structured diagnosis
|
|
159
175
|
const stderr = e.stderr?.toString() ?? "";
|
|
160
176
|
const stdout = e.stdout?.toString() ?? "";
|
|
161
|
-
|
|
177
|
+
const errorOutput = stripNoise(stripAnsi(stdout + stderr)).cleaned;
|
|
178
|
+
|
|
179
|
+
// Try structured error parsing
|
|
180
|
+
const { errorParser } = await import("./parsers/errors.js");
|
|
181
|
+
if (errorOutput.length > 200 && errorParser.detect(actualCmd, errorOutput)) {
|
|
182
|
+
const info = errorParser.parse(actualCmd, errorOutput);
|
|
183
|
+
if (jsonMode) {
|
|
184
|
+
console.log(JSON.stringify({ exitCode: e.status ?? 1, error: info }));
|
|
185
|
+
} else {
|
|
186
|
+
console.log(`Error: ${info.type}`);
|
|
187
|
+
console.log(` ${info.message}`);
|
|
188
|
+
if (info.file) console.log(` File: ${info.file}${info.line ? `:${info.line}` : ""}`);
|
|
189
|
+
if (info.suggestion) console.log(` Fix: ${info.suggestion}`);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Short error or no parser match — pass through cleaned
|
|
193
|
+
console.log(errorOutput);
|
|
194
|
+
}
|
|
162
195
|
process.exit(e.status ?? 1);
|
|
163
196
|
}
|
|
164
197
|
process.exit(0);
|
package/src/parsers/git.ts
CHANGED
|
@@ -13,6 +13,21 @@ export const gitLogParser: Parser<GitLogEntry[]> = {
|
|
|
13
13
|
const entries: GitLogEntry[] = [];
|
|
14
14
|
const lines = output.split("\n");
|
|
15
15
|
|
|
16
|
+
// Detect oneline format: "abc1234 commit message"
|
|
17
|
+
const firstLine = lines[0]?.trim() ?? "";
|
|
18
|
+
const isOneline = /^[a-f0-9]{7,12}\s+/.test(firstLine) && !firstLine.startsWith("commit ");
|
|
19
|
+
|
|
20
|
+
if (isOneline) {
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const match = line.trim().match(/^([a-f0-9]{7,12})\s+(.+)$/);
|
|
23
|
+
if (match) {
|
|
24
|
+
entries.push({ hash: match[1], author: "", date: "", message: match[2] });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return entries;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Verbose format
|
|
16
31
|
let hash = "", author = "", date = "", message: string[] = [];
|
|
17
32
|
|
|
18
33
|
for (const line of lines) {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Test focus tracker — tracks test status across runs, only reports changes
|
|
2
|
+
// Instead of showing "248 passed, 2 failed" every time, shows:
|
|
3
|
+
// "auth.login: FIXED, auth.logout: STILL FAILING, 246 unchanged"
|
|
4
|
+
|
|
5
|
+
export interface TestStatus {
|
|
6
|
+
name: string;
|
|
7
|
+
status: "pass" | "fail";
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TestWatchResult {
|
|
12
|
+
/** Tests that changed status since last run */
|
|
13
|
+
changed: { name: string; from: "pass" | "fail"; to: "pass" | "fail"; error?: string }[];
|
|
14
|
+
/** New tests not seen before */
|
|
15
|
+
newTests: TestStatus[];
|
|
16
|
+
/** Summary counts */
|
|
17
|
+
totalPassed: number;
|
|
18
|
+
totalFailed: number;
|
|
19
|
+
unchangedCount: number;
|
|
20
|
+
/** Whether this is the first run (no previous data) */
|
|
21
|
+
firstRun: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Per-cwd watchlist
|
|
25
|
+
const watchlists = new Map<string, Map<string, TestStatus>>();
|
|
26
|
+
|
|
27
|
+
/** Extract test names and status from test runner output (any runner) */
|
|
28
|
+
function extractTests(output: string): TestStatus[] {
|
|
29
|
+
const tests: TestStatus[] = [];
|
|
30
|
+
const lines = output.split("\n");
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i];
|
|
34
|
+
|
|
35
|
+
// PASS/FAIL with test name: "PASS src/auth.test.ts" or "✓ login works" or "✗ logout fails"
|
|
36
|
+
const passMatch = line.match(/(?:PASS|✓|✔|✅)\s+(.+)/);
|
|
37
|
+
if (passMatch) {
|
|
38
|
+
tests.push({ name: passMatch[1].trim(), status: "pass" });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const failMatch = line.match(/(?:FAIL|✗|✕|❌|×)\s+(.+)/);
|
|
43
|
+
if (failMatch) {
|
|
44
|
+
// Capture error from next few lines
|
|
45
|
+
const errorLines: string[] = [];
|
|
46
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
47
|
+
if (lines[j].match(/(?:PASS|FAIL|✓|✗|✔|✕|Tests:|^\s*$)/)) break;
|
|
48
|
+
errorLines.push(lines[j].trim());
|
|
49
|
+
}
|
|
50
|
+
tests.push({ name: failMatch[1].trim(), status: "fail", error: errorLines.join(" ").slice(0, 200) });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Jest/vitest style: " ● test name" for failures
|
|
55
|
+
const jestFail = line.match(/^\s*●\s+(.+)/);
|
|
56
|
+
if (jestFail) {
|
|
57
|
+
tests.push({ name: jestFail[1].trim(), status: "fail" });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return tests;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Detect if output looks like test runner output */
|
|
66
|
+
export function isTestOutput(output: string): boolean {
|
|
67
|
+
const markers = /(?:Tests?:|PASS|FAIL|✓|✗|passed|failed|\d+\s+pass|\d+\s+fail)/i;
|
|
68
|
+
const lines = output.split("\n");
|
|
69
|
+
return lines.filter(l => markers.test(l)).length >= 2;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Track test results and return only changes */
|
|
73
|
+
export function trackTests(cwd: string, output: string): TestWatchResult {
|
|
74
|
+
const current = extractTests(output);
|
|
75
|
+
const prev = watchlists.get(cwd);
|
|
76
|
+
|
|
77
|
+
// Count totals from raw output (more reliable than extracted tests)
|
|
78
|
+
let totalPassed = 0, totalFailed = 0;
|
|
79
|
+
const summaryMatch = output.match(/(\d+)\s+pass/i);
|
|
80
|
+
const failMatch = output.match(/(\d+)\s+fail/i);
|
|
81
|
+
if (summaryMatch) totalPassed = parseInt(summaryMatch[1]);
|
|
82
|
+
if (failMatch) totalFailed = parseInt(failMatch[1]);
|
|
83
|
+
// Fallback to extracted counts
|
|
84
|
+
if (totalPassed === 0) totalPassed = current.filter(t => t.status === "pass").length;
|
|
85
|
+
if (totalFailed === 0) totalFailed = current.filter(t => t.status === "fail").length;
|
|
86
|
+
|
|
87
|
+
// Store current for next comparison
|
|
88
|
+
const currentMap = new Map<string, TestStatus>();
|
|
89
|
+
for (const t of current) currentMap.set(t.name, t);
|
|
90
|
+
watchlists.set(cwd, currentMap);
|
|
91
|
+
|
|
92
|
+
// First run — no comparison possible
|
|
93
|
+
if (!prev) {
|
|
94
|
+
return {
|
|
95
|
+
changed: [],
|
|
96
|
+
newTests: current.filter(t => t.status === "fail"), // only show failures on first run
|
|
97
|
+
totalPassed,
|
|
98
|
+
totalFailed,
|
|
99
|
+
unchangedCount: 0,
|
|
100
|
+
firstRun: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Compare with previous
|
|
105
|
+
const changed: TestWatchResult["changed"] = [];
|
|
106
|
+
const newTests: TestStatus[] = [];
|
|
107
|
+
let unchangedCount = 0;
|
|
108
|
+
|
|
109
|
+
for (const [name, test] of currentMap) {
|
|
110
|
+
const prevTest = prev.get(name);
|
|
111
|
+
if (!prevTest) {
|
|
112
|
+
newTests.push(test);
|
|
113
|
+
} else if (prevTest.status !== test.status) {
|
|
114
|
+
changed.push({ name, from: prevTest.status, to: test.status, error: test.error });
|
|
115
|
+
} else {
|
|
116
|
+
unchangedCount++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { changed, newTests, totalPassed, totalFailed, unchangedCount, firstRun: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Format watchlist result for display */
|
|
124
|
+
export function formatWatchResult(result: TestWatchResult): string {
|
|
125
|
+
const lines: string[] = [];
|
|
126
|
+
|
|
127
|
+
if (result.firstRun) {
|
|
128
|
+
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed`);
|
|
129
|
+
if (result.newTests.length > 0) {
|
|
130
|
+
for (const t of result.newTests) {
|
|
131
|
+
lines.push(` ✗ ${t.name}${t.error ? `: ${t.error}` : ""}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Status changes
|
|
138
|
+
for (const c of result.changed) {
|
|
139
|
+
if (c.to === "pass") lines.push(` ✓ FIXED: ${c.name}`);
|
|
140
|
+
else lines.push(` ✗ BROKE: ${c.name}${c.error ? ` — ${c.error}` : ""}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// New failures
|
|
144
|
+
for (const t of result.newTests.filter(t => t.status === "fail")) {
|
|
145
|
+
lines.push(` ✗ NEW FAIL: ${t.name}${t.error ? ` — ${t.error}` : ""}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Summary
|
|
149
|
+
if (result.changed.length === 0 && result.newTests.filter(t => t.status === "fail").length === 0) {
|
|
150
|
+
lines.push(`✓ ${result.totalPassed} passed, ${result.totalFailed} failed (no changes)`);
|
|
151
|
+
} else {
|
|
152
|
+
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed, ${result.unchangedCount} unchanged`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|