From 0a43185510ce0b0dd0c4293cb5a2bba56c852253 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 26 May 2026 11:23:29 -0400 Subject: [PATCH] test: add typed failure classification module (#333) --- scripts/test-failure-reasons.cjs | 34 ++++++++++++++++++++++++++ tests/test-failure-reasons.test.cjs | 37 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 scripts/test-failure-reasons.cjs create mode 100644 tests/test-failure-reasons.test.cjs diff --git a/scripts/test-failure-reasons.cjs b/scripts/test-failure-reasons.cjs new file mode 100644 index 000000000..22b411537 --- /dev/null +++ b/scripts/test-failure-reasons.cjs @@ -0,0 +1,34 @@ +'use strict'; + +const TEST_GATE_REASON = Object.freeze({ + PASS: 'pass', + TEST_FAILURE: 'test_failure', + INFRA_FAILURE: 'infra_failure', + UNKNOWN_FAILURE: 'unknown_failure', +}); + +/** + * Classify gsd-test style output + exit code into a typed reason. + * @param {{exitCode:number, output?:string}} input + * @returns {{ok:boolean, reason:string}} + */ +function classifyTestGateResult(input) { + const exitCode = Number(input?.exitCode ?? 0); + const output = String(input?.output ?? ''); + + if (exitCode === 0) return { ok: true, reason: TEST_GATE_REASON.PASS }; + + // gsd-test/gsd-test-summary infrastructure class (exit 2) + if (exitCode === 2 || /infrastructure failure|worktree\.Construct/i.test(output)) { + return { ok: false, reason: TEST_GATE_REASON.INFRA_FAILURE }; + } + + // test failures (non-zero plus explicit FAIL lines) + if (/\bFAIL\b|\d+\s+failures?\)|failed\)/i.test(output)) { + return { ok: false, reason: TEST_GATE_REASON.TEST_FAILURE }; + } + + return { ok: false, reason: TEST_GATE_REASON.UNKNOWN_FAILURE }; +} + +module.exports = { TEST_GATE_REASON, classifyTestGateResult }; diff --git a/tests/test-failure-reasons.test.cjs b/tests/test-failure-reasons.test.cjs new file mode 100644 index 000000000..1d81d8732 --- /dev/null +++ b/tests/test-failure-reasons.test.cjs @@ -0,0 +1,37 @@ +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); + +const { TEST_GATE_REASON, classifyTestGateResult } = require('../scripts/test-failure-reasons.cjs'); + +describe('test failure reason classification', () => { + test('classifies pass on exitCode 0', () => { + const out = classifyTestGateResult({ exitCode: 0, output: 'all good' }); + assert.deepStrictEqual(out, { ok: true, reason: TEST_GATE_REASON.PASS }); + }); + + test('classifies infrastructure failure via exitCode 2', () => { + const out = classifyTestGateResult({ exitCode: 2, output: 'ERROR: infrastructure failure' }); + assert.deepStrictEqual(out, { ok: false, reason: TEST_GATE_REASON.INFRA_FAILURE }); + }); + + test('classifies infrastructure failure via known worktree construct signature', () => { + const out = classifyTestGateResult({ + exitCode: 1, + output: 'worktree.Construct: worktree construction failed at merge', + }); + assert.deepStrictEqual(out, { ok: false, reason: TEST_GATE_REASON.INFRA_FAILURE }); + }); + + test('classifies test failure via FAIL markers', () => { + const out = classifyTestGateResult({ + exitCode: 1, + output: 'linux FAIL 11274/11277 tests (3 failures)', + }); + assert.deepStrictEqual(out, { ok: false, reason: TEST_GATE_REASON.TEST_FAILURE }); + }); + + test('classifies unknown failure when non-zero without known signatures', () => { + const out = classifyTestGateResult({ exitCode: 1, output: 'nonzero but unknown format' }); + assert.deepStrictEqual(out, { ok: false, reason: TEST_GATE_REASON.UNKNOWN_FAILURE }); + }); +});