faf-taf-git 2.0.4

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 (47) hide show
  1. package/.faf +126 -0
  2. package/.faf-dna +95 -0
  3. package/.fafignore +41 -0
  4. package/.taf +21 -0
  5. package/CHANGELOG.md +208 -0
  6. package/CLAUDE.md +30 -0
  7. package/CONTRIBUTING.md +286 -0
  8. package/LICENSE +21 -0
  9. package/README.md +496 -0
  10. package/action.yml +42 -0
  11. package/dist/bundle/index.js +36554 -0
  12. package/dist/cli.d.ts +31 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +380 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +36560 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/licenses.txt +148 -0
  21. package/dist/main.d.ts +7 -0
  22. package/dist/main.d.ts.map +1 -0
  23. package/dist/main.js +147 -0
  24. package/dist/main.js.map +1 -0
  25. package/dist/parsers/jest.d.ts +38 -0
  26. package/dist/parsers/jest.d.ts.map +1 -0
  27. package/dist/parsers/jest.js +87 -0
  28. package/dist/parsers/jest.js.map +1 -0
  29. package/dist/sourcemap-register.js +1 -0
  30. package/dist/taf-core.d.ts +12 -0
  31. package/dist/taf-core.d.ts.map +1 -0
  32. package/dist/taf-core.js +136 -0
  33. package/dist/taf-core.js.map +1 -0
  34. package/docs/RELEASE-NOTES.md +200 -0
  35. package/docs/USER-GUIDE.md +301 -0
  36. package/jest.config.js +13 -0
  37. package/package.json +47 -0
  38. package/project.faf +259 -0
  39. package/src/cli.ts +393 -0
  40. package/src/index.ts +131 -0
  41. package/src/parsers/jest.ts +104 -0
  42. package/src/taf-core.ts +139 -0
  43. package/test-mcp.ts +83 -0
  44. package/test-standalone.ts +87 -0
  45. package/tests/jest-parser.test.ts +122 -0
  46. package/tests/wjttc-taf.test.ts +470 -0
  47. package/tsconfig.json +18 -0
package/src/index.ts ADDED
@@ -0,0 +1,131 @@
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 { parseJestOutput } from './parsers/jest';
13
+ import { updateTafFile } from './taf-core';
14
+
15
+ /**
16
+ * Commit .taf file changes to git
17
+ */
18
+ async function commitTafUpdate(cwd: string, message: string): Promise<void> {
19
+ const execOptions = { cwd };
20
+
21
+ // Configure git
22
+ await exec.exec('git', ['config', '--global', 'user.name', 'faf-taf-git[bot]'], execOptions);
23
+ await exec.exec('git', ['config', '--global', 'user.email', 'faf-taf-git[bot]@users.noreply.github.com'], execOptions);
24
+
25
+ // Stage .taf file
26
+ await exec.exec('git', ['add', '.taf'], execOptions);
27
+
28
+ // Check if there are changes to commit
29
+ const { exitCode } = await exec.getExecOutput('git', ['diff', '--cached', '--name-only'], execOptions);
30
+
31
+ if (exitCode === 0) {
32
+ await exec.exec('git', ['commit', '-m', message], execOptions);
33
+
34
+ // Try to push (might fail in some environments, that's ok)
35
+ try {
36
+ await exec.exec('git', ['push'], execOptions);
37
+ } catch (error) {
38
+ core.warning('Could not push changes (this is ok in some CI environments)');
39
+ }
40
+ }
41
+ }
42
+
43
+ async function run(): Promise<void> {
44
+ try {
45
+ // Get GitHub Action inputs
46
+ const testOutputFile = core.getInput('test-output-file', { required: true });
47
+ const autoCommit = core.getInput('auto-commit') !== 'false';
48
+ const commitMessage = core.getInput('commit-message') || 'chore(taf): update .taf receipt [skip ci]';
49
+ const cwd = process.cwd();
50
+
51
+ core.info(`Reading test output from: ${testOutputFile}`);
52
+
53
+ // Read test output file (written by previous CI step)
54
+ const testOutputPath = path.isAbsolute(testOutputFile)
55
+ ? testOutputFile
56
+ : path.join(cwd, testOutputFile);
57
+
58
+ if (!fs.existsSync(testOutputPath)) {
59
+ core.setFailed(`Test output file not found: ${testOutputPath}`);
60
+ return;
61
+ }
62
+
63
+ const testOutput = fs.readFileSync(testOutputPath, 'utf-8');
64
+ core.info(`Read ${testOutput.length} bytes from test output file`);
65
+
66
+ // Debug: Show sample of what we're parsing
67
+ const lines = testOutput.split('\n');
68
+ const testSummaryLine = lines.find(line => line.includes('Tests:'));
69
+ if (testSummaryLine) {
70
+ core.info(`Found summary line: ${testSummaryLine.substring(0, 200)}`);
71
+ } else {
72
+ core.warning('No "Tests:" summary line found in output');
73
+ // Show last 20 lines to help debug
74
+ core.info('Last 20 lines of output:');
75
+ lines.slice(-20).forEach((line, i) => {
76
+ core.info(` ${i}: ${line.substring(0, 150)}`);
77
+ });
78
+ }
79
+
80
+ // Parse test results
81
+ const testResults = parseJestOutput(testOutput);
82
+
83
+ if (!testResults) {
84
+ core.setFailed('Could not parse test output. Ensure you are using Jest.');
85
+ return;
86
+ }
87
+
88
+ core.info(`Parsed results: ${testResults.passed}/${testResults.total} tests passing`);
89
+
90
+ // Set GitHub Action outputs
91
+ core.setOutput('result', testResults.result);
92
+ core.setOutput('passed', testResults.passed.toString());
93
+ core.setOutput('failed', testResults.failed.toString());
94
+ core.setOutput('total', testResults.total.toString());
95
+
96
+ // Check if .taf file exists
97
+ const tafPath = path.join(cwd, '.taf');
98
+ if (!fs.existsSync(tafPath)) {
99
+ core.setFailed('No .taf file found. Run `faf taf init` to create one.');
100
+ return;
101
+ }
102
+
103
+ // Update .taf file
104
+ const updated = await updateTafFile(tafPath, testResults);
105
+
106
+ if (!updated) {
107
+ core.setOutput('taf-updated', 'false');
108
+ core.setFailed('.taf file was not updated');
109
+ return;
110
+ }
111
+
112
+ core.setOutput('taf-updated', 'true');
113
+ core.info('✅ .taf file updated successfully');
114
+
115
+ // Commit changes if enabled
116
+ if (autoCommit) {
117
+ await commitTafUpdate(cwd, commitMessage);
118
+ core.info('✅ Changes committed to git');
119
+ }
120
+
121
+ } catch (error) {
122
+ if (error instanceof Error) {
123
+ core.setFailed(error.message);
124
+ } else {
125
+ core.setFailed('Unknown error occurred');
126
+ }
127
+ }
128
+ }
129
+
130
+ // Run the GitHub Action
131
+ run();
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Jest output parser
3
+ *
4
+ * Extracts test counts from Jest CLI output
5
+ */
6
+
7
+ export interface TestResults {
8
+ total: number;
9
+ passed: number;
10
+ failed: number;
11
+ skipped?: number;
12
+ result: 'PASSED' | 'FAILED' | 'IMPROVED' | 'DEGRADED';
13
+ }
14
+
15
+ /**
16
+ * Parse Jest output to extract test results
17
+ *
18
+ * Jest output formats:
19
+ * - "Tests: 1 failed, 172 passed, 173 total"
20
+ * - "Tests: 173 passed, 173 total"
21
+ * - "Test Suites: 2 failed, 16 passed, 18 total"
22
+ * - "Tests: 9 skipped, 799 passed, 808 total" (with padding)
23
+ */
24
+ export function parseJestOutput(output: string): TestResults | null {
25
+ // Strip ANSI color codes and other control characters
26
+ // eslint-disable-next-line no-control-regex
27
+ let cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
28
+ // Also strip carriage returns and normalize line endings
29
+ cleanOutput = cleanOutput.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
30
+
31
+ // Find the Jest test summary line
32
+ // CRITICAL: Must match "Tests:" line that contains "total" to avoid matching
33
+ // test descriptions like "Brake Tests: Critical dogfooding scenarios"
34
+ const testSummaryPattern = /Tests:\s+(.+?\d+\s+total)/im;
35
+ const testLineMatch = cleanOutput.match(testSummaryPattern);
36
+
37
+ if (!testLineMatch || !testLineMatch[1]) {
38
+ return null;
39
+ }
40
+
41
+ const testLine = testLineMatch[1];
42
+
43
+ // Extract counts using flexible patterns
44
+ let total = 0;
45
+ let passed = 0;
46
+ let failed = 0;
47
+ let skipped = 0;
48
+
49
+ // Parse numbers - use global search on the whole output near the Tests: line
50
+ const totalMatch = testLine.match(/(\d+)\s+total/i);
51
+ if (totalMatch) {
52
+ total = parseInt(totalMatch[1], 10);
53
+ }
54
+
55
+ const passedMatch = testLine.match(/(\d+)\s+passed/i);
56
+ if (passedMatch) {
57
+ passed = parseInt(passedMatch[1], 10);
58
+ }
59
+
60
+ const failedMatch = testLine.match(/(\d+)\s+failed/i);
61
+ if (failedMatch) {
62
+ failed = parseInt(failedMatch[1], 10);
63
+ }
64
+
65
+ const skippedMatch = testLine.match(/(\d+)\s+skipped/i);
66
+ if (skippedMatch) {
67
+ skipped = parseInt(skippedMatch[1], 10);
68
+ }
69
+
70
+ // Validate
71
+ if (total === 0) {
72
+ return null;
73
+ }
74
+
75
+ // Determine result
76
+ let result: 'PASSED' | 'FAILED' | 'IMPROVED' | 'DEGRADED' = 'PASSED';
77
+ if (failed > 0) {
78
+ result = 'FAILED';
79
+ }
80
+
81
+ return {
82
+ total,
83
+ passed,
84
+ failed,
85
+ skipped: skipped > 0 ? skipped : undefined,
86
+ result,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Example Jest outputs for testing:
92
+ *
93
+ * All passing:
94
+ * "Tests: 173 passed, 173 total"
95
+ *
96
+ * Some failing:
97
+ * "Tests: 1 failed, 172 passed, 173 total"
98
+ *
99
+ * With skipped:
100
+ * "Tests: 1 failed, 2 skipped, 170 passed, 173 total"
101
+ *
102
+ * With padding (CI):
103
+ * "Tests: 9 skipped, 799 passed, 808 total"
104
+ */
@@ -0,0 +1,139 @@
1
+ /**
2
+ * TAF Core - Simplified for GitHub Action
3
+ *
4
+ * Pure functions for reading, parsing, and updating .taf files
5
+ * Derived from faf-cli TAF implementation (MCP-portable)
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as yaml from 'yaml';
10
+ import { TestResults } from './parsers/jest';
11
+
12
+ interface TAFFile {
13
+ format_version?: string;
14
+ project?: string;
15
+ created?: string;
16
+ last_updated?: string;
17
+ test_history: TestRun[];
18
+ [key: string]: any; // Allow other fields
19
+ }
20
+
21
+ interface TestRun {
22
+ timestamp: string;
23
+ result: 'PASSED' | 'FAILED' | 'IMPROVED' | 'DEGRADED';
24
+ tests: {
25
+ total: number;
26
+ passed: number;
27
+ failed: number;
28
+ skipped?: number;
29
+ };
30
+ command?: string;
31
+ trigger?: string;
32
+ }
33
+
34
+ /**
35
+ * Update .taf file with new test results
36
+ * Creates the file if it doesn't exist
37
+ */
38
+ export function updateTafFile(
39
+ tafPath: string,
40
+ testResults: TestResults
41
+ ): boolean {
42
+ try {
43
+ let taf: TAFFile;
44
+
45
+ // Check if .taf exists, create if not
46
+ if (fs.existsSync(tafPath)) {
47
+ const content = fs.readFileSync(tafPath, 'utf-8');
48
+ taf = parseTAF(content);
49
+ } else {
50
+ // Create new .taf file
51
+ const projectName = require('path').basename(require('path').dirname(tafPath));
52
+ taf = createTAF(projectName);
53
+ }
54
+
55
+ // Create test run entry
56
+ const run: TestRun = {
57
+ timestamp: new Date().toISOString(),
58
+ result: testResults.result,
59
+ tests: {
60
+ total: testResults.total,
61
+ passed: testResults.passed,
62
+ failed: testResults.failed,
63
+ skipped: testResults.skipped,
64
+ },
65
+ trigger: 'github-actions',
66
+ };
67
+
68
+ // Append to history
69
+ taf.test_history.push(run);
70
+
71
+ // Serialize and write back
72
+ const updated = serializeTAF(taf);
73
+ fs.writeFileSync(tafPath, updated, 'utf-8');
74
+
75
+ return true;
76
+ } catch (error) {
77
+ console.error('Failed to update .taf file:', error);
78
+ return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Create a new TAF file structure
84
+ */
85
+ function createTAF(projectName: string): TAFFile {
86
+ return {
87
+ format_version: '1.0.0',
88
+ project: projectName,
89
+ created: new Date().toISOString(),
90
+ last_updated: new Date().toISOString(),
91
+ test_history: [],
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Parse .taf file content
97
+ */
98
+ function parseTAF(content: string): TAFFile {
99
+ // Split on document separator and take only first document
100
+ const lines = content.split('\n');
101
+ const yamlLines: string[] = [];
102
+
103
+ for (const line of lines) {
104
+ // Stop at document separator (but skip initial one if present)
105
+ if (line.trim() === '---' && yamlLines.length > 0) {
106
+ break;
107
+ }
108
+ // Skip initial document separator
109
+ if (line.trim() === '---' && yamlLines.length === 0) {
110
+ continue;
111
+ }
112
+ yamlLines.push(line);
113
+ }
114
+
115
+ const parsed = yaml.parse(yamlLines.join('\n'));
116
+
117
+ // Basic validation
118
+ if (!parsed || typeof parsed !== 'object') {
119
+ throw new Error('Invalid .taf file: not a valid YAML object');
120
+ }
121
+
122
+ if (!parsed.test_history || !Array.isArray(parsed.test_history)) {
123
+ throw new Error('Invalid .taf file: missing or invalid test_history');
124
+ }
125
+
126
+ return parsed as TAFFile;
127
+ }
128
+
129
+ /**
130
+ * Serialize TAF object back to YAML
131
+ * Uses yaml library to maintain existing format
132
+ */
133
+ function serializeTAF(taf: TAFFile): string {
134
+ // Update last_updated timestamp
135
+ taf.last_updated = new Date().toISOString();
136
+
137
+ // Use yaml.stringify to preserve format
138
+ return yaml.stringify(taf);
139
+ }
package/test-mcp.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Test action with claude-faf-mcp repository
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { parseJestOutput } from './src/parsers/jest';
8
+ import { updateTafFile } from './src/taf-core';
9
+
10
+ async function testMCP() {
11
+ console.log('Testing action with claude-faf-mcp repo...\n');
12
+
13
+ // Simulate Jest output (based on typical MCP test output)
14
+ const jestOutput = `
15
+ PASS tests/tools.test.ts
16
+ PASS tests/validation.test.ts
17
+
18
+ Test Suites: 2 passed, 2 total
19
+ Tests: 15 passed, 15 total
20
+ Snapshots: 0 total
21
+ Time: 1.234 s
22
+ `;
23
+
24
+ console.log('1. Parsing Jest output...');
25
+ const testResults = parseJestOutput(jestOutput);
26
+
27
+ if (!testResults) {
28
+ console.error('❌ Failed to parse Jest output');
29
+ process.exit(1);
30
+ }
31
+
32
+ console.log('✅ Parsed successfully:');
33
+ console.log(` - Total: ${testResults.total}`);
34
+ console.log(` - Passed: ${testResults.passed}`);
35
+ console.log(` - Failed: ${testResults.failed}`);
36
+ console.log(` - Result: ${testResults.result}\n`);
37
+
38
+ // Test with MCP .taf file
39
+ const tafPath = path.join('/Users/wolfejam/FAF/claude-faf-mcp', '.taf');
40
+
41
+ console.log('2. Checking .taf file...');
42
+ if (!fs.existsSync(tafPath)) {
43
+ console.error('❌ No .taf file found at:', tafPath);
44
+ process.exit(1);
45
+ }
46
+ console.log('✅ Found .taf file\n');
47
+
48
+ console.log('3. Reading current .taf file...');
49
+ const beforeContent = fs.readFileSync(tafPath, 'utf-8');
50
+ const beforeLines = beforeContent.split('\n').filter(line => line.includes('timestamp')).length;
51
+ console.log(`✅ Current test runs: ${beforeLines}\n`);
52
+
53
+ console.log('4. Updating .taf file...');
54
+ const updated = await updateTafFile(tafPath, testResults);
55
+
56
+ if (!updated) {
57
+ console.error('❌ Failed to update .taf file');
58
+ process.exit(1);
59
+ }
60
+ console.log('✅ .taf file updated\n');
61
+
62
+ console.log('5. Verifying update...');
63
+ const afterContent = fs.readFileSync(tafPath, 'utf-8');
64
+ const afterLines = afterContent.split('\n').filter(line => line.includes('timestamp')).length;
65
+ console.log(`✅ Test runs after update: ${afterLines}`);
66
+
67
+ if (afterLines !== beforeLines + 1) {
68
+ console.error('❌ Expected one additional run');
69
+ process.exit(1);
70
+ }
71
+
72
+ console.log('\n✅ MCP test PASSED');
73
+ console.log('\nThe action successfully:');
74
+ console.log(' - Parsed Jest output (15/15 tests)');
75
+ console.log(' - Updated .taf file');
76
+ console.log(' - Added new test run entry');
77
+ console.log(' - Maintained proper format');
78
+ }
79
+
80
+ testMCP().catch(error => {
81
+ console.error('❌ Test failed:', error);
82
+ process.exit(1);
83
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Standalone test script to simulate the action
3
+ * Tests the action logic without GitHub Actions environment
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { parseJestOutput } from './src/parsers/jest';
9
+ import { updateTafFile } from './src/taf-core';
10
+
11
+ async function testAction() {
12
+ console.log('Starting standalone action test...\n');
13
+
14
+ // Simulate Jest output from faf-cli
15
+ const jestOutput = `
16
+ PASS src/commands/taf-init.test.ts
17
+ PASS src/commands/taf-log.test.ts
18
+ PASS src/commands/taf-validate.test.ts
19
+ PASS src/commands/taf-stats.test.ts
20
+
21
+ Test Suites: 18 passed, 18 total
22
+ Tests: 173 passed, 173 total
23
+ Snapshots: 0 total
24
+ Time: 2.512 s
25
+ Ran all test suites.
26
+ `;
27
+
28
+ console.log('1. Parsing Jest output...');
29
+ const testResults = parseJestOutput(jestOutput);
30
+
31
+ if (!testResults) {
32
+ console.error('❌ Failed to parse Jest output');
33
+ process.exit(1);
34
+ }
35
+
36
+ console.log('✅ Parsed successfully:');
37
+ console.log(` - Total: ${testResults.total}`);
38
+ console.log(` - Passed: ${testResults.passed}`);
39
+ console.log(` - Failed: ${testResults.failed}`);
40
+ console.log(` - Result: ${testResults.result}\n`);
41
+
42
+ // Test with faf-cli .taf file
43
+ const tafPath = path.join('/Users/wolfejam/FAF/cli', '.taf');
44
+
45
+ console.log('2. Checking .taf file...');
46
+ if (!fs.existsSync(tafPath)) {
47
+ console.error('❌ No .taf file found at:', tafPath);
48
+ process.exit(1);
49
+ }
50
+ console.log('✅ Found .taf file\n');
51
+
52
+ console.log('3. Reading current .taf file...');
53
+ const beforeContent = fs.readFileSync(tafPath, 'utf-8');
54
+ const beforeLines = beforeContent.split('\n').filter(line => line.includes('timestamp')).length;
55
+ console.log(`✅ Current test runs: ${beforeLines}\n`);
56
+
57
+ console.log('4. Updating .taf file...');
58
+ const updated = await updateTafFile(tafPath, testResults);
59
+
60
+ if (!updated) {
61
+ console.error('❌ Failed to update .taf file');
62
+ process.exit(1);
63
+ }
64
+ console.log('✅ .taf file updated\n');
65
+
66
+ console.log('5. Verifying update...');
67
+ const afterContent = fs.readFileSync(tafPath, 'utf-8');
68
+ const afterLines = afterContent.split('\n').filter(line => line.includes('timestamp')).length;
69
+ console.log(`✅ Test runs after update: ${afterLines}`);
70
+
71
+ if (afterLines !== beforeLines + 1) {
72
+ console.error('❌ Expected one additional run');
73
+ process.exit(1);
74
+ }
75
+
76
+ console.log('\n✅ Standalone test PASSED');
77
+ console.log('\nThe action successfully:');
78
+ console.log(' - Parsed Jest output (173/173 tests)');
79
+ console.log(' - Updated .taf file');
80
+ console.log(' - Added new test run entry');
81
+ console.log(' - Maintained proper format');
82
+ }
83
+
84
+ testAction().catch(error => {
85
+ console.error('❌ Test failed:', error);
86
+ process.exit(1);
87
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Tests for Jest output parser
3
+ */
4
+
5
+ import { parseJestOutput } from '../src/parsers/jest';
6
+
7
+ describe('Jest Parser', () => {
8
+ describe('parseJestOutput', () => {
9
+ it('should parse all passing tests', () => {
10
+ const output = `
11
+ Test Suites: 18 passed, 18 total
12
+ Tests: 173 passed, 173 total
13
+ Snapshots: 0 total
14
+ Time: 2.5s
15
+ `;
16
+
17
+ const result = parseJestOutput(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?.result).toBe('PASSED');
24
+ });
25
+
26
+ it('should parse with failed tests', () => {
27
+ const output = `
28
+ Test Suites: 1 failed, 17 passed, 18 total
29
+ Tests: 1 failed, 172 passed, 173 total
30
+ Snapshots: 0 total
31
+ Time: 2.5s
32
+ `;
33
+
34
+ const result = parseJestOutput(output);
35
+
36
+ expect(result).not.toBeNull();
37
+ expect(result?.total).toBe(173);
38
+ expect(result?.passed).toBe(172);
39
+ expect(result?.failed).toBe(1);
40
+ expect(result?.result).toBe('FAILED');
41
+ });
42
+
43
+ it('should parse with skipped tests', () => {
44
+ const output = `
45
+ Test Suites: 18 passed, 18 total
46
+ Tests: 2 skipped, 171 passed, 173 total
47
+ Snapshots: 0 total
48
+ Time: 2.5s
49
+ `;
50
+
51
+ const result = parseJestOutput(output);
52
+
53
+ expect(result).not.toBeNull();
54
+ expect(result?.total).toBe(173);
55
+ expect(result?.passed).toBe(171);
56
+ expect(result?.failed).toBe(0);
57
+ expect(result?.skipped).toBe(2);
58
+ expect(result?.result).toBe('PASSED');
59
+ });
60
+
61
+ it('should parse with mixed results', () => {
62
+ const output = `
63
+ Test Suites: 2 failed, 16 passed, 18 total
64
+ Tests: 5 failed, 3 skipped, 165 passed, 173 total
65
+ Snapshots: 0 total
66
+ Time: 2.5s
67
+ `;
68
+
69
+ const result = parseJestOutput(output);
70
+
71
+ expect(result).not.toBeNull();
72
+ expect(result?.total).toBe(173);
73
+ expect(result?.passed).toBe(165);
74
+ expect(result?.failed).toBe(5);
75
+ expect(result?.skipped).toBe(3);
76
+ expect(result?.result).toBe('FAILED');
77
+ });
78
+
79
+ it('should handle output without test line', () => {
80
+ const output = `
81
+ Some random output
82
+ No test results here
83
+ `;
84
+
85
+ const result = parseJestOutput(output);
86
+
87
+ expect(result).toBeNull();
88
+ });
89
+
90
+ it('should handle malformed output', () => {
91
+ const output = `
92
+ Tests: invalid data
93
+ `;
94
+
95
+ const result = parseJestOutput(output);
96
+
97
+ expect(result).toBeNull();
98
+ });
99
+
100
+ it('should handle real CLI output with colors', () => {
101
+ const output = `
102
+ PASS src/commands/taf-init.test.ts
103
+ PASS src/commands/taf-log.test.ts
104
+ PASS src/commands/taf-validate.test.ts
105
+
106
+ Test Suites: 18 passed, 18 total
107
+ Tests: 173 passed, 173 total
108
+ Snapshots: 0 total
109
+ Time: 2.512 s
110
+ Ran all test suites.
111
+ `;
112
+
113
+ const result = parseJestOutput(output);
114
+
115
+ expect(result).not.toBeNull();
116
+ expect(result?.total).toBe(173);
117
+ expect(result?.passed).toBe(173);
118
+ expect(result?.failed).toBe(0);
119
+ expect(result?.result).toBe('PASSED');
120
+ });
121
+ });
122
+ });