Files
qinglong/packages/ql3-local-application/test/shutdownReceipt.test.cjs
T

134 lines
4.2 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LocalApplicationShutdownReceiptError,
buildLocalApplicationShutdownReceipt,
localApplicationShutdownReceiptPath,
observeLocalApplicationShutdown,
parseLocalApplicationShutdownReceipt,
publishLocalApplicationShutdownReceipt,
} = require('../dist/production-process/shutdownReceipt.js');
function directory(t) {
const value = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-stop-receipt-'));
t.after(() => fs.rmSync(value, { recursive: true, force: true }));
return value;
}
function receipt(stoppedBootAgeMs = 2_500, processId = 41) {
return buildLocalApplicationShutdownReceipt({
instanceId: 'edge-router-1',
profile: 'edge',
signal: 'SIGTERM',
startupReceiptDigest: 'a'.repeat(64),
observation: {
bootId: '12345678-1234-4abc-8def-123456789abc',
stoppedBootAgeMs,
processId,
processStartTicks: String(100 + processId),
nodeExecutable: '/usr/bin/node',
nodeVersion: 'v24.18.0',
},
});
}
test('publishes one bounded graceful shutdown receipt', (t) => {
const root = directory(t);
const configFilePath = path.join(root, 'local-application.json');
const target = publishLocalApplicationShutdownReceipt(
configFilePath,
receipt(),
);
assert.equal(target, localApplicationShutdownReceiptPath(configFilePath));
assert.equal(fs.statSync(target).mode & 0o777, 0o600);
assert.equal(fs.statSync(target).nlink, 1);
assert.equal(fs.existsSync(`${target}.stage`), false);
const parsed = parseLocalApplicationShutdownReceipt(
fs.readFileSync(target, 'utf8'),
);
assert.equal(parsed.signal, 'SIGTERM');
assert.equal(parsed.stopResult, 'stopped');
assert.equal(parsed.startupReceiptDigest, 'a'.repeat(64));
});
test('atomically replaces the prior process shutdown receipt', (t) => {
const root = directory(t);
const configFilePath = path.join(root, 'local-application.json');
const target = publishLocalApplicationShutdownReceipt(
configFilePath,
receipt(),
);
const replacement = receipt(3_000, 42);
assert.equal(
publishLocalApplicationShutdownReceipt(configFilePath, replacement),
target,
);
assert.deepEqual(
parseLocalApplicationShutdownReceipt(fs.readFileSync(target, 'utf8')),
replacement,
);
});
test('canonicalizes the Linux runtime observation property order', () => {
const runtimeOrderedObservation = {
bootId: '12345678-1234-4abc-8def-123456789abc',
processId: 41,
processStartTicks: '141',
nodeExecutable: '/usr/bin/node',
nodeVersion: 'v24.18.0',
stoppedBootAgeMs: 2_500,
};
const built = buildLocalApplicationShutdownReceipt({
instanceId: 'edge-router-1',
profile: 'edge',
signal: 'SIGTERM',
startupReceiptDigest: 'a'.repeat(64),
observation: runtimeOrderedObservation,
});
assert.deepEqual(
parseLocalApplicationShutdownReceipt(JSON.stringify(built)),
built,
);
});
test('rejects a forged digest and unsafe deterministic stage', (t) => {
const root = directory(t);
const configFilePath = path.join(root, 'local-application.json');
const valid = receipt();
assert.throws(
() =>
parseLocalApplicationShutdownReceipt(
JSON.stringify({
...valid,
stoppedBootAgeMs: valid.stoppedBootAgeMs + 1,
}),
),
/digest is invalid/,
);
const target = localApplicationShutdownReceiptPath(configFilePath);
const outside = path.join(root, 'outside');
fs.writeFileSync(outside, 'do-not-replace', { mode: 0o600 });
fs.symlinkSync(outside, `${target}.stage`);
assert.throws(
() => publishLocalApplicationShutdownReceipt(configFilePath, valid),
LocalApplicationShutdownReceiptError,
);
assert.equal(fs.readFileSync(outside, 'utf8'), 'do-not-replace');
assert.equal(fs.existsSync(target), false);
});
test(
'observes the still-live Linux process after application shutdown',
{ skip: process.platform !== 'linux' },
() => {
const observed = observeLocalApplicationShutdown();
assert.equal(observed.processId, process.pid);
assert.match(observed.bootId, /^[0-9a-f-]{36}$/);
assert.ok(observed.stoppedBootAgeMs >= 0);
},
);