From a0b85dd8b31c0491865b94058f9a18873dcb59b7 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 3 May 2026 13:42:59 +0800 Subject: [PATCH] test(ci): log unhandledRejection / uncaughtException in backend test bootstrap (#7663) Backend tests on develop have a ~22% silent failure rate (mostly Windows, sometimes Linux) where mocha exits with code 1 mid-suite, producing no test failure marker, no error, and no Mocha summary. Different exit points each run. Root cause discovery is blocked by src/tests/backend/common.ts:33, which rethrows unhandled Promise rejections as uncaught exceptions but never logs the reason first. When the rethrow happens between specs, mocha exits with code 1 and the original rejection is lost - especially on Windows, where stderr is not always flushed before abrupt exit. This patch is purely diagnostic: it writes the reason (or stack) to stderr before rethrowing, and adds a matching uncaughtException handler for the same purpose. Behavior on success is unchanged. The next CI failure will surface what is actually rejecting (DirtyDB write? plugin lifecycle? socket cleanup?), so we can fix the real cause. Co-authored-by: Claude Opus 4.7 (1M context) --- src/tests/backend/common.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/tests/backend/common.ts b/src/tests/backend/common.ts index 10d603377..b5a4d3edf 100644 --- a/src/tests/backend/common.ts +++ b/src/tests/backend/common.ts @@ -30,7 +30,27 @@ const logLevel = logger.level; // Mocha doesn't monitor unhandled Promise rejections, so convert them to uncaught exceptions. // https://github.com/mochajs/mocha/issues/2640 -process.on('unhandledRejection', (reason: string) => { throw reason; }); +// +// Log via process.stderr.write before throwing: when the rethrown rejection +// kills mocha between specs, the runner exits with code 1 and no summary. +// Without this, the rejection reason is lost (worst on Windows runners, +// where stderr is not always flushed before abrupt exit) and CI shows a +// silent ELIFECYCLE with no clue what rejected. +process.on('unhandledRejection', (reason: any) => { + process.stderr.write(`[backend tests] unhandledRejection: ${ + reason && reason.stack ? reason.stack : String(reason)}\n`); + throw reason; +}); + +// Surface uncaught exceptions for the same reason. Node's default behavior +// (exit non-zero) is preserved by the explicit process.exit below — without +// the handler, Node would write to stderr and exit; with the handler we have +// to do it ourselves. +process.on('uncaughtException', (err: any) => { + process.stderr.write(`[backend tests] uncaughtException: ${ + err && err.stack ? err.stack : String(err)}\n`); + process.exit(1); +}); before(async function () { this.timeout(60000);