test: classify subprocess crashes without exposing credentials

This commit is contained in:
whyour
2026-10-23 13:50:37 +08:00
parent 81de8f7094
commit 49cec13fcd
3 changed files with 82 additions and 5 deletions
+52
View File
@@ -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);
});
+2 -5
View File
@@ -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;
});
+28
View File
@@ -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 };