Skip to content

Commit 3c2da34

Browse files
authored
fix(github): tolerate test:fail events without a details.error (#175)
Node can emit `test:fail` where `details.error` is null — for example when a hook re-run resets `this.error` before reporting (see `lib/internal/test_runner/test.js`). The previous code dereferenced `error.code` unconditionally on line 90, throwing `TypeError: Cannot read properties of null (reading 'code')`. Because `transformEvent` runs inside a Transform stream (`SpecReporter._transform`), the synchronous throw surfaces as an unhandled `'error'` event on the stream, which crashes the whole `node --test` process. Any reporters wired alongside it (including the internal `rerun` reporter used by `--test-rerun-failures`) lose their chance to flush, leaving truncated output files. Skip annotation when there is no error object, matching the existing behavior for `subtestsFailed` wrappers.
1 parent 5637d57 commit 3c2da34

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

packages/github/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ function transformEvent(event) {
8282
return new Command('debug', {}, `completed running ${event.data.name}`).toString();
8383
case 'test:fail': {
8484
const error = event.data.details?.error;
85-
if (error?.code === 'ERR_TEST_FAILURE' && error?.failureType === 'subtestsFailed') {
86-
// This means the failed subtests are already reported
87-
// no need to re-annotate the file itself
85+
if (!error || (error.code === 'ERR_TEST_FAILURE' && error.failureType === 'subtestsFailed')) {
86+
// Either no error object on the event, or failed subtests are already
87+
// reported — no need to re-annotate the file itself.
8888
break;
8989
}
9090
const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error;

packages/github/tests/index.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
'use strict';
22

33
const { test, describe, beforeEach } = require('node:test');
4+
const assert = require('node:assert');
45
const { spawnSync } = require('child_process');
56
const { tmpdir } = require('os');
67
const { join } = require('path');
78
const path = require('path');
89
const { readFileSync, writeFileSync } = require('fs');
910
const { Snap, nodeMajor } = require('../../../tests/utils');
11+
const { transformEvent } = require('../index');
1012

1113
const snapshot = Snap(`${__filename}.${nodeMajor}`);
1214
const GITHUB_STEP_SUMMARY = join(tmpdir(), 'github-actions-test-reporter');
@@ -46,4 +48,30 @@ describe('github reporter', () => {
4648
const silentChild = spawnSync(process.execPath, ['--test-reporter', './index.js', '../../tests/example'], { env: { } });
4749
await snapshot(silentChild);
4850
});
51+
52+
test('transformEvent tolerates test:fail with no error object', () => {
53+
// Node can emit test:fail where details.error is null (e.g. hook re-runs
54+
// that reset this.error — see lib/internal/test_runner/test.js). The
55+
// reporter must not throw on such events, otherwise the unhandled 'error'
56+
// on the Transform stream crashes the whole test runner.
57+
assert.doesNotThrow(() => transformEvent({
58+
type: 'test:fail',
59+
data: {
60+
name: 'no error',
61+
details: { error: null },
62+
file: 'x.js',
63+
line: 1,
64+
column: 1,
65+
},
66+
}));
67+
assert.doesNotThrow(() => transformEvent({
68+
type: 'test:fail',
69+
data: {
70+
name: 'no details',
71+
file: 'x.js',
72+
line: 1,
73+
column: 1,
74+
},
75+
}));
76+
});
4977
});

0 commit comments

Comments
 (0)