diff --git a/test/back/process-failure.test.cjs b/test/back/process-failure.test.cjs new file mode 100644 index 00000000..fb217887 --- /dev/null +++ b/test/back/process-failure.test.cjs @@ -0,0 +1,52 @@ +const assert = require('node:assert/strict'); +const { test } = require('node:test'); +const { processFailure } = require('../helpers/process-failure.cjs'); + +test('process failure diagnostics classify native crashes without credentials or raw output', () => { + const secret = 'private-client-secret'; + const summary = processFailure({ + code: null, + signal: 'SIGTRAP', + killed: false, + cmd: `node worker.cjs ${secret}`, + message: secret, + stdout: JSON.stringify({ token: secret }), + stderr: `${secret}\nCheck failed: (data_) != nullptr.\ncompiler/heap-refs.h\nv8::internal::compiler::JSHeapBroker\n`, + }); + assert.equal(JSON.stringify(summary).includes(secret), false); + assert.equal(summary.signal, 'SIGTRAP'); + assert.equal(summary.fatal.v8Check, true); + assert.equal(summary.fatal.heapBroker, true); + assert.equal(summary.fatal.compiler, true); + assert.equal(summary.fatal.outOfMemory, false); + assert.equal(summary.fatal.threadCreation, false); +}); + +test('process failure diagnostics retain timeout and resource classes without arbitrary error fields', () => { + const runtime = { versions: { node: '24.21.0' }, arch: 'x64' }; + const summary = processFailure( + { + code: 1, + signal: 'SIGTERM private-token', + killed: true, + stderr: 'pthread_create: resource unavailable; heap out of memory', + }, + runtime, + ); + assert.deepEqual(summary, { + code: 1, + signal: null, + killed: true, + node: '24.21.0', + arch: 'x64', + fatal: { + threadCreation: true, + outOfMemory: true, + v8Check: false, + heapBroker: false, + compiler: false, + }, + }); + assert.equal(processFailure({ code: 'private-token' }).code, null); + assert.equal(processFailure().signal, null); +}); diff --git a/test/back/systemTokenRenewal.test.cjs b/test/back/systemTokenRenewal.test.cjs index 5e70be50..c807a5f3 100644 --- a/test/back/systemTokenRenewal.test.cjs +++ b/test/back/systemTokenRenewal.test.cjs @@ -7,6 +7,7 @@ const os = require('node:os'); const path = require('node:path'); const { execFile } = require('node:child_process'); const { promisify } = require('node:util'); +const { processFailure } = require('../helpers/process-failure.cjs'); const environment = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-token-env-')); fs.writeFileSync(path.join(environment, '.env'), ''); fs.mkdirSync(path.join(environment, 'data/db'), { recursive: true }); @@ -622,11 +623,7 @@ test('real SQLite and authentication cache retain concurrent process updates', a assert.equal( outcome.status, 'fulfilled', - JSON.stringify({ - code: outcome.reason?.code, - signal: outcome.reason?.signal, - killed: outcome.reason?.killed, - }), + JSON.stringify(processFailure(outcome.reason)), ); return outcome.value; }); diff --git a/test/helpers/process-failure.cjs b/test/helpers/process-failure.cjs new file mode 100644 index 00000000..47356444 --- /dev/null +++ b/test/helpers/process-failure.cjs @@ -0,0 +1,28 @@ +// execFile errors contain the command (including credentials), stdout and +// stderr. Keep CI diagnostics useful without publishing any of those values. +function processFailure(error, runtime = process) { + const stderr = String(error?.stderr || ''); + return { + code: Number.isInteger(error?.code) ? error.code : null, + signal: /^SIG[A-Z0-9]+$/.test(error?.signal || '') ? error.signal : null, + killed: error?.killed === true, + node: runtime.versions.node, + arch: runtime.arch, + fatal: { + threadCreation: + /uv_thread_create|pthread_create|Failed to start thread|failed to create[^\n]*thread/i.test( + stderr, + ), + outOfMemory: /heap out of memory|Allocation failed|OutOfMemory/i.test( + stderr, + ), + v8Check: /Check failed:|Fatal error in/i.test(stderr), + heapBroker: /JSHeapBroker|compiler\/heap-refs/.test(stderr), + compiler: /v8::internal::(?:compiler|maglev)|PersistentHandles/.test( + stderr, + ), + }, + }; +} + +module.exports = { processFailure };