faf-taf-git 2.2.2 → 2.3.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 +4 -4
- package/.github/workflows/{claude-code-review.yml.disabled → claude-code-review.yml} +1 -1
- package/.github/workflows/claude.yml +1 -1
- package/CHANGELOG.md +20 -1
- package/README.md +7 -5
- package/RECEIPT-FORMAT.md +90 -0
- package/action.yml +2 -2
- package/dist/cli.js +3 -53
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1016 -346
- package/dist/index.js.map +1 -1
- package/dist/parsers/index.d.ts +6 -9
- package/dist/parsers/index.d.ts.map +1 -1
- package/dist/parsers/index.js +28 -10
- package/dist/parsers/index.js.map +1 -1
- package/dist/parsers/wjttc.d.ts +38 -0
- package/dist/parsers/wjttc.d.ts.map +1 -0
- package/dist/parsers/wjttc.js +152 -0
- package/dist/parsers/wjttc.js.map +1 -0
- package/package.json +4 -1
- package/project.faf +1 -1
- package/src/cli.ts +4 -54
- package/src/index.ts +2 -2
- package/src/parsers/index.ts +28 -10
- package/src/parsers/wjttc.ts +160 -0
- package/tests/action-bundle.test.ts +34 -0
- package/tests/action-parse-error.test.ts +47 -0
- package/tests/wjttc-parser.test.ts +116 -0
package/dist/parsers/index.d.ts
CHANGED
|
@@ -5,16 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { TestResults } from './jest';
|
|
7
7
|
export { TestResults } from './jest';
|
|
8
|
+
export { parseWjttcOutput } from './wjttc';
|
|
9
|
+
export declare const SUPPORTED_PARSERS: readonly string[];
|
|
10
|
+
/** What the CLI and the Action report when no parser matches. */
|
|
11
|
+
export declare const UNPARSEABLE_OUTPUT: string;
|
|
8
12
|
/**
|
|
9
|
-
* Parse test output from any supported framework
|
|
10
|
-
*
|
|
11
|
-
* Order matters: Bun is tried first because its `Ran N tests across M files`
|
|
12
|
-
* anchor is bun-unique — it never false-matches on jest/vitest output. Putting
|
|
13
|
-
* it first also lets bun consumers ship truthful totals even when their CI
|
|
14
|
-
* still has a legacy `Tests: N passed, N total` jest-shim line appended to the
|
|
15
|
-
* test output (the bun parser ignores it and reads the canonical `Ran` line).
|
|
16
|
-
*
|
|
17
|
-
* Tries parsers in order: Bun, Jest, Vitest.
|
|
13
|
+
* Parse test output from any supported framework: the first parser in
|
|
14
|
+
* PARSERS that recognises the output wins.
|
|
18
15
|
*/
|
|
19
16
|
export declare function parseTestOutput(output: string): TestResults | null;
|
|
20
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["file:///Users/wolfejam/FAF/faf-taf-git/src/parsers/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["file:///Users/wolfejam/FAF/faf-taf-git/src/parsers/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAMrC,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAgB3C,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAkC,CAAC;AAElF,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,QAA6E,CAAC;AAE7G;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAMlE"}
|
package/dist/parsers/index.js
CHANGED
|
@@ -5,22 +5,40 @@
|
|
|
5
5
|
* Tries each framework parser in order until one succeeds.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.UNPARSEABLE_OUTPUT = exports.SUPPORTED_PARSERS = exports.parseWjttcOutput = void 0;
|
|
8
9
|
exports.parseTestOutput = parseTestOutput;
|
|
9
10
|
const bun_1 = require("./bun");
|
|
10
11
|
const jest_1 = require("./jest");
|
|
11
12
|
const vitest_1 = require("./vitest");
|
|
13
|
+
const wjttc_1 = require("./wjttc");
|
|
14
|
+
var wjttc_2 = require("./wjttc");
|
|
15
|
+
Object.defineProperty(exports, "parseWjttcOutput", { enumerable: true, get: function () { return wjttc_2.parseWjttcOutput; } });
|
|
12
16
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
* Every parser, in the order parseTestOutput tries them. Order matters:
|
|
18
|
+
* 1. Bun — `Ran N tests across M files` is bun-unique.
|
|
19
|
+
* 2. WJTTC — `pc-ai WJTTC` / suite `pc-ai-wjttc` (FAF bar; not Jest-shaped).
|
|
20
|
+
* 3. Jest — `Tests: … total`.
|
|
21
|
+
* 4. Vitest — `Tests … (N)`.
|
|
22
|
+
*/
|
|
23
|
+
const PARSERS = [
|
|
24
|
+
['Bun', bun_1.parseBunOutput],
|
|
25
|
+
['WJTTC', wjttc_1.parseWjttcOutput],
|
|
26
|
+
['Jest', jest_1.parseJestOutput],
|
|
27
|
+
['Vitest', vitest_1.parseVitestOutput],
|
|
28
|
+
];
|
|
29
|
+
exports.SUPPORTED_PARSERS = PARSERS.map(([name]) => name);
|
|
30
|
+
/** What the CLI and the Action report when no parser matches. */
|
|
31
|
+
exports.UNPARSEABLE_OUTPUT = `Could not parse test output. Supported: ${exports.SUPPORTED_PARSERS.join(', ')}.`;
|
|
32
|
+
/**
|
|
33
|
+
* Parse test output from any supported framework: the first parser in
|
|
34
|
+
* PARSERS that recognises the output wins.
|
|
22
35
|
*/
|
|
23
36
|
function parseTestOutput(output) {
|
|
24
|
-
|
|
37
|
+
for (const [, parse] of PARSERS) {
|
|
38
|
+
const results = parse(output);
|
|
39
|
+
if (results)
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
25
43
|
}
|
|
26
44
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parsers/index.ts"],"names":[],"mappings":";AAAA;;;;GAIG
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parsers/index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAkCH,0CAMC;AArCD,+BAAuC;AACvC,iCAAyC;AACzC,qCAA6C;AAC7C,mCAA2C;AAG3C,iCAA2C;AAAlC,yGAAA,gBAAgB,OAAA;AAEzB;;;;;;GAMG;AACH,MAAM,OAAO,GAA0F;IACrG,CAAC,KAAK,EAAE,oBAAc,CAAC;IACvB,CAAC,OAAO,EAAE,wBAAgB,CAAC;IAC3B,CAAC,MAAM,EAAE,sBAAe,CAAC;IACzB,CAAC,QAAQ,EAAE,0BAAiB,CAAC;CAC9B,CAAC;AAEW,QAAA,iBAAiB,GAAsB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AAElF,iEAAiE;AACpD,QAAA,kBAAkB,GAAG,2CAA2C,yBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAE7G;;;GAGG;AACH,SAAgB,eAAe,CAAC,MAAc;IAC5C,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC9B,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WJTTC output parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts test counts from the FAF / pc-ai WJTTC bar runner
|
|
5
|
+
* (tests/wjttc/run-wjttc.mjs and sibling suite runners).
|
|
6
|
+
*
|
|
7
|
+
* Two shapes:
|
|
8
|
+
*
|
|
9
|
+
* 1) Human log (default):
|
|
10
|
+
* ```
|
|
11
|
+
* pc-ai WJTTC
|
|
12
|
+
* ----------
|
|
13
|
+
* PASS VAL-01 valid-simple portfolio accepts
|
|
14
|
+
* FAIL OPT-01 …
|
|
15
|
+
* ----------
|
|
16
|
+
* 31/33 passed
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* 2) Machine JSON (`--json`):
|
|
20
|
+
* ```
|
|
21
|
+
* {
|
|
22
|
+
* "suite": "pc-ai-wjttc",
|
|
23
|
+
* "total": 33,
|
|
24
|
+
* "passed": 31,
|
|
25
|
+
* "failed": 2,
|
|
26
|
+
* "results": [ … ]
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Anchors are WJTTC-specific so this never false-matches Jest/Vitest/Bun.
|
|
31
|
+
* Prefer JSON when both appear in a teed stream (structured totals win).
|
|
32
|
+
*/
|
|
33
|
+
import { TestResults } from './jest';
|
|
34
|
+
/**
|
|
35
|
+
* Parse WJTTC (pc-ai bar) output to TestResults, or null if not WJTTC.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseWjttcOutput(output: string): TestResults | null;
|
|
38
|
+
//# sourceMappingURL=wjttc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wjttc.d.ts","sourceRoot":"","sources":["file:///Users/wolfejam/FAF/faf-taf-git/src/parsers/wjttc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAmHrC;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAQnE"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WJTTC output parser
|
|
4
|
+
*
|
|
5
|
+
* Extracts test counts from the FAF / pc-ai WJTTC bar runner
|
|
6
|
+
* (tests/wjttc/run-wjttc.mjs and sibling suite runners).
|
|
7
|
+
*
|
|
8
|
+
* Two shapes:
|
|
9
|
+
*
|
|
10
|
+
* 1) Human log (default):
|
|
11
|
+
* ```
|
|
12
|
+
* pc-ai WJTTC
|
|
13
|
+
* ----------
|
|
14
|
+
* PASS VAL-01 valid-simple portfolio accepts
|
|
15
|
+
* FAIL OPT-01 …
|
|
16
|
+
* ----------
|
|
17
|
+
* 31/33 passed
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* 2) Machine JSON (`--json`):
|
|
21
|
+
* ```
|
|
22
|
+
* {
|
|
23
|
+
* "suite": "pc-ai-wjttc",
|
|
24
|
+
* "total": 33,
|
|
25
|
+
* "passed": 31,
|
|
26
|
+
* "failed": 2,
|
|
27
|
+
* "results": [ … ]
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Anchors are WJTTC-specific so this never false-matches Jest/Vitest/Bun.
|
|
32
|
+
* Prefer JSON when both appear in a teed stream (structured totals win).
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.parseWjttcOutput = parseWjttcOutput;
|
|
36
|
+
function stripAnsi(output) {
|
|
37
|
+
// eslint-disable-next-line no-control-regex
|
|
38
|
+
return output
|
|
39
|
+
.replace(/\x1b\[[0-9;]*m/g, '')
|
|
40
|
+
.replace(/\r\n/g, '\n')
|
|
41
|
+
.replace(/\r/g, '\n');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Try to parse a WJTTC --json summary object from output.
|
|
45
|
+
* Handles pure JSON or JSON embedded after build noise in a tee file.
|
|
46
|
+
*/
|
|
47
|
+
function parseJsonSuite(clean) {
|
|
48
|
+
// Prefer a top-level object that names the suite.
|
|
49
|
+
const candidates = [];
|
|
50
|
+
const trimmed = clean.trim();
|
|
51
|
+
if (trimmed.startsWith('{')) {
|
|
52
|
+
candidates.push(trimmed);
|
|
53
|
+
}
|
|
54
|
+
// Last {...} block often is the suite when logs precede JSON.
|
|
55
|
+
const lastBrace = clean.lastIndexOf('{');
|
|
56
|
+
if (lastBrace !== -1) {
|
|
57
|
+
candidates.push(clean.slice(lastBrace));
|
|
58
|
+
}
|
|
59
|
+
for (const raw of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
const body = JSON.parse(raw);
|
|
62
|
+
if (!body || typeof body !== 'object')
|
|
63
|
+
continue;
|
|
64
|
+
if (body.suite !== 'pc-ai-wjttc')
|
|
65
|
+
continue;
|
|
66
|
+
const total = Number(body.total);
|
|
67
|
+
const passed = Number(body.passed);
|
|
68
|
+
const failed = Number(body.failed);
|
|
69
|
+
if (!Number.isFinite(total) || total <= 0)
|
|
70
|
+
continue;
|
|
71
|
+
if (!Number.isFinite(passed) || !Number.isFinite(failed))
|
|
72
|
+
continue;
|
|
73
|
+
// Prefer explicit fields; fall back to results[] length if needed.
|
|
74
|
+
let p = passed;
|
|
75
|
+
let f = failed;
|
|
76
|
+
let t = total;
|
|
77
|
+
if (Array.isArray(body.results) && body.results.length > 0) {
|
|
78
|
+
const fromRows = body.results.length;
|
|
79
|
+
if (t !== fromRows && fromRows > 0) {
|
|
80
|
+
// Trust results array as the executed set (partition honesty).
|
|
81
|
+
t = fromRows;
|
|
82
|
+
p = body.results.filter((r) => r && r.ok === true).length;
|
|
83
|
+
f = t - p;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
total: t,
|
|
88
|
+
passed: p,
|
|
89
|
+
failed: f,
|
|
90
|
+
result: f > 0 ? 'FAILED' : 'PASSED',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// try next candidate
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Human WJTTC log: header + optional PASS/FAIL lines + "N/N passed".
|
|
101
|
+
*/
|
|
102
|
+
function parseHumanLog(clean) {
|
|
103
|
+
// Require the suite banner so we do not steal "12/12 passed" from other tools.
|
|
104
|
+
if (!/pc-ai\s+WJTTC/i.test(clean)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const summary = clean.match(/(\d+)\s*\/\s*(\d+)\s+passed\b/i);
|
|
108
|
+
if (!summary)
|
|
109
|
+
return null;
|
|
110
|
+
const passedFromSummary = parseInt(summary[1], 10);
|
|
111
|
+
const totalFromSummary = parseInt(summary[2], 10);
|
|
112
|
+
if (totalFromSummary <= 0)
|
|
113
|
+
return null;
|
|
114
|
+
// Cross-check with PASS/FAIL lines when present (stronger than summary alone).
|
|
115
|
+
const passLines = clean.match(/^PASS\s+\S+/gm);
|
|
116
|
+
const failLines = clean.match(/^FAIL\s+\S+/gm);
|
|
117
|
+
const linePassed = passLines ? passLines.length : -1;
|
|
118
|
+
const lineFailed = failLines ? failLines.length : -1;
|
|
119
|
+
let total = totalFromSummary;
|
|
120
|
+
let passed = passedFromSummary;
|
|
121
|
+
let failed = total - passed;
|
|
122
|
+
if (linePassed >= 0 && lineFailed >= 0) {
|
|
123
|
+
const lineTotal = linePassed + lineFailed;
|
|
124
|
+
if (lineTotal === totalFromSummary) {
|
|
125
|
+
passed = linePassed;
|
|
126
|
+
failed = lineFailed;
|
|
127
|
+
total = lineTotal;
|
|
128
|
+
}
|
|
129
|
+
// If lines disagree with summary, still use summary (runner is source of truth)
|
|
130
|
+
// but failed must be non-negative.
|
|
131
|
+
if (failed < 0)
|
|
132
|
+
failed = 0;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
total,
|
|
136
|
+
passed,
|
|
137
|
+
failed,
|
|
138
|
+
result: failed > 0 ? 'FAILED' : 'PASSED',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse WJTTC (pc-ai bar) output to TestResults, or null if not WJTTC.
|
|
143
|
+
*/
|
|
144
|
+
function parseWjttcOutput(output) {
|
|
145
|
+
const clean = stripAnsi(output);
|
|
146
|
+
// JSON first — structured totals when --json was used.
|
|
147
|
+
const fromJson = parseJsonSuite(clean);
|
|
148
|
+
if (fromJson)
|
|
149
|
+
return fromJson;
|
|
150
|
+
return parseHumanLog(clean);
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=wjttc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wjttc.js","sourceRoot":"","sources":["../../src/parsers/wjttc.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "faf-taf-git",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "A Test Receipt Printer for git. Every CI run prints a receipt to .taf — append-only, timestamped, cannot be gamed. Proof over time.",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
"jest": "^29.7.0",
|
|
43
43
|
"ts-jest": "^29.1.1",
|
|
44
44
|
"typescript": "^5.3.3"
|
|
45
|
+
},
|
|
46
|
+
"overrides": {
|
|
47
|
+
"undici": "^6.28.0"
|
|
45
48
|
}
|
|
46
49
|
}
|
package/project.faf
CHANGED
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
-
import { parseTestOutput } from './parsers';
|
|
12
|
+
import { parseTestOutput, UNPARSEABLE_OUTPUT } from './parsers';
|
|
13
13
|
import { updateTafFile } from './taf-core';
|
|
14
14
|
import { generateBadge } from './badge';
|
|
15
15
|
|
|
@@ -81,7 +81,7 @@ export async function runTafGit(options: CLIOptions = {}): Promise<CLIResult> {
|
|
|
81
81
|
return {
|
|
82
82
|
success: false,
|
|
83
83
|
tafUpdated: false,
|
|
84
|
-
error:
|
|
84
|
+
error: UNPARSEABLE_OUTPUT,
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -182,18 +182,7 @@ function commitTafUpdate(cwd: string, message: string, verbose: boolean, logger:
|
|
|
182
182
|
*/
|
|
183
183
|
function handleBadgeCommand(args: string[]): void {
|
|
184
184
|
if (args.includes('--help') || args.includes('-h')) {
|
|
185
|
-
console.log(`
|
|
186
|
-
faf-taf-git badge - Generate SVG badge from .taf history
|
|
187
|
-
|
|
188
|
-
USAGE:
|
|
189
|
-
npx faf-taf-git badge [OPTIONS]
|
|
190
|
-
|
|
191
|
-
OPTIONS:
|
|
192
|
-
--output <path> Write SVG to file (default: stdout)
|
|
193
|
-
--label <text> Badge label (default: TAF)
|
|
194
|
-
--taf <path> Path to .taf file (default: .taf)
|
|
195
|
-
--help, -h Show this help
|
|
196
|
-
`);
|
|
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`);
|
|
197
186
|
process.exit(0);
|
|
198
187
|
}
|
|
199
188
|
|
|
@@ -247,46 +236,7 @@ async function main() {
|
|
|
247
236
|
|
|
248
237
|
// Show help
|
|
249
238
|
if (args.includes('--help') || args.includes('-h')) {
|
|
250
|
-
console.log(`
|
|
251
|
-
faf-taf-git - The Git-Native Receipt Printer
|
|
252
|
-
|
|
253
|
-
USAGE:
|
|
254
|
-
npm test 2>&1 | tee test-output.txt
|
|
255
|
-
npx faf-taf-git --file test-output.txt [OPTIONS]
|
|
256
|
-
|
|
257
|
-
COMMANDS:
|
|
258
|
-
badge Generate a shields.io-style SVG badge from .taf
|
|
259
|
-
|
|
260
|
-
OPTIONS:
|
|
261
|
-
--file <path> Path to test output file (required)
|
|
262
|
-
--commit Auto-commit .taf changes to git
|
|
263
|
-
--message <msg> Custom commit message
|
|
264
|
-
--cwd <dir> Working directory (default: current)
|
|
265
|
-
--verbose, -v Verbose output
|
|
266
|
-
--help, -h Show this help
|
|
267
|
-
|
|
268
|
-
EXAMPLES:
|
|
269
|
-
# Run tests, capture output, generate receipt
|
|
270
|
-
npm test 2>&1 | tee test-output.txt
|
|
271
|
-
npx faf-taf-git --file test-output.txt
|
|
272
|
-
|
|
273
|
-
# Auto-commit changes
|
|
274
|
-
npx faf-taf-git --file test-output.txt --commit
|
|
275
|
-
|
|
276
|
-
# Custom commit message
|
|
277
|
-
npx faf-taf-git --file test-output.txt --commit --message "test: update TAF"
|
|
278
|
-
|
|
279
|
-
# Generate badge
|
|
280
|
-
npx faf-taf-git badge --output badge.svg
|
|
281
|
-
|
|
282
|
-
PLATFORM SUPPORT:
|
|
283
|
-
Works in ANY CI/CD that runs Node.js:
|
|
284
|
-
GitHub Actions, GitLab CI, Jenkins, CircleCI,
|
|
285
|
-
Bitbucket Pipelines, Azure Pipelines, local dev
|
|
286
|
-
|
|
287
|
-
LEARN MORE:
|
|
288
|
-
https://github.com/Wolfe-Jam/faf-taf-git
|
|
289
|
-
`);
|
|
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 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`);
|
|
290
240
|
process.exit(0);
|
|
291
241
|
}
|
|
292
242
|
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import * as core from '@actions/core';
|
|
|
9
9
|
import * as exec from '@actions/exec';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
-
import { parseTestOutput } from './parsers';
|
|
12
|
+
import { parseTestOutput, UNPARSEABLE_OUTPUT } from './parsers';
|
|
13
13
|
import { updateTafFile } from './taf-core';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -138,7 +138,7 @@ async function run(): Promise<void> {
|
|
|
138
138
|
const testResults = parseTestOutput(testOutput);
|
|
139
139
|
|
|
140
140
|
if (!testResults) {
|
|
141
|
-
core.setFailed(
|
|
141
|
+
core.setFailed(UNPARSEABLE_OUTPUT);
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
144
|
|
package/src/parsers/index.ts
CHANGED
|
@@ -8,20 +8,38 @@ import { TestResults } from './jest';
|
|
|
8
8
|
import { parseBunOutput } from './bun';
|
|
9
9
|
import { parseJestOutput } from './jest';
|
|
10
10
|
import { parseVitestOutput } from './vitest';
|
|
11
|
+
import { parseWjttcOutput } from './wjttc';
|
|
11
12
|
|
|
12
13
|
export { TestResults } from './jest';
|
|
14
|
+
export { parseWjttcOutput } from './wjttc';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
* Every parser, in the order parseTestOutput tries them. Order matters:
|
|
18
|
+
* 1. Bun — `Ran N tests across M files` is bun-unique.
|
|
19
|
+
* 2. WJTTC — `pc-ai WJTTC` / suite `pc-ai-wjttc` (FAF bar; not Jest-shaped).
|
|
20
|
+
* 3. Jest — `Tests: … total`.
|
|
21
|
+
* 4. Vitest — `Tests … (N)`.
|
|
22
|
+
*/
|
|
23
|
+
const PARSERS: ReadonlyArray<readonly [name: string, parse: (output: string) => TestResults | null]> = [
|
|
24
|
+
['Bun', parseBunOutput],
|
|
25
|
+
['WJTTC', parseWjttcOutput],
|
|
26
|
+
['Jest', parseJestOutput],
|
|
27
|
+
['Vitest', parseVitestOutput],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const SUPPORTED_PARSERS: readonly string[] = PARSERS.map(([name]) => name);
|
|
31
|
+
|
|
32
|
+
/** What the CLI and the Action report when no parser matches. */
|
|
33
|
+
export const UNPARSEABLE_OUTPUT = `Could not parse test output. Supported: ${SUPPORTED_PARSERS.join(', ')}.`;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse test output from any supported framework: the first parser in
|
|
37
|
+
* PARSERS that recognises the output wins.
|
|
24
38
|
*/
|
|
25
39
|
export function parseTestOutput(output: string): TestResults | null {
|
|
26
|
-
|
|
40
|
+
for (const [, parse] of PARSERS) {
|
|
41
|
+
const results = parse(output);
|
|
42
|
+
if (results) return results;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
27
45
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WJTTC output parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts test counts from the FAF / pc-ai WJTTC bar runner
|
|
5
|
+
* (tests/wjttc/run-wjttc.mjs and sibling suite runners).
|
|
6
|
+
*
|
|
7
|
+
* Two shapes:
|
|
8
|
+
*
|
|
9
|
+
* 1) Human log (default):
|
|
10
|
+
* ```
|
|
11
|
+
* pc-ai WJTTC
|
|
12
|
+
* ----------
|
|
13
|
+
* PASS VAL-01 valid-simple portfolio accepts
|
|
14
|
+
* FAIL OPT-01 …
|
|
15
|
+
* ----------
|
|
16
|
+
* 31/33 passed
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* 2) Machine JSON (`--json`):
|
|
20
|
+
* ```
|
|
21
|
+
* {
|
|
22
|
+
* "suite": "pc-ai-wjttc",
|
|
23
|
+
* "total": 33,
|
|
24
|
+
* "passed": 31,
|
|
25
|
+
* "failed": 2,
|
|
26
|
+
* "results": [ … ]
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Anchors are WJTTC-specific so this never false-matches Jest/Vitest/Bun.
|
|
31
|
+
* Prefer JSON when both appear in a teed stream (structured totals win).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { TestResults } from './jest';
|
|
35
|
+
|
|
36
|
+
function stripAnsi(output: string): string {
|
|
37
|
+
// eslint-disable-next-line no-control-regex
|
|
38
|
+
return output
|
|
39
|
+
.replace(/\x1b\[[0-9;]*m/g, '')
|
|
40
|
+
.replace(/\r\n/g, '\n')
|
|
41
|
+
.replace(/\r/g, '\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Try to parse a WJTTC --json summary object from output.
|
|
46
|
+
* Handles pure JSON or JSON embedded after build noise in a tee file.
|
|
47
|
+
*/
|
|
48
|
+
function parseJsonSuite(clean: string): TestResults | null {
|
|
49
|
+
// Prefer a top-level object that names the suite.
|
|
50
|
+
const candidates: string[] = [];
|
|
51
|
+
|
|
52
|
+
const trimmed = clean.trim();
|
|
53
|
+
if (trimmed.startsWith('{')) {
|
|
54
|
+
candidates.push(trimmed);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Last {...} block often is the suite when logs precede JSON.
|
|
58
|
+
const lastBrace = clean.lastIndexOf('{');
|
|
59
|
+
if (lastBrace !== -1) {
|
|
60
|
+
candidates.push(clean.slice(lastBrace));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const raw of candidates) {
|
|
64
|
+
try {
|
|
65
|
+
const body = JSON.parse(raw);
|
|
66
|
+
if (!body || typeof body !== 'object') continue;
|
|
67
|
+
if (body.suite !== 'pc-ai-wjttc') continue;
|
|
68
|
+
|
|
69
|
+
const total = Number(body.total);
|
|
70
|
+
const passed = Number(body.passed);
|
|
71
|
+
const failed = Number(body.failed);
|
|
72
|
+
if (!Number.isFinite(total) || total <= 0) continue;
|
|
73
|
+
if (!Number.isFinite(passed) || !Number.isFinite(failed)) continue;
|
|
74
|
+
|
|
75
|
+
// Prefer explicit fields; fall back to results[] length if needed.
|
|
76
|
+
let p = passed;
|
|
77
|
+
let f = failed;
|
|
78
|
+
let t = total;
|
|
79
|
+
if (Array.isArray(body.results) && body.results.length > 0) {
|
|
80
|
+
const fromRows = body.results.length;
|
|
81
|
+
if (t !== fromRows && fromRows > 0) {
|
|
82
|
+
// Trust results array as the executed set (partition honesty).
|
|
83
|
+
t = fromRows;
|
|
84
|
+
p = body.results.filter((r: { ok?: boolean }) => r && r.ok === true).length;
|
|
85
|
+
f = t - p;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
total: t,
|
|
91
|
+
passed: p,
|
|
92
|
+
failed: f,
|
|
93
|
+
result: f > 0 ? 'FAILED' : 'PASSED',
|
|
94
|
+
};
|
|
95
|
+
} catch {
|
|
96
|
+
// try next candidate
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Human WJTTC log: header + optional PASS/FAIL lines + "N/N passed".
|
|
105
|
+
*/
|
|
106
|
+
function parseHumanLog(clean: string): TestResults | null {
|
|
107
|
+
// Require the suite banner so we do not steal "12/12 passed" from other tools.
|
|
108
|
+
if (!/pc-ai\s+WJTTC/i.test(clean)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const summary = clean.match(/(\d+)\s*\/\s*(\d+)\s+passed\b/i);
|
|
113
|
+
if (!summary) return null;
|
|
114
|
+
|
|
115
|
+
const passedFromSummary = parseInt(summary[1], 10);
|
|
116
|
+
const totalFromSummary = parseInt(summary[2], 10);
|
|
117
|
+
if (totalFromSummary <= 0) return null;
|
|
118
|
+
|
|
119
|
+
// Cross-check with PASS/FAIL lines when present (stronger than summary alone).
|
|
120
|
+
const passLines = clean.match(/^PASS\s+\S+/gm);
|
|
121
|
+
const failLines = clean.match(/^FAIL\s+\S+/gm);
|
|
122
|
+
const linePassed = passLines ? passLines.length : -1;
|
|
123
|
+
const lineFailed = failLines ? failLines.length : -1;
|
|
124
|
+
|
|
125
|
+
let total = totalFromSummary;
|
|
126
|
+
let passed = passedFromSummary;
|
|
127
|
+
let failed = total - passed;
|
|
128
|
+
|
|
129
|
+
if (linePassed >= 0 && lineFailed >= 0) {
|
|
130
|
+
const lineTotal = linePassed + lineFailed;
|
|
131
|
+
if (lineTotal === totalFromSummary) {
|
|
132
|
+
passed = linePassed;
|
|
133
|
+
failed = lineFailed;
|
|
134
|
+
total = lineTotal;
|
|
135
|
+
}
|
|
136
|
+
// If lines disagree with summary, still use summary (runner is source of truth)
|
|
137
|
+
// but failed must be non-negative.
|
|
138
|
+
if (failed < 0) failed = 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
total,
|
|
143
|
+
passed,
|
|
144
|
+
failed,
|
|
145
|
+
result: failed > 0 ? 'FAILED' : 'PASSED',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parse WJTTC (pc-ai bar) output to TestResults, or null if not WJTTC.
|
|
151
|
+
*/
|
|
152
|
+
export function parseWjttcOutput(output: string): TestResults | null {
|
|
153
|
+
const clean = stripAnsi(output);
|
|
154
|
+
|
|
155
|
+
// JSON first — structured totals when --json was used.
|
|
156
|
+
const fromJson = parseJsonSuite(clean);
|
|
157
|
+
if (fromJson) return fromJson;
|
|
158
|
+
|
|
159
|
+
return parseHumanLog(clean);
|
|
160
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built Action bundle — dist/index.js, what action.yml runs — loads and
|
|
3
|
+
* reaches the parse step under this Node (CI runs 24.x, the runtime
|
|
4
|
+
* action.yml declares).
|
|
5
|
+
*
|
|
6
|
+
* An open-ended `overrides.undici: ">=6.28.0"` resolved undici 8 (Node >=
|
|
7
|
+
* 22.19), which crashed the bundle at load on Node 20, before any input was
|
|
8
|
+
* read. The src-level tests never executed the bundle, so nothing caught it.
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from 'child_process';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as os from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { UNPARSEABLE_OUTPUT } from '../src/parsers';
|
|
15
|
+
|
|
16
|
+
describe('Action bundle (dist/index.js)', () => {
|
|
17
|
+
it('loads and fails an unparseable test output with the parser list', () => {
|
|
18
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'taf-bundle-'));
|
|
19
|
+
const file = path.join(dir, 'test-output.txt');
|
|
20
|
+
fs.writeFileSync(file, 'no test summary here\n');
|
|
21
|
+
|
|
22
|
+
// Actions pass inputs as INPUT_<NAME> env vars; stdout is captured here,
|
|
23
|
+
// so the ::error:: line never reaches a real runner's log.
|
|
24
|
+
const run = spawnSync(process.execPath, [path.join(__dirname, '..', 'dist', 'index.js')], {
|
|
25
|
+
env: { ...process.env, 'INPUT_TEST-OUTPUT-FILE': file },
|
|
26
|
+
encoding: 'utf-8',
|
|
27
|
+
});
|
|
28
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
|
|
30
|
+
expect(run.stderr).not.toMatch(/TypeError/);
|
|
31
|
+
expect(run.stdout).toContain(`::error::${UNPARSEABLE_OUTPUT}`);
|
|
32
|
+
expect(run.status).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
});
|