Skip to content

Commit 7740e05

Browse files
NP-790: Harden bundle smoke test and document commitOpts coupling
Addresses ts-node-reviewer findings on the previous commit (4de84b0): * `runChildAndCapture` now listens for `error` on the child so a failed spawn (e.g. ENOENT on the node binary) folds the error into the captured stderr and resolves immediately instead of hanging until the SIGKILL timer fires. A `settled` guard protects against the close/error race. * The mock HTTP server's `listen` promise now attaches a one-shot `error` handler so a failed bind (EADDRINUSE on a constrained CI host, etc.) rejects with a useful message instead of letting the test silently wedge until Vitest's test timeout. * `src/action.ts` gains an explicit comment next to the preset pre- resolution explaining that upstream `load-changelog-config.js` never merges a plugin-supplied `commits` field, so `commitOpts` always comes from angular when no `preset` / `config` is passed. Harmless today (angular and conventionalcommits both default to `{ ignore: undefined, merges: false }`) but worth flagging for future upstream drift. No behavioural change to the shipping bundle — `esbuild` strips comments and the smoke test's runtime changes only affect error paths that previously caused hangs, not the happy path. All 37 tests across 5 files still pass; lint, prettier, and typecheck clean. Made-with: Cursor
1 parent 4de84b0 commit 7740e05

2 files changed

Lines changed: 47 additions & 10 deletions

File tree

src/action.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ export default async function main() {
216216
// `preset: 'conventionalcommits'` would trigger a dynamic
217217
// `import-from-esm` lookup that fails on the Actions runner
218218
// where no `node_modules` directory exists.
219+
//
220+
// Note: upstream `load-changelog-config.js` only reads
221+
// `loadedConfig.commits` (never merging a plugin-supplied `commits`
222+
// field), so when no `preset` / `config` is passed `commitOpts`
223+
// always comes from the angular default preset. That is harmless
224+
// today — angular and conventionalcommits both default to
225+
// `{ ignore: undefined, merges: false }` — but is worth knowing if
226+
// those defaults ever diverge upstream.
219227
const resolvedPreset = conventionalCommitsPreset({
220228
types: mergeWithDefaultChangelogRules(mappedReleaseRules),
221229
});

tests/bundle.smoke.test.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ interface ChildResult {
5454
* Drain stdout/stderr asynchronously while the child runs so large
5555
* bursts of startup output can't deadlock the parent. Always resolves
5656
* (never rejects) so the caller can assert on the captured streams even
57-
* if the child crashed, exited non-zero, or had to be SIGKILLed.
57+
* if the child crashed, exited non-zero, had to be SIGKILLed, or failed
58+
* to spawn in the first place.
5859
*/
5960
function runChildAndCapture(
6061
child: ChildProcess,
@@ -63,21 +64,39 @@ function runChildAndCapture(
6364
return new Promise((resolve) => {
6465
const stdoutChunks: Buffer[] = [];
6566
const stderrChunks: Buffer[] = [];
67+
let settled = false;
68+
69+
const finish = (overrides: Partial<ChildResult>): void => {
70+
if (settled) return;
71+
settled = true;
72+
clearTimeout(timer);
73+
resolve({
74+
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
75+
stderr: Buffer.concat(stderrChunks).toString('utf8'),
76+
status: null,
77+
signal: null,
78+
...overrides,
79+
});
80+
};
81+
6682
child.stdout?.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
6783
child.stderr?.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
6884

85+
// If spawn itself fails (e.g. ENOENT for the node binary), Node emits
86+
// `error` and never a `close`. Fold the error into stderr so the
87+
// caller still sees a useful message instead of hanging until the
88+
// SIGKILL timer fires.
89+
child.on('error', (err) => {
90+
stderrChunks.push(Buffer.from(`\n${String(err)}\n`));
91+
finish({});
92+
});
93+
6994
const timer = setTimeout(() => {
7095
child.kill('SIGKILL');
7196
}, timeoutMs);
7297

7398
child.on('close', (status, signal) => {
74-
clearTimeout(timer);
75-
resolve({
76-
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
77-
stderr: Buffer.concat(stderrChunks).toString('utf8'),
78-
status,
79-
signal,
80-
});
99+
finish({ status, signal });
81100
});
82101
});
83102
}
@@ -209,8 +228,18 @@ describe('bundled action artefact', () => {
209228
};
210229

211230
const server = createServer(handleRequest);
212-
await new Promise<void>((resolve) => {
213-
server.listen(0, '127.0.0.1', resolve);
231+
await new Promise<void>((resolve, reject) => {
232+
// Without an `error` listener, a failed `listen` (e.g. EADDRINUSE
233+
// on a constrained CI host) would never reject and the test would
234+
// silently hang until vitest's test timeout.
235+
const onError = (err: Error): void => {
236+
reject(err);
237+
};
238+
server.once('error', onError);
239+
server.listen(0, '127.0.0.1', () => {
240+
server.off('error', onError);
241+
resolve();
242+
});
214243
});
215244

216245
try {

0 commit comments

Comments
 (0)