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.
@@ -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;AAKrC,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAErC;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAElE"}
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"}
@@ -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
- * Parse test output from any supported framework.
14
- *
15
- * Order matters: Bun is tried first because its `Ran N tests across M files`
16
- * anchor is bun-unique — it never false-matches on jest/vitest output. Putting
17
- * it first also lets bun consumers ship truthful totals even when their CI
18
- * still has a legacy `Tests: N passed, N total` jest-shim line appended to the
19
- * test output (the bun parser ignores it and reads the canonical `Ran` line).
20
- *
21
- * Tries parsers in order: Bun, Jest, Vitest.
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
- return (0, bun_1.parseBunOutput)(output) || (0, jest_1.parseJestOutput)(output) || (0, vitest_1.parseVitestOutput)(output);
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;;AAoBH,0CAEC;AAnBD,+BAAuC;AACvC,iCAAyC;AACzC,qCAA6C;AAI7C;;;;;;;;;;GAUG;AACH,SAAgB,eAAe,CAAC,MAAc;IAC5C,OAAO,IAAA,oBAAc,EAAC,MAAM,CAAC,IAAI,IAAA,sBAAe,EAAC,MAAM,CAAC,IAAI,IAAA,0BAAiB,EAAC,MAAM,CAAC,CAAC;AACxF,CAAC"}
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":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;;AAwHH,4CAQC;AA5HD,SAAS,SAAS,CAAC,MAAc;IAC/B,4CAA4C;IAC5C,OAAO,MAAM;SACV,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC;SAC9B,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC;SACtB,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,SAAS,cAAc,CAAC,KAAa;IACnC,kDAAkD;IAClD,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IAED,8DAA8D;IAC9D,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;QACrB,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,SAAS;YAChD,IAAI,IAAI,CAAC,KAAK,KAAK,aAAa;gBAAE,SAAS;YAE3C,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjC,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACnC,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;gBAAE,SAAS;YACpD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAAE,SAAS;YAEnE,mEAAmE;YACnE,IAAI,CAAC,GAAG,MAAM,CAAC;YACf,IAAI,CAAC,GAAG,MAAM,CAAC;YACf,IAAI,CAAC,GAAG,KAAK,CAAC;YACd,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;gBACrC,IAAI,CAAC,KAAK,QAAQ,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;oBACnC,+DAA+D;oBAC/D,CAAC,GAAG,QAAQ,CAAC;oBACb,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,MAAM,CAAC;oBAC5E,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACZ,CAAC;YACH,CAAC;YAED,OAAO;gBACL,KAAK,EAAE,CAAC;gBACR,MAAM,EAAE,CAAC;gBACT,MAAM,EAAE,CAAC;gBACT,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;aACpC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,qBAAqB;QACvB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,SAAS,aAAa,CAAC,KAAa;IAClC,+EAA+E;IAC/E,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;IAC9D,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,iBAAiB,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACnD,MAAM,gBAAgB,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAClD,IAAI,gBAAgB,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,+EAA+E;IAC/E,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,MAAM,UAAU,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD,IAAI,KAAK,GAAG,gBAAgB,CAAC;IAC7B,IAAI,MAAM,GAAG,iBAAiB,CAAC;IAC/B,IAAI,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;IAE5B,IAAI,UAAU,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;QAC1C,IAAI,SAAS,KAAK,gBAAgB,EAAE,CAAC;YACnC,MAAM,GAAG,UAAU,CAAC;YACpB,MAAM,GAAG,UAAU,CAAC;YACpB,KAAK,GAAG,SAAS,CAAC;QACpB,CAAC;QACD,gFAAgF;QAChF,mCAAmC;QACnC,IAAI,MAAM,GAAG,CAAC;YAAE,MAAM,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO;QACL,KAAK;QACL,MAAM;QACN,MAAM;QACN,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;KACzC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAgB,gBAAgB,CAAC,MAAc;IAC7C,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAEhC,uDAAuD;IACvD,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACvC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faf-taf-git",
3
- "version": "2.2.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
@@ -182,7 +182,7 @@ resources:
182
182
  discussions: https://github.com/Wolfe-Jam/faf/discussions
183
183
  state:
184
184
  phase: production
185
- version: 2.1.1
185
+ version: 2.3.1
186
186
  focus: ecosystem_adoption
187
187
  status: production_ready
188
188
  achievements:
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: 'Could not parse test output. Supported: Jest, Vitest.',
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('Could not parse test output. Supported: Jest, Vitest.');
141
+ core.setFailed(UNPARSEABLE_OUTPUT);
142
142
  return;
143
143
  }
144
144
 
@@ -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
- * 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.
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
- return parseBunOutput(output) || parseJestOutput(output) || parseVitestOutput(output);
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
+ });