faf-taf-git 2.0.4 → 2.2.1
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/.github/workflows/ci.yml +53 -0
- package/.github/workflows/claude-code-review.yml.disabled +29 -0
- package/.github/workflows/claude.yml +39 -0
- package/.github/workflows/homebrew.yml +19 -0
- package/.taf +79 -2
- package/CHANGELOG.md +28 -0
- package/CLAUDE.md +29 -21
- package/CODE_OF_CONDUCT.md +21 -0
- package/CONTRIBUTING.md +3 -0
- package/README.md +69 -12
- package/SECURITY.md +19 -0
- package/action.yml +6 -1
- package/badge.svg +21 -0
- package/dist/badge.d.ts +23 -0
- package/dist/badge.d.ts.map +1 -0
- package/dist/badge.js +147 -0
- package/dist/badge.js.map +1 -0
- package/dist/cli.d.ts +5 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +144 -193
- package/dist/cli.js.map +1 -1
- package/dist/index.js +26045 -20566
- package/dist/index.js.map +1 -1
- package/dist/licenses.txt +0 -22
- package/dist/parsers/bun.d.ts +59 -0
- package/dist/parsers/bun.d.ts.map +1 -0
- package/dist/parsers/bun.js +100 -0
- package/dist/parsers/bun.js.map +1 -0
- package/dist/parsers/index.d.ts +20 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +26 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/vitest.d.ts +17 -0
- package/dist/parsers/vitest.d.ts.map +1 -0
- package/dist/parsers/vitest.js +61 -0
- package/dist/parsers/vitest.js.map +1 -0
- package/package.json +6 -7
- package/project.faf +20 -24
- package/src/badge.ts +138 -0
- package/src/cli.ts +154 -200
- package/src/index.ts +79 -10
- package/src/parsers/bun.ts +103 -0
- package/src/parsers/index.ts +27 -0
- package/src/parsers/vitest.ts +65 -0
- package/tests/badge.test.ts +260 -0
- package/tests/bun-parser.test.ts +173 -0
- package/tests/vitest-parser.test.ts +112 -0
- package/.faf +0 -126
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun test output parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts test counts from `bun test` CLI output.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TestResults } from './jest';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse Bun test output to extract test results.
|
|
11
|
+
*
|
|
12
|
+
* Bun output format (summary block at the end of a run):
|
|
13
|
+
*
|
|
14
|
+
* ```
|
|
15
|
+
* 232 pass
|
|
16
|
+
* 1 skip
|
|
17
|
+
* 1 todo
|
|
18
|
+
* 0 fail
|
|
19
|
+
* 848 expect() calls
|
|
20
|
+
* Ran 234 tests across 9 files. [21.53s]
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Notes:
|
|
24
|
+
* - Anchored on `Ran <N> tests across <M> files.` — the canonical truthful
|
|
25
|
+
* total (includes skip + todo + pass + fail). Without this line, the parser
|
|
26
|
+
* returns null and the dispatcher falls through to jest/vitest.
|
|
27
|
+
* - Per-category counts (`pass`, `fail`, `skip`, `todo`) come from their own
|
|
28
|
+
* lines elsewhere in the same output. Singular forms — bun does not use
|
|
29
|
+
* `passed`/`failed`/`skipped` (intentional differentiator from jest/vitest).
|
|
30
|
+
* - `skipped` aggregates `skip` + `todo` (matches the convention vitest.ts
|
|
31
|
+
* already uses for parity in the shared TestResults shape).
|
|
32
|
+
*/
|
|
33
|
+
export function parseBunOutput(output: string): TestResults | null {
|
|
34
|
+
// Strip ANSI color codes + normalize line endings (parity with siblings).
|
|
35
|
+
// eslint-disable-next-line no-control-regex
|
|
36
|
+
let cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
|
|
37
|
+
cleanOutput = cleanOutput.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
38
|
+
|
|
39
|
+
// Canonical anchor: bun-unique "Ran N tests across M files." summary line.
|
|
40
|
+
// Without this, this isn't bun output — let the dispatcher try the others.
|
|
41
|
+
const ranMatch = cleanOutput.match(/^Ran\s+(\d+)\s+tests?\s+across\s+\d+\s+files?/m);
|
|
42
|
+
if (!ranMatch) return null;
|
|
43
|
+
|
|
44
|
+
const total = parseInt(ranMatch[1], 10);
|
|
45
|
+
if (total === 0) return null;
|
|
46
|
+
|
|
47
|
+
let passed = 0;
|
|
48
|
+
let failed = 0;
|
|
49
|
+
let skipped = 0;
|
|
50
|
+
|
|
51
|
+
// Per-line counts. Each line is `<leading-ws><N> <singular-word>` on its own.
|
|
52
|
+
// Anchored with ^...$ + multiline to avoid catching the "expect() calls" line
|
|
53
|
+
// or any noise inside test descriptions.
|
|
54
|
+
const passedMatch = cleanOutput.match(/^\s*(\d+)\s+pass\s*$/m);
|
|
55
|
+
if (passedMatch) passed = parseInt(passedMatch[1], 10);
|
|
56
|
+
|
|
57
|
+
const failedMatch = cleanOutput.match(/^\s*(\d+)\s+fail\s*$/m);
|
|
58
|
+
if (failedMatch) failed = parseInt(failedMatch[1], 10);
|
|
59
|
+
|
|
60
|
+
const skipMatch = cleanOutput.match(/^\s*(\d+)\s+skip\s*$/m);
|
|
61
|
+
if (skipMatch) skipped += parseInt(skipMatch[1], 10);
|
|
62
|
+
|
|
63
|
+
const todoMatch = cleanOutput.match(/^\s*(\d+)\s+todo\s*$/m);
|
|
64
|
+
if (todoMatch) skipped += parseInt(todoMatch[1], 10);
|
|
65
|
+
|
|
66
|
+
const result: 'PASSED' | 'FAILED' = failed > 0 ? 'FAILED' : 'PASSED';
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
total,
|
|
70
|
+
passed,
|
|
71
|
+
failed,
|
|
72
|
+
skipped: skipped > 0 ? skipped : undefined,
|
|
73
|
+
result,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Example Bun outputs for testing:
|
|
79
|
+
*
|
|
80
|
+
* All passing:
|
|
81
|
+
* ```
|
|
82
|
+
* 173 pass
|
|
83
|
+
* 0 fail
|
|
84
|
+
* Ran 173 tests across 8 files. [4.2s]
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* Mixed (the grok-faf-mcp 1.4.9 shape):
|
|
88
|
+
* ```
|
|
89
|
+
* 232 pass
|
|
90
|
+
* 1 skip
|
|
91
|
+
* 1 todo
|
|
92
|
+
* 0 fail
|
|
93
|
+
* 848 expect() calls
|
|
94
|
+
* Ran 234 tests across 9 files. [21.53s]
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* Failing:
|
|
98
|
+
* ```
|
|
99
|
+
* 170 pass
|
|
100
|
+
* 3 fail
|
|
101
|
+
* Ran 173 tests across 8 files. [5.1s]
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified test output parser
|
|
3
|
+
*
|
|
4
|
+
* Tries each framework parser in order until one succeeds.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TestResults } from './jest';
|
|
8
|
+
import { parseBunOutput } from './bun';
|
|
9
|
+
import { parseJestOutput } from './jest';
|
|
10
|
+
import { parseVitestOutput } from './vitest';
|
|
11
|
+
|
|
12
|
+
export { TestResults } from './jest';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse test output from any supported framework.
|
|
16
|
+
*
|
|
17
|
+
* Order matters: Bun is tried first because its `Ran N tests across M files`
|
|
18
|
+
* anchor is bun-unique — it never false-matches on jest/vitest output. Putting
|
|
19
|
+
* it first also lets bun consumers ship truthful totals even when their CI
|
|
20
|
+
* still has a legacy `Tests: N passed, N total` jest-shim line appended to the
|
|
21
|
+
* test output (the bun parser ignores it and reads the canonical `Ran` line).
|
|
22
|
+
*
|
|
23
|
+
* Tries parsers in order: Bun, Jest, Vitest.
|
|
24
|
+
*/
|
|
25
|
+
export function parseTestOutput(output: string): TestResults | null {
|
|
26
|
+
return parseBunOutput(output) || parseJestOutput(output) || parseVitestOutput(output);
|
|
27
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest output parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts test counts from Vitest CLI output
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TestResults } from './jest';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse Vitest output to extract test results
|
|
11
|
+
*
|
|
12
|
+
* Vitest output formats:
|
|
13
|
+
* - " Tests 8 passed (8)"
|
|
14
|
+
* - " Tests 2 failed | 6 passed (8)"
|
|
15
|
+
* - " Tests 1 skipped | 7 passed (8)"
|
|
16
|
+
* - " Tests 1 failed | 2 skipped | 5 passed (8)"
|
|
17
|
+
*/
|
|
18
|
+
export function parseVitestOutput(output: string): TestResults | null {
|
|
19
|
+
// Strip ANSI color codes
|
|
20
|
+
// eslint-disable-next-line no-control-regex
|
|
21
|
+
let cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
|
|
22
|
+
cleanOutput = cleanOutput.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
23
|
+
|
|
24
|
+
// Vitest summary pattern: " Tests ... (N)" where N is total in parens
|
|
25
|
+
const testSummaryPattern = /^\s*Tests\s+(.+?\(\d+\))/m;
|
|
26
|
+
const testLineMatch = cleanOutput.match(testSummaryPattern);
|
|
27
|
+
|
|
28
|
+
if (!testLineMatch || !testLineMatch[1]) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const testLine = testLineMatch[1];
|
|
33
|
+
|
|
34
|
+
// Extract total from parentheses at end
|
|
35
|
+
const totalMatch = testLine.match(/\((\d+)\)\s*$/);
|
|
36
|
+
if (!totalMatch) return null;
|
|
37
|
+
const total = parseInt(totalMatch[1], 10);
|
|
38
|
+
if (total === 0) return null;
|
|
39
|
+
|
|
40
|
+
let passed = 0;
|
|
41
|
+
let failed = 0;
|
|
42
|
+
let skipped = 0;
|
|
43
|
+
|
|
44
|
+
const passedMatch = testLine.match(/(\d+)\s+passed/i);
|
|
45
|
+
if (passedMatch) passed = parseInt(passedMatch[1], 10);
|
|
46
|
+
|
|
47
|
+
const failedMatch = testLine.match(/(\d+)\s+failed/i);
|
|
48
|
+
if (failedMatch) failed = parseInt(failedMatch[1], 10);
|
|
49
|
+
|
|
50
|
+
const skippedMatch = testLine.match(/(\d+)\s+skipped/i);
|
|
51
|
+
if (skippedMatch) skipped = parseInt(skippedMatch[1], 10);
|
|
52
|
+
|
|
53
|
+
const todoMatch = testLine.match(/(\d+)\s+todo/i);
|
|
54
|
+
if (todoMatch) skipped += parseInt(todoMatch[1], 10);
|
|
55
|
+
|
|
56
|
+
let result: 'PASSED' | 'FAILED' = failed > 0 ? 'FAILED' : 'PASSED';
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
total,
|
|
60
|
+
passed,
|
|
61
|
+
failed,
|
|
62
|
+
skipped: skipped > 0 ? skipped : undefined,
|
|
63
|
+
result,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Badge Generator Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests SVG output validity, .taf parsing, and edge cases.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import { renderBadgeSvg, generateBadge } from '../src/badge';
|
|
11
|
+
|
|
12
|
+
function createTempDir(prefix: string): string {
|
|
13
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
17
|
+
// SVG RENDERING
|
|
18
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
19
|
+
|
|
20
|
+
describe('SVG Rendering', () => {
|
|
21
|
+
test('should produce valid SVG with label and value', () => {
|
|
22
|
+
const svg = renderBadgeSvg('TAF', '10/10 passing', '#003366');
|
|
23
|
+
|
|
24
|
+
expect(svg).toContain('<svg');
|
|
25
|
+
expect(svg).toContain('</svg>');
|
|
26
|
+
expect(svg).toContain('TAF');
|
|
27
|
+
expect(svg).toContain('10/10 passing');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should include correct color', () => {
|
|
31
|
+
const svg = renderBadgeSvg('TAF', '10/10 passing', '#003366');
|
|
32
|
+
|
|
33
|
+
expect(svg).toContain('fill="#003366"');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should include accessibility attributes', () => {
|
|
37
|
+
const svg = renderBadgeSvg('TAF', '5/5 passing', '#003366');
|
|
38
|
+
|
|
39
|
+
expect(svg).toContain('role="img"');
|
|
40
|
+
expect(svg).toContain('aria-label="TAF: 5/5 passing"');
|
|
41
|
+
expect(svg).toContain('<title>TAF: 5/5 passing</title>');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should use Verdana font', () => {
|
|
45
|
+
const svg = renderBadgeSvg('TAF', 'test', '#555');
|
|
46
|
+
|
|
47
|
+
expect(svg).toContain('Verdana');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should have 20px height', () => {
|
|
51
|
+
const svg = renderBadgeSvg('TAF', 'test', '#555');
|
|
52
|
+
|
|
53
|
+
expect(svg).toContain('height="20"');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should have rounded corners via clipPath', () => {
|
|
57
|
+
const svg = renderBadgeSvg('TAF', 'test', '#555');
|
|
58
|
+
|
|
59
|
+
expect(svg).toContain('rx="3"');
|
|
60
|
+
expect(svg).toContain('clip-path="url(#r)"');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should handle custom label', () => {
|
|
64
|
+
const svg = renderBadgeSvg('tests', '99/100 passing', '#E32400');
|
|
65
|
+
|
|
66
|
+
expect(svg).toContain('tests');
|
|
67
|
+
expect(svg).toContain('99/100 passing');
|
|
68
|
+
expect(svg).toContain('#E32400');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
73
|
+
// BADGE GENERATION FROM .taf
|
|
74
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
75
|
+
|
|
76
|
+
describe('Badge Generation', () => {
|
|
77
|
+
let tempDir: string;
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
tempDir = createTempDir('badge-test-');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should generate navy badge when all tests pass', () => {
|
|
88
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
89
|
+
fs.writeFileSync(tafPath, `format_version: 1.0.0
|
|
90
|
+
project: test
|
|
91
|
+
test_history:
|
|
92
|
+
- timestamp: "2026-01-01T00:00:00Z"
|
|
93
|
+
result: PASSED
|
|
94
|
+
tests:
|
|
95
|
+
total: 38
|
|
96
|
+
passed: 38
|
|
97
|
+
failed: 0
|
|
98
|
+
`);
|
|
99
|
+
|
|
100
|
+
const result = generateBadge({ tafPath });
|
|
101
|
+
|
|
102
|
+
expect(result.success).toBe(true);
|
|
103
|
+
expect(result.svg).toContain('38/38 passing');
|
|
104
|
+
expect(result.svg).toContain('#003366');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should generate red badge when tests fail', () => {
|
|
108
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
109
|
+
fs.writeFileSync(tafPath, `format_version: 1.0.0
|
|
110
|
+
project: test
|
|
111
|
+
test_history:
|
|
112
|
+
- timestamp: "2026-01-01T00:00:00Z"
|
|
113
|
+
result: FAILED
|
|
114
|
+
tests:
|
|
115
|
+
total: 38
|
|
116
|
+
passed: 35
|
|
117
|
+
failed: 3
|
|
118
|
+
`);
|
|
119
|
+
|
|
120
|
+
const result = generateBadge({ tafPath });
|
|
121
|
+
|
|
122
|
+
expect(result.success).toBe(true);
|
|
123
|
+
expect(result.svg).toContain('35/38 passing');
|
|
124
|
+
expect(result.svg).toContain('#E32400');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('should generate gray badge when .taf is missing', () => {
|
|
128
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
129
|
+
// Don't create the file
|
|
130
|
+
|
|
131
|
+
const result = generateBadge({ tafPath });
|
|
132
|
+
|
|
133
|
+
expect(result.success).toBe(true);
|
|
134
|
+
expect(result.svg).toContain('no data');
|
|
135
|
+
expect(result.svg).toContain('#9F9F9F');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should generate gray badge when test_history is empty', () => {
|
|
139
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
140
|
+
fs.writeFileSync(tafPath, `format_version: 1.0.0
|
|
141
|
+
project: test
|
|
142
|
+
test_history: []
|
|
143
|
+
`);
|
|
144
|
+
|
|
145
|
+
const result = generateBadge({ tafPath });
|
|
146
|
+
|
|
147
|
+
expect(result.success).toBe(true);
|
|
148
|
+
expect(result.svg).toContain('no data');
|
|
149
|
+
expect(result.svg).toContain('#9F9F9F');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('should use latest run from multiple entries', () => {
|
|
153
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
154
|
+
fs.writeFileSync(tafPath, `format_version: 1.0.0
|
|
155
|
+
project: test
|
|
156
|
+
test_history:
|
|
157
|
+
- timestamp: "2026-01-01T00:00:00Z"
|
|
158
|
+
result: FAILED
|
|
159
|
+
tests:
|
|
160
|
+
total: 10
|
|
161
|
+
passed: 7
|
|
162
|
+
failed: 3
|
|
163
|
+
- timestamp: "2026-01-02T00:00:00Z"
|
|
164
|
+
result: PASSED
|
|
165
|
+
tests:
|
|
166
|
+
total: 10
|
|
167
|
+
passed: 10
|
|
168
|
+
failed: 0
|
|
169
|
+
`);
|
|
170
|
+
|
|
171
|
+
const result = generateBadge({ tafPath });
|
|
172
|
+
|
|
173
|
+
expect(result.success).toBe(true);
|
|
174
|
+
expect(result.svg).toContain('10/10 passing');
|
|
175
|
+
expect(result.svg).toContain('#003366');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('should use custom label', () => {
|
|
179
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
180
|
+
fs.writeFileSync(tafPath, `format_version: 1.0.0
|
|
181
|
+
project: test
|
|
182
|
+
test_history:
|
|
183
|
+
- timestamp: "2026-01-01T00:00:00Z"
|
|
184
|
+
result: PASSED
|
|
185
|
+
tests:
|
|
186
|
+
total: 5
|
|
187
|
+
passed: 5
|
|
188
|
+
failed: 0
|
|
189
|
+
`);
|
|
190
|
+
|
|
191
|
+
const result = generateBadge({ tafPath, label: 'tests' });
|
|
192
|
+
|
|
193
|
+
expect(result.success).toBe(true);
|
|
194
|
+
expect(result.svg).toContain('tests');
|
|
195
|
+
expect(result.svg).not.toContain('>TAF<');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('should handle single test run', () => {
|
|
199
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
200
|
+
fs.writeFileSync(tafPath, `format_version: 1.0.0
|
|
201
|
+
project: test
|
|
202
|
+
test_history:
|
|
203
|
+
- timestamp: "2026-01-01T00:00:00Z"
|
|
204
|
+
result: PASSED
|
|
205
|
+
tests:
|
|
206
|
+
total: 1
|
|
207
|
+
passed: 1
|
|
208
|
+
failed: 0
|
|
209
|
+
`);
|
|
210
|
+
|
|
211
|
+
const result = generateBadge({ tafPath });
|
|
212
|
+
|
|
213
|
+
expect(result.success).toBe(true);
|
|
214
|
+
expect(result.svg).toContain('1/1 passing');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('should handle large test counts', () => {
|
|
218
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
219
|
+
fs.writeFileSync(tafPath, `format_version: 1.0.0
|
|
220
|
+
project: test
|
|
221
|
+
test_history:
|
|
222
|
+
- timestamp: "2026-01-01T00:00:00Z"
|
|
223
|
+
result: PASSED
|
|
224
|
+
tests:
|
|
225
|
+
total: 10000
|
|
226
|
+
passed: 10000
|
|
227
|
+
failed: 0
|
|
228
|
+
`);
|
|
229
|
+
|
|
230
|
+
const result = generateBadge({ tafPath });
|
|
231
|
+
|
|
232
|
+
expect(result.success).toBe(true);
|
|
233
|
+
expect(result.svg).toContain('10000/10000 passing');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('should handle invalid YAML gracefully', () => {
|
|
237
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
238
|
+
fs.writeFileSync(tafPath, '{{{{ not yaml at all');
|
|
239
|
+
|
|
240
|
+
const result = generateBadge({ tafPath });
|
|
241
|
+
|
|
242
|
+
// Should either return no-data badge or error, but never throw
|
|
243
|
+
expect(result).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('should handle .taf with missing tests field', () => {
|
|
247
|
+
const tafPath = path.join(tempDir, '.taf');
|
|
248
|
+
fs.writeFileSync(tafPath, `format_version: 1.0.0
|
|
249
|
+
project: test
|
|
250
|
+
test_history:
|
|
251
|
+
- timestamp: "2026-01-01T00:00:00Z"
|
|
252
|
+
result: PASSED
|
|
253
|
+
`);
|
|
254
|
+
|
|
255
|
+
const result = generateBadge({ tafPath });
|
|
256
|
+
|
|
257
|
+
expect(result.success).toBe(true);
|
|
258
|
+
expect(result.svg).toContain('no data');
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Bun output parser
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { parseBunOutput } from '../src/parsers/bun';
|
|
6
|
+
|
|
7
|
+
describe('Bun Parser', () => {
|
|
8
|
+
describe('parseBunOutput', () => {
|
|
9
|
+
it('should parse all passing tests (minimal output)', () => {
|
|
10
|
+
const output = `
|
|
11
|
+
173 pass
|
|
12
|
+
0 fail
|
|
13
|
+
612 expect() calls
|
|
14
|
+
Ran 173 tests across 8 files. [4.2s]
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const result = parseBunOutput(output);
|
|
18
|
+
|
|
19
|
+
expect(result).not.toBeNull();
|
|
20
|
+
expect(result?.total).toBe(173);
|
|
21
|
+
expect(result?.passed).toBe(173);
|
|
22
|
+
expect(result?.failed).toBe(0);
|
|
23
|
+
expect(result?.skipped).toBeUndefined();
|
|
24
|
+
expect(result?.result).toBe('PASSED');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should parse the grok-faf-mcp 1.4.9 shape (pass + skip + todo + fail=0)', () => {
|
|
28
|
+
const output = `
|
|
29
|
+
232 pass
|
|
30
|
+
1 skip
|
|
31
|
+
1 todo
|
|
32
|
+
0 fail
|
|
33
|
+
848 expect() calls
|
|
34
|
+
Ran 234 tests across 9 files. [21.53s]
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const result = parseBunOutput(output);
|
|
38
|
+
|
|
39
|
+
expect(result).not.toBeNull();
|
|
40
|
+
expect(result?.total).toBe(234);
|
|
41
|
+
expect(result?.passed).toBe(232);
|
|
42
|
+
expect(result?.failed).toBe(0);
|
|
43
|
+
// skipped aggregates skip + todo (parity with vitest convention)
|
|
44
|
+
expect(result?.skipped).toBe(2);
|
|
45
|
+
expect(result?.result).toBe('PASSED');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should mark result FAILED when any tests fail', () => {
|
|
49
|
+
const output = `
|
|
50
|
+
170 pass
|
|
51
|
+
3 fail
|
|
52
|
+
612 expect() calls
|
|
53
|
+
Ran 173 tests across 8 files. [5.1s]
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const result = parseBunOutput(output);
|
|
57
|
+
|
|
58
|
+
expect(result).not.toBeNull();
|
|
59
|
+
expect(result?.total).toBe(173);
|
|
60
|
+
expect(result?.passed).toBe(170);
|
|
61
|
+
expect(result?.failed).toBe(3);
|
|
62
|
+
expect(result?.result).toBe('FAILED');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle skip-only output', () => {
|
|
66
|
+
const output = `
|
|
67
|
+
170 pass
|
|
68
|
+
3 skip
|
|
69
|
+
0 fail
|
|
70
|
+
612 expect() calls
|
|
71
|
+
Ran 173 tests across 8 files. [5.0s]
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const result = parseBunOutput(output);
|
|
75
|
+
|
|
76
|
+
expect(result?.total).toBe(173);
|
|
77
|
+
expect(result?.passed).toBe(170);
|
|
78
|
+
expect(result?.skipped).toBe(3);
|
|
79
|
+
expect(result?.result).toBe('PASSED');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should strip ANSI color codes', () => {
|
|
83
|
+
const output =
|
|
84
|
+
'\x1b[32m 232 pass\x1b[0m\n' +
|
|
85
|
+
'\x1b[33m 1 skip\x1b[0m\n' +
|
|
86
|
+
'\x1b[33m 1 todo\x1b[0m\n' +
|
|
87
|
+
'\x1b[31m 0 fail\x1b[0m\n' +
|
|
88
|
+
' 848 expect() calls\n' +
|
|
89
|
+
'Ran 234 tests across 9 files. [21.53s]\n';
|
|
90
|
+
|
|
91
|
+
const result = parseBunOutput(output);
|
|
92
|
+
|
|
93
|
+
expect(result?.total).toBe(234);
|
|
94
|
+
expect(result?.passed).toBe(232);
|
|
95
|
+
expect(result?.skipped).toBe(2);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle CRLF line endings', () => {
|
|
99
|
+
const output =
|
|
100
|
+
' 173 pass\r\n 0 fail\r\nRan 173 tests across 8 files. [4.2s]\r\n';
|
|
101
|
+
|
|
102
|
+
const result = parseBunOutput(output);
|
|
103
|
+
|
|
104
|
+
expect(result?.total).toBe(173);
|
|
105
|
+
expect(result?.passed).toBe(173);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should ignore an appended jest-shim "Tests:" line (legacy ci.yml conversion)', () => {
|
|
109
|
+
// Some consumer CIs append a jest-shaped summary so the legacy jest
|
|
110
|
+
// parser could read bun output. With bun support, the appended line
|
|
111
|
+
// should be ignored — the canonical Ran-line wins.
|
|
112
|
+
const output = `
|
|
113
|
+
232 pass
|
|
114
|
+
1 skip
|
|
115
|
+
1 todo
|
|
116
|
+
0 fail
|
|
117
|
+
848 expect() calls
|
|
118
|
+
Ran 234 tests across 9 files. [21.53s]
|
|
119
|
+
Tests: 232 passed, 232 total
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const result = parseBunOutput(output);
|
|
123
|
+
|
|
124
|
+
// Truthful total from "Ran 234" — NOT 232 from the jest-shim line.
|
|
125
|
+
expect(result?.total).toBe(234);
|
|
126
|
+
expect(result?.passed).toBe(232);
|
|
127
|
+
expect(result?.skipped).toBe(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return null when no "Ran N tests" line is present (non-bun output)', () => {
|
|
131
|
+
const output = `
|
|
132
|
+
Test Suites: 18 passed, 18 total
|
|
133
|
+
Tests: 173 passed, 173 total
|
|
134
|
+
Snapshots: 0 total
|
|
135
|
+
Time: 2.5s
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
const result = parseBunOutput(output);
|
|
139
|
+
|
|
140
|
+
// Defers to the next parser in the dispatcher chain.
|
|
141
|
+
expect(result).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should return null on empty input', () => {
|
|
145
|
+
expect(parseBunOutput('')).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return null when "Ran 0 tests" (nothing actually ran)', () => {
|
|
149
|
+
const output = `
|
|
150
|
+
0 pass
|
|
151
|
+
0 fail
|
|
152
|
+
Ran 0 tests across 0 files. [0.1s]
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const result = parseBunOutput(output);
|
|
156
|
+
|
|
157
|
+
expect(result).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle singular forms gracefully (1 test)', () => {
|
|
161
|
+
const output = `
|
|
162
|
+
1 pass
|
|
163
|
+
0 fail
|
|
164
|
+
Ran 1 test across 1 file. [0.5s]
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const result = parseBunOutput(output);
|
|
168
|
+
|
|
169
|
+
expect(result?.total).toBe(1);
|
|
170
|
+
expect(result?.passed).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|