faf-taf-git 2.4.0 → 2.4.2

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +1 -1
  3. package/RECEIPT-FORMAT.md +1 -1
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +14 -1
  7. package/dist/cli.js.map +1 -1
  8. package/package.json +15 -2
  9. package/project.faf +32 -242
  10. package/.faf-dna +0 -95
  11. package/.fafignore +0 -41
  12. package/.github/dependabot.yml +0 -10
  13. package/.github/workflows/ci.yml +0 -53
  14. package/.github/workflows/claude-code-review.yml +0 -36
  15. package/.github/workflows/claude.yml +0 -39
  16. package/.github/workflows/homebrew.yml +0 -19
  17. package/.taf +0 -98
  18. package/CLAUDE.md +0 -38
  19. package/CODE_OF_CONDUCT.md +0 -21
  20. package/CONTRIBUTING.md +0 -289
  21. package/SECURITY.md +0 -19
  22. package/action.yml +0 -50
  23. package/agent.fafa +0 -53
  24. package/badge.svg +0 -21
  25. package/dist/bundle/index.js +0 -36554
  26. package/dist/index.js +0 -42827
  27. package/dist/index.js.map +0 -1
  28. package/dist/licenses.txt +0 -126
  29. package/dist/sourcemap-register.js +0 -1
  30. package/docs/RELEASE-NOTES.md +0 -200
  31. package/docs/USER-GUIDE.md +0 -301
  32. package/jest.config.js +0 -13
  33. package/src/badge.ts +0 -138
  34. package/src/cli.ts +0 -297
  35. package/src/index.ts +0 -201
  36. package/src/parsers/bun.ts +0 -103
  37. package/src/parsers/index.ts +0 -50
  38. package/src/parsers/jest.ts +0 -104
  39. package/src/parsers/junit.ts +0 -110
  40. package/src/parsers/vitest.ts +0 -65
  41. package/src/parsers/wjttc.ts +0 -160
  42. package/src/taf-core.ts +0 -139
  43. package/test-mcp.ts +0 -83
  44. package/test-standalone.ts +0 -87
  45. package/tests/action-bundle.test.ts +0 -34
  46. package/tests/action-parse-error.test.ts +0 -47
  47. package/tests/badge.test.ts +0 -260
  48. package/tests/bun-parser.test.ts +0 -173
  49. package/tests/jest-parser.test.ts +0 -122
  50. package/tests/junit-parser.test.ts +0 -109
  51. package/tests/vitest-parser.test.ts +0 -112
  52. package/tests/wjttc-parser.test.ts +0 -116
  53. package/tests/wjttc-taf.test.ts +0 -470
  54. package/tsconfig.json +0 -18
package/src/cli.ts DELETED
@@ -1,297 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * faf-taf-git - The Git-Native Receipt Printer
4
- *
5
- * v2.x CLI: reads test output from file (pre-capture pattern)
6
- * Platform-agnostic - works in ANY CI/CD environment or locally
7
- */
8
-
9
- import { execSync } from 'child_process';
10
- import * as fs from 'fs';
11
- import * as path from 'path';
12
- import { parseTestOutput, UNPARSEABLE_OUTPUT } from './parsers';
13
- import { updateTafFile } from './taf-core';
14
- import { generateBadge } from './badge';
15
-
16
- export interface CLIOptions {
17
- file?: string;
18
- autoCommit?: boolean;
19
- commitMessage?: string;
20
- cwd?: string;
21
- verbose?: boolean;
22
- logger?: (message: string) => void;
23
- }
24
-
25
- export interface CLIResult {
26
- success: boolean;
27
- testResults?: {
28
- total: number;
29
- passed: number;
30
- failed: number;
31
- result: string;
32
- };
33
- tafUpdated: boolean;
34
- error?: string;
35
- }
36
-
37
- /**
38
- * Main CLI function - reads test output from file
39
- */
40
- export async function runTafGit(options: CLIOptions = {}): Promise<CLIResult> {
41
- const {
42
- file,
43
- autoCommit = false,
44
- commitMessage = 'chore: update .taf with test results',
45
- cwd = process.cwd(),
46
- verbose = false,
47
- logger = console.log,
48
- } = options;
49
-
50
- try {
51
- if (!file) {
52
- return {
53
- success: false,
54
- tafUpdated: false,
55
- error: 'No test output file specified. Use --file <path>.\nExample: npm test 2>&1 | tee test-output.txt && npx faf-taf-git --file test-output.txt',
56
- };
57
- }
58
-
59
- // Resolve file path
60
- const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
61
-
62
- if (!fs.existsSync(filePath)) {
63
- return {
64
- success: false,
65
- tafUpdated: false,
66
- error: `Test output file not found: ${filePath}`,
67
- };
68
- }
69
-
70
- const output = fs.readFileSync(filePath, 'utf-8');
71
- if (verbose) logger(`Read ${output.length} bytes from ${filePath}`);
72
-
73
- // Parse test output
74
- const testResults = parseTestOutput(output);
75
-
76
- if (!testResults) {
77
- if (verbose) {
78
- logger(`Parser returned null`);
79
- logger(`Output sample (first 500 chars): ${output.slice(0, 500)}`);
80
- }
81
- return {
82
- success: false,
83
- tafUpdated: false,
84
- error: UNPARSEABLE_OUTPUT,
85
- };
86
- }
87
-
88
- if (verbose) {
89
- logger(`Parsed results: ${testResults.passed}/${testResults.total} tests passing`);
90
- }
91
-
92
- // Check if .taf file exists
93
- const tafPath = path.join(cwd, '.taf');
94
- if (!fs.existsSync(tafPath)) {
95
- return {
96
- success: false,
97
- testResults: {
98
- total: testResults.total,
99
- passed: testResults.passed,
100
- failed: testResults.failed,
101
- result: testResults.result,
102
- },
103
- tafUpdated: false,
104
- error: 'No .taf file found. Run `faf taf init` to create one.',
105
- };
106
- }
107
-
108
- // Update .taf file
109
- const updated = await updateTafFile(tafPath, testResults);
110
-
111
- if (updated) {
112
- if (verbose) logger('.taf file updated successfully');
113
-
114
- // Commit changes if enabled
115
- if (autoCommit) {
116
- commitTafUpdate(cwd, commitMessage, verbose, logger);
117
- if (verbose) logger('Changes committed to git');
118
- }
119
-
120
- return {
121
- success: true,
122
- testResults: {
123
- total: testResults.total,
124
- passed: testResults.passed,
125
- failed: testResults.failed,
126
- result: testResults.result,
127
- },
128
- tafUpdated: true,
129
- };
130
- } else {
131
- return {
132
- success: false,
133
- testResults: {
134
- total: testResults.total,
135
- passed: testResults.passed,
136
- failed: testResults.failed,
137
- result: testResults.result,
138
- },
139
- tafUpdated: false,
140
- error: '.taf file was not updated',
141
- };
142
- }
143
- } catch (error) {
144
- return {
145
- success: false,
146
- tafUpdated: false,
147
- error: error instanceof Error ? error.message : 'Unknown error occurred',
148
- };
149
- }
150
- }
151
-
152
- /**
153
- * Commit .taf file changes to git
154
- */
155
- function commitTafUpdate(cwd: string, message: string, verbose: boolean, logger: (msg: string) => void = console.log): void {
156
- try {
157
- const opts = { cwd, stdio: 'pipe' as const };
158
-
159
- execSync('git add .taf', opts);
160
-
161
- const diff = execSync('git diff --cached --name-only', opts).toString().trim();
162
- if (diff.length === 0) {
163
- if (verbose) logger('No changes to commit');
164
- return;
165
- }
166
-
167
- execSync(`git commit -m "${message}"`, opts);
168
-
169
- try {
170
- execSync('git push', opts);
171
- } catch {
172
- if (verbose) logger('Could not push changes (this is ok in some CI environments)');
173
- }
174
- } catch (error) {
175
- if (verbose) logger(`Error committing changes: ${error}`);
176
- throw error;
177
- }
178
- }
179
-
180
- /**
181
- * Handle the 'badge' subcommand
182
- */
183
- function handleBadgeCommand(args: string[]): void {
184
- if (args.includes('--help') || args.includes('-h')) {
185
- console.log(`\nfaf-taf-git badge - Generate SVG badge from .taf history\n\nUSAGE:\n npx faf-taf-git badge [OPTIONS]\n\nOPTIONS:\n --output <path> Write SVG to file (default: stdout)\n --label <text> Badge label (default: TAF)\n --taf <path> Path to .taf file (default: .taf)\n --help, -h Show this help\n`);
186
- process.exit(0);
187
- }
188
-
189
- // Parse --output
190
- let outputPath: string | undefined;
191
- const outputIndex = args.indexOf('--output');
192
- if (outputIndex !== -1 && args[outputIndex + 1]) {
193
- outputPath = args[outputIndex + 1];
194
- }
195
-
196
- // Parse --label
197
- let label: string | undefined;
198
- const labelIndex = args.indexOf('--label');
199
- if (labelIndex !== -1 && args[labelIndex + 1]) {
200
- label = args[labelIndex + 1];
201
- }
202
-
203
- // Parse --taf
204
- let tafPath: string | undefined;
205
- const tafIndex = args.indexOf('--taf');
206
- if (tafIndex !== -1 && args[tafIndex + 1]) {
207
- tafPath = args[tafIndex + 1];
208
- }
209
-
210
- const result = generateBadge({ tafPath, label });
211
-
212
- if (!result.success || !result.svg) {
213
- console.error(`Error: ${result.error || 'Failed to generate badge'}`);
214
- process.exit(1);
215
- }
216
-
217
- if (outputPath) {
218
- fs.writeFileSync(outputPath, result.svg, 'utf-8');
219
- console.log(`Badge written to ${outputPath}`);
220
- } else {
221
- process.stdout.write(result.svg);
222
- }
223
- }
224
-
225
- /**
226
- * CLI entry point
227
- */
228
- async function main() {
229
- const args = process.argv.slice(2);
230
-
231
- // Subcommand: badge
232
- if (args[0] === 'badge') {
233
- handleBadgeCommand(args.slice(1));
234
- return;
235
- }
236
-
237
- // Show help
238
- if (args.includes('--help') || args.includes('-h')) {
239
- console.log(`\nfaf-taf-git - The Git-Native Receipt Printer\n\nUSAGE:\n npm test 2>&1 | tee test-output.txt\n npx faf-taf-git --file test-output.txt [OPTIONS]\n\n # WJTTC / pc-ai bar\n node tests/wjttc/run-wjttc.mjs 2>&1 | tee wjttc-out.txt\n npx faf-taf-git --file wjttc-out.txt\n\nCOMMANDS:\n badge Generate a shields.io-style SVG badge from .taf\n\nOPTIONS:\n --file <path> Path to test output file (required)\n --commit Auto-commit .taf changes to git\n --message <msg> Custom commit message\n --cwd <dir> Working directory (default: current)\n --verbose, -v Verbose output\n --help, -h Show this help\n\nPARSERS:\n JUnit XML (pytest, cargo, Go, Java, .NET ...), Bun, WJTTC (pc-ai bar), Jest, Vitest\n\nEXAMPLES:\n npm test 2>&1 | tee test-output.txt\n npx faf-taf-git --file test-output.txt\n\n npx faf-taf-git --file test-output.txt --commit\n\n npx faf-taf-git badge --output badge.svg\n\nPLATFORM SUPPORT:\n Works in ANY CI/CD that runs Node.js:\n GitHub Actions, GitLab CI, Jenkins, CircleCI,\n Bitbucket Pipelines, Azure Pipelines, local dev\n\nLEARN MORE:\n https://github.com/Wolfe-Jam/faf-taf-git\n`);
240
- process.exit(0);
241
- }
242
-
243
- const options: CLIOptions = {
244
- verbose: args.includes('--verbose') || args.includes('-v'),
245
- };
246
-
247
- // Parse --file
248
- const fileIndex = args.indexOf('--file');
249
- if (fileIndex !== -1 && args[fileIndex + 1]) {
250
- options.file = args[fileIndex + 1];
251
- }
252
-
253
- // Parse auto-commit flag
254
- if (args.includes('--commit')) {
255
- options.autoCommit = true;
256
- }
257
-
258
- // Parse commit message
259
- const messageIndex = args.indexOf('--message');
260
- if (messageIndex !== -1 && args[messageIndex + 1]) {
261
- options.commitMessage = args[messageIndex + 1];
262
- }
263
-
264
- // Parse working directory
265
- const cwdIndex = args.indexOf('--cwd');
266
- if (cwdIndex !== -1 && args[cwdIndex + 1]) {
267
- options.cwd = args[cwdIndex + 1];
268
- }
269
-
270
- // Run the tool
271
- const result = await runTafGit(options);
272
-
273
- if (result.success) {
274
- if (result.testResults) {
275
- console.log(`Tests: ${result.testResults.passed}/${result.testResults.total} passing`);
276
- console.log(`Result: ${result.testResults.result}`);
277
- }
278
- if (result.tafUpdated) {
279
- console.log('.taf file updated');
280
- }
281
- process.exit(0);
282
- } else {
283
- console.error(`Error: ${result.error}`);
284
- if (result.testResults) {
285
- console.log(`Tests: ${result.testResults.passed}/${result.testResults.total} passing`);
286
- }
287
- process.exit(1);
288
- }
289
- }
290
-
291
- // Run CLI if called directly
292
- if (require.main === module) {
293
- main().catch((error) => {
294
- console.error('Fatal error:', error);
295
- process.exit(1);
296
- });
297
- }
package/src/index.ts DELETED
@@ -1,201 +0,0 @@
1
- /**
2
- * faf-taf-git v2.0.0 - GitHub Actions wrapper
3
- *
4
- * Pre-Capture Pattern: Reads test output from file (written by previous step)
5
- * Handles GitHub Actions-specific inputs/outputs
6
- */
7
-
8
- import * as core from '@actions/core';
9
- import * as exec from '@actions/exec';
10
- import * as fs from 'fs';
11
- import * as path from 'path';
12
- import { parseTestOutput, UNPARSEABLE_OUTPUT } from './parsers';
13
- import { updateTafFile } from './taf-core';
14
-
15
- /**
16
- * Switch to a dedicated target branch for TAF receipts.
17
- * Creates the branch as an orphan on first run, seeded with existing .taf.
18
- */
19
- async function switchToTargetBranch(cwd: string, targetBranch: string, tafSeed: string | null): Promise<void> {
20
- const execOptions = { cwd };
21
-
22
- // Configure git (needed before any git operations)
23
- await exec.exec('git', ['config', '--global', 'user.name', 'faf-taf-git[bot]'], execOptions);
24
- await exec.exec('git', ['config', '--global', 'user.email', 'faf-taf-git[bot]@users.noreply.github.com'], execOptions);
25
-
26
- // Check if target branch exists on remote
27
- const { exitCode } = await exec.getExecOutput(
28
- 'git', ['ls-remote', '--exit-code', 'origin', targetBranch],
29
- { ...execOptions, ignoreReturnCode: true }
30
- );
31
-
32
- if (exitCode === 0) {
33
- // Branch exists — fetch and checkout
34
- // Stash any working tree changes (e.g. dist/ from build step) to allow clean checkout
35
- await exec.exec('git', ['stash', '--include-untracked'], execOptions);
36
- await exec.exec('git', ['fetch', 'origin', targetBranch], execOptions);
37
- await exec.exec('git', ['checkout', targetBranch], execOptions);
38
- core.info(`Switched to existing branch: ${targetBranch}`);
39
- } else {
40
- // Branch doesn't exist — create orphan
41
- await exec.exec('git', ['checkout', '--orphan', targetBranch], execOptions);
42
- await exec.getExecOutput('git', ['rm', '-rf', '.'], { ...execOptions, ignoreReturnCode: true });
43
-
44
- const tafPath = path.join(cwd, '.taf');
45
-
46
- if (tafSeed) {
47
- // Seed with existing .taf from source branch
48
- fs.writeFileSync(tafPath, tafSeed, 'utf-8');
49
- core.info(`Seeded ${targetBranch} with existing .taf data`);
50
- } else {
51
- // No seed — create minimal valid YAML so updateTafFile can populate test_history.
52
- // Without this, `git add .taf` fails with exit 128 (pathspec did not match)
53
- // when consumer repo has no .taf on source branch.
54
- const projectName = path.basename(cwd);
55
- const minimal =
56
- `format_version: 1.0.0\n` +
57
- `project: ${projectName}\n` +
58
- `created: ${new Date().toISOString()}\n` +
59
- `test_history: []\n`;
60
- fs.writeFileSync(tafPath, minimal, 'utf-8');
61
- core.info(`Created empty .taf for new ${targetBranch} branch (no seed)`);
62
- }
63
-
64
- await exec.exec('git', ['add', '.taf'], execOptions);
65
- await exec.exec('git', ['commit', '-m', `chore(taf): initialize ${targetBranch} branch [skip ci]`], execOptions);
66
- core.info(`Created new branch: ${targetBranch}`);
67
- }
68
- }
69
-
70
- /**
71
- * Commit .taf file changes to git and push
72
- */
73
- async function commitTafUpdate(cwd: string, message: string, targetBranch?: string): Promise<void> {
74
- const execOptions = { cwd };
75
-
76
- // Configure git
77
- await exec.exec('git', ['config', '--global', 'user.name', 'faf-taf-git[bot]'], execOptions);
78
- await exec.exec('git', ['config', '--global', 'user.email', 'faf-taf-git[bot]@users.noreply.github.com'], execOptions);
79
-
80
- // Stage .taf file
81
- await exec.exec('git', ['add', '.taf'], execOptions);
82
-
83
- // Check if there are changes to commit
84
- const { exitCode } = await exec.getExecOutput('git', ['diff', '--cached', '--name-only'], execOptions);
85
-
86
- if (exitCode === 0) {
87
- await exec.exec('git', ['commit', '-m', message], execOptions);
88
-
89
- // Push to target branch or current branch
90
- const pushArgs = targetBranch ? ['push', 'origin', targetBranch] : ['push'];
91
- try {
92
- await exec.exec('git', pushArgs, execOptions);
93
- } catch (error) {
94
- core.warning('Could not push changes (this is ok in some CI environments)');
95
- }
96
- }
97
- }
98
-
99
- async function run(): Promise<void> {
100
- try {
101
- // Get GitHub Action inputs
102
- const testOutputFile = core.getInput('test-output-file', { required: true });
103
- const autoCommit = core.getInput('auto-commit') !== 'false';
104
- const commitMessage = core.getInput('commit-message') || 'chore(taf): update .taf receipt [skip ci]';
105
- const targetBranch = core.getInput('target-branch') || '';
106
- const cwd = process.cwd();
107
-
108
- core.info(`Reading test output from: ${testOutputFile}`);
109
-
110
- // Read test output file (written by previous CI step)
111
- const testOutputPath = path.isAbsolute(testOutputFile)
112
- ? testOutputFile
113
- : path.join(cwd, testOutputFile);
114
-
115
- if (!fs.existsSync(testOutputPath)) {
116
- core.setFailed(`Test output file not found: ${testOutputPath}`);
117
- return;
118
- }
119
-
120
- const testOutput = fs.readFileSync(testOutputPath, 'utf-8');
121
- core.info(`Read ${testOutput.length} bytes from test output file`);
122
-
123
- // Debug: Show sample of what we're parsing
124
- const lines = testOutput.split('\n');
125
- const testSummaryLine = lines.find(line => line.includes('Tests:'));
126
- if (testSummaryLine) {
127
- core.info(`Found summary line: ${testSummaryLine.substring(0, 200)}`);
128
- } else {
129
- core.warning('No "Tests:" summary line found in output');
130
- // Show last 20 lines to help debug
131
- core.info('Last 20 lines of output:');
132
- lines.slice(-20).forEach((line, i) => {
133
- core.info(` ${i}: ${line.substring(0, 150)}`);
134
- });
135
- }
136
-
137
- // Parse test results
138
- const testResults = parseTestOutput(testOutput);
139
-
140
- if (!testResults) {
141
- core.setFailed(UNPARSEABLE_OUTPUT);
142
- return;
143
- }
144
-
145
- core.info(`Parsed results: ${testResults.passed}/${testResults.total} tests passing`);
146
-
147
- // Set GitHub Action outputs
148
- core.setOutput('result', testResults.result);
149
- core.setOutput('passed', testResults.passed.toString());
150
- core.setOutput('failed', testResults.failed.toString());
151
- core.setOutput('total', testResults.total.toString());
152
- core.setOutput('skipped', (testResults.skipped ?? 0).toString());
153
-
154
- // If target-branch is set, save .taf seed and switch branches
155
- let tafSeed: string | null = null;
156
- const tafPath = path.join(cwd, '.taf');
157
-
158
- if (targetBranch) {
159
- // Save existing .taf from source branch as seed for first-run
160
- if (fs.existsSync(tafPath)) {
161
- tafSeed = fs.readFileSync(tafPath, 'utf-8');
162
- }
163
- core.info(`Switching to target branch: ${targetBranch}`);
164
- await switchToTargetBranch(cwd, targetBranch, tafSeed);
165
- }
166
-
167
- // Check if .taf file exists (on current or target branch)
168
- if (!fs.existsSync(tafPath)) {
169
- core.setFailed('No .taf file found. Run `faf taf init` to create one.');
170
- return;
171
- }
172
-
173
- // Update .taf file
174
- const updated = await updateTafFile(tafPath, testResults);
175
-
176
- if (!updated) {
177
- core.setOutput('taf-updated', 'false');
178
- core.setFailed('.taf file was not updated');
179
- return;
180
- }
181
-
182
- core.setOutput('taf-updated', 'true');
183
- core.info('✅ .taf file updated successfully');
184
-
185
- // Commit changes if enabled
186
- if (autoCommit) {
187
- await commitTafUpdate(cwd, commitMessage, targetBranch || undefined);
188
- core.info(`✅ Changes committed${targetBranch ? ` to ${targetBranch}` : ''}`);
189
- }
190
-
191
- } catch (error) {
192
- if (error instanceof Error) {
193
- core.setFailed(error.message);
194
- } else {
195
- core.setFailed('Unknown error occurred');
196
- }
197
- }
198
- }
199
-
200
- // Run the GitHub Action
201
- run();
@@ -1,103 +0,0 @@
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
- */
@@ -1,50 +0,0 @@
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 { parseJunitOutput } from './junit';
11
- import { parseVitestOutput } from './vitest';
12
- import { parseWjttcOutput } from './wjttc';
13
-
14
- export { TestResults } from './jest';
15
- export { parseJunitOutput } from './junit';
16
- export { parseWjttcOutput } from './wjttc';
17
-
18
- /**
19
- * Every parser, in the order parseTestOutput tries them. Order matters:
20
- * 1. JUnit XML — a `<testsuite>` element; first, because a report's captured
21
- * stdout can hold another runner's summary line.
22
- * 2. Bun — `Ran N tests across M files` is bun-unique.
23
- * 3. WJTTC — `pc-ai WJTTC` / suite `pc-ai-wjttc` (FAF bar; not Jest-shaped).
24
- * 4. Jest — `Tests: … total`.
25
- * 5. Vitest — `Tests … (N)`.
26
- */
27
- const PARSERS: ReadonlyArray<readonly [name: string, parse: (output: string) => TestResults | null]> = [
28
- ['JUnit XML', parseJunitOutput],
29
- ['Bun', parseBunOutput],
30
- ['WJTTC', parseWjttcOutput],
31
- ['Jest', parseJestOutput],
32
- ['Vitest', parseVitestOutput],
33
- ];
34
-
35
- export const SUPPORTED_PARSERS: readonly string[] = PARSERS.map(([name]) => name);
36
-
37
- /** What the CLI and the Action report when no parser matches. */
38
- export const UNPARSEABLE_OUTPUT = `Could not parse test output. Supported: ${SUPPORTED_PARSERS.join(', ')}.`;
39
-
40
- /**
41
- * Parse test output from any supported framework: the first parser in
42
- * PARSERS that recognises the output wins.
43
- */
44
- export function parseTestOutput(output: string): TestResults | null {
45
- for (const [, parse] of PARSERS) {
46
- const results = parse(output);
47
- if (results) return results;
48
- }
49
- return null;
50
- }