Files
qinglong/packages/ql3-local-process/test/launcher.test.cjs
T

425 lines
15 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { quotaEnvironment } = require('./helpers/quotaEnvironment.cjs');
const { test } = require('node:test');
const { CompletionReceiptFileStore } = require('../dist');
const { LocalProcessLaunchError, LocalProcessLauncher } = require('../dist');
const RUN_ID = '019f70e0-0000-7000-8000-000000000001';
const ATTEMPT_ID = '019f70e0-0000-7000-8000-000000000002';
const TOKEN = '0123456789abcdef0123456789abcdef0123456789abcdef';
function fixture(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-local-process-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return {
directory,
receiptRoot: path.join(directory, 'receipts'),
};
}
function identityProvider(onCapture = () => undefined) {
return {
async capture(pid) {
onCapture(pid);
return {
platform: 'linux',
bootId: '11111111-2222-3333-4444-555555555555',
pid,
processGroupId: pid,
startTimeTicks: '1',
};
},
async inspect(identity) {
return { status: 'running', identityPid: identity.pid };
},
};
}
async function waitForReceipt(store) {
for (let index = 0; index < 100; index += 1) {
const receipt = await store.read(ATTEMPT_ID);
if (receipt) return receipt;
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error('receipt was not published');
}
test('persists the journal barrier before spawn and publishes a trusted receipt', async (t) => {
const { receiptRoot } = fixture(t);
let registered = false;
const journal = {
async register(command) {
assert.deepEqual(command, {
runId: RUN_ID,
attemptId: ATTEMPT_ID,
registeredAtMs: 100,
});
registered = true;
},
};
const launcher = new LocalProcessLauncher(journal, {
receiptRoot,
clock: { now: () => 100 },
createHandleId: () => 'handle-1',
identityProvider: identityProvider(() => assert.equal(registered, true)),
});
const handle = await launcher.start({
runId: RUN_ID,
attemptId: ATTEMPT_ID,
callbackSequence: 1,
callbackToken: TOKEN,
command: {
kind: 'shell',
command:
'test -z "${QL3_RECEIPT_CALLBACK_TOKEN-}" && test -z "${QL3_RECEIPT_TARGET-}"',
},
environment: { USER_VALUE: 'present' },
});
assert.equal(handle.startedAtMs, 100);
assert.equal(handle.pid > 0, true);
assert.match(handle.durableHandle, /^ql3lp1\./);
assert.deepEqual(await handle.completion, { exitCode: 0, signal: null });
const receipt = await waitForReceipt(
new CompletionReceiptFileStore(receiptRoot),
);
assert.deepEqual(
{ ...receipt, finishedAtMs: 100 },
{
schemaVersion: 1,
runId: RUN_ID,
attemptId: ATTEMPT_ID,
callbackSequence: 1,
token: TOKEN,
startedAtMs: 100,
finishedAtMs: 100,
exitCode: 0,
},
);
assert.equal(receipt.finishedAtMs >= 100, true);
});
test('journal failure prevents spawn and filesystem side effects from user code', async (t) => {
const { directory, receiptRoot } = fixture(t);
const marker = path.join(directory, 'spawned');
const launcher = new LocalProcessLauncher(
{
async register() {
throw new Error('database unavailable');
},
},
{ receiptRoot, identityProvider: identityProvider() },
);
await assert.rejects(
launcher.start({
runId: RUN_ID,
attemptId: ATTEMPT_ID,
callbackSequence: 1,
callbackToken: TOKEN,
command: { kind: 'argv', file: '/usr/bin/touch', args: [marker] },
}),
LocalProcessLaunchError,
);
assert.equal(fs.existsSync(marker), false);
});
test('a modified launcher is rejected before durable registration', async (t) => {
const { directory, receiptRoot } = fixture(t);
const launcherPath = path.join(directory, 'launcher.sh');
fs.writeFileSync(launcherPath, '#!/bin/sh\nexit 0\n', { mode: 0o700 });
let registered = false;
const launcher = new LocalProcessLauncher(
{
async register() {
registered = true;
},
},
{ receiptRoot, launcherPath, identityProvider: identityProvider() },
);
await assert.rejects(
launcher.start({
runId: RUN_ID,
attemptId: ATTEMPT_ID,
callbackSequence: 1,
callbackToken: TOKEN,
command: { kind: 'argv', file: '/usr/bin/true', args: [] },
}),
/digest does not match review/,
);
assert.equal(registered, false);
});
test('executes the verified launcher fd when its path is replaced after registration', async (t) => {
const { directory, receiptRoot } = fixture(t);
const launcherPath = path.join(directory, 'launcher.sh');
fs.copyFileSync(
path.resolve(__dirname, '../assets/ql3-launcher.sh'),
launcherPath,
);
const replacementPath = path.join(directory, 'replacement.sh');
const launcher = new LocalProcessLauncher(
{
async register() {
fs.writeFileSync(replacementPath, '#!/bin/sh\nexit 99\n', {
mode: 0o700,
});
fs.renameSync(replacementPath, launcherPath);
},
},
{
receiptRoot,
launcherPath,
clock: { now: () => 100 },
identityProvider: identityProvider(),
},
);
const handle = await launcher.start({
runId: RUN_ID,
attemptId: ATTEMPT_ID,
callbackSequence: 1,
callbackToken: TOKEN,
command: { kind: 'argv', file: '/usr/bin/true', args: [] },
});
assert.deepEqual(await handle.completion, { exitCode: 0, signal: null });
assert.equal(
(await waitForReceipt(new CompletionReceiptFileStore(receiptRoot)))
.exitCode,
0,
);
});
test('hard-caps durable output and publishes an immutable truncation fact', async (t) => {
const { directory, receiptRoot } = fixture(t);
const logArtifactId = `local-${'b'.repeat(30)}`;
const outputDirectory = path.join(directory, 'artifacts', 'bb');
const outputFilePath = path.join(outputDirectory, `${logArtifactId}.log`);
const maximumBytes = 64 * 1024;
const launcher = new LocalProcessLauncher(
{ register: async () => undefined },
{
receiptRoot,
clock: { now: () => 100 },
identityProvider: identityProvider(),
},
);
const handle = await launcher.start({
runId: RUN_ID,
attemptId: ATTEMPT_ID,
callbackSequence: 1,
callbackToken: TOKEN,
environment: quotaEnvironment(directory),
command: {
kind: 'argv',
file: process.execPath,
args: [
'-e',
[
'if (process.env.QL3_OUTPUT_QUOTA_FIFO) process.exit(91);',
'if (process.env.QL3_OUTPUT_TRUNCATION_TARGET) process.exit(92);',
'process.stdout.write(Buffer.alloc(256 * 1024, 0x61), () => process.exit(0));',
].join(''),
],
},
output: {
filePath: outputFilePath,
maximumBytes,
logArtifactId,
},
});
assert.deepEqual(await handle.completion, { exitCode: 0, signal: null });
assert.equal(fs.statSync(outputFilePath).size, maximumBytes);
const factPath = path.join(
outputDirectory,
`.${logArtifactId}.log.truncated.json`,
);
const fact = JSON.parse(fs.readFileSync(factPath, 'utf8'));
assert.deepEqual(
{
schemaVersion: fact.schemaVersion,
runId: fact.runId,
attemptId: fact.attemptId,
logArtifactId: fact.logArtifactId,
maximumBytes: fact.maximumBytes,
quotaReached: fact.quotaReached,
},
{
schemaVersion: 1,
runId: RUN_ID,
attemptId: ATTEMPT_ID,
logArtifactId,
maximumBytes,
quotaReached: true,
},
);
assert.equal(Number.isSafeInteger(fact.observedAtMs), true);
assert.deepEqual(
fs.readdirSync(outputDirectory).filter((name) => name.endsWith('.fifo')),
[],
);
});
for (const kind of ['argv', 'shell']) {
test(`publishes sparse binary ${kind} output before the process exits`, async (t) => {
const { directory, receiptRoot } = fixture(t);
const logArtifactId = `local-${'c'.repeat(30)}`;
const outputFilePath = path.join(directory, `${logArtifactId}.log`);
const release = path.join(directory, 'release');
const marker = Buffer.from([0x00, 0xff, 0x71, 0x6c, 0x33]);
const script = path.join(directory, 'producer.cjs');
fs.writeFileSync(script, `
const fs = require('node:fs');
process.stdout.write(Buffer.from([0x00, 0xff, 0x71, 0x6c, 0x33]));
const deadline = setTimeout(() => process.exit(92), 15000);
const timer = setInterval(() => {
if (!fs.existsSync(process.argv[2])) return;
clearInterval(timer);
clearTimeout(deadline);
process.stderr.write('tail', () => process.exit(7));
}, 10);
`, { mode: 0o600 });
const launcher = new LocalProcessLauncher(
{ register: async () => undefined },
{ receiptRoot, identityProvider: identityProvider() },
);
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
const handle = await launcher.start({
runId: RUN_ID, attemptId: ATTEMPT_ID,
callbackSequence: 1, callbackToken: TOKEN,
environment: quotaEnvironment(directory),
command: kind === 'argv'
? { kind, file: process.execPath, args: [script, release] }
: { kind, command: [process.execPath, script, release].map(quote).join(' ') },
output: { filePath: outputFilePath, maximumBytes: 65536, logArtifactId },
});
try {
const deadline = Date.now() + 5000;
while (fs.statSync(outputFilePath).size < marker.length && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
assert.deepEqual(fs.readFileSync(outputFilePath), marker,
'sparse bytes must be readable while the producer is waiting for release');
assert.equal(await new CompletionReceiptFileStore(receiptRoot).read(ATTEMPT_ID), undefined);
fs.writeFileSync(release, '', { mode: 0o600 });
assert.deepEqual(await handle.completion, { exitCode: 7, signal: null });
assert.deepEqual(fs.readFileSync(outputFilePath), Buffer.concat([marker, Buffer.from('tail')]));
const fact = JSON.parse(fs.readFileSync(path.join(directory, `.${logArtifactId}.log.truncated.json`)));
assert.equal(fact.quotaReached, false);
assert.equal((await waitForReceipt(new CompletionReceiptFileStore(receiptRoot))).exitCode, 7);
} finally {
fs.writeFileSync(release, '', { mode: 0o600 });
await handle.completion;
}
});
}
for (const [label, initialBytes, producedBytes] of [
['empty output', 0, 0],
['exact quota', 0, 65536],
['one-byte overflow', 0, 65537],
['partial final block', 65513, 31],
['exhausted quota', 65536, 23],
['exhausted quota without overflow', 65536, 0],
]) {
test(`keeps byte-exact capture and truncation for ${label}`, async (t) => {
const { directory, receiptRoot } = fixture(t);
const logArtifactId = `local-${'d'.repeat(30)}`;
const filePath = path.join(directory, `${logArtifactId}.log`);
const initial = Buffer.alloc(initialBytes, 0x5a);
fs.writeFileSync(filePath, initial, { mode: 0o600 });
const produced = Buffer.from(Array.from({ length: producedBytes }, (_, i) => i % 256));
const launcher = new LocalProcessLauncher(
{ register: async () => undefined },
{ receiptRoot, identityProvider: identityProvider() },
);
const handle = await launcher.start({
runId: RUN_ID, attemptId: ATTEMPT_ID,
callbackSequence: 1, callbackToken: TOKEN,
environment: quotaEnvironment(directory),
command: { kind: 'argv', file: process.execPath, args: ['-e', `
const data = Buffer.from(Array.from({length: ${producedBytes}}, (_, i) => i % 256));
let offset = 0;
function write() {
if (offset === data.length) return process.exit(9);
const end = Math.min(data.length, offset + 997);
const chunk = data.subarray(offset, end);
offset = end;
process.stdout.write(chunk, () => setImmediate(write));
}
write();
`] },
output: { filePath, maximumBytes: 65536, logArtifactId },
});
assert.deepEqual(await handle.completion, { exitCode: 9, signal: null });
assert.deepEqual(fs.readFileSync(filePath), Buffer.concat([initial, produced]).subarray(0, 65536));
const fact = JSON.parse(fs.readFileSync(path.join(directory, `.${logArtifactId}.log.truncated.json`)));
assert.equal(fact.quotaReached, initialBytes + producedBytes > 65536);
assert.equal((await waitForReceipt(new CompletionReceiptFileStore(receiptRoot))).exitCode, 9);
});
}
test('rejects unsupported capture utilities before running user code', async (t) => {
const { directory, receiptRoot } = fixture(t);
const bin = path.join(directory, 'unsupported-bin');
fs.mkdirSync(bin, { mode: 0o700 });
for (const name of ['busybox', 'head', 'stdbuf']) {
fs.writeFileSync(path.join(bin, name), '#!/bin/sh\nexit 1\n', { mode: 0o700 });
}
const marker = path.join(directory, 'must-not-run');
const logArtifactId = `local-${'e'.repeat(30)}`;
const filePath = path.join(directory, `${logArtifactId}.log`);
const launcher = new LocalProcessLauncher(
{ register: async () => undefined },
{ receiptRoot, identityProvider: identityProvider() },
);
const handle = await launcher.start({
runId: RUN_ID, attemptId: ATTEMPT_ID,
callbackSequence: 1, callbackToken: TOKEN,
environment: { PATH: `${bin}:/usr/bin:/bin` },
command: { kind: 'argv', file: '/usr/bin/touch', args: [marker] },
output: { filePath, maximumBytes: 65536, logArtifactId },
});
assert.deepEqual(await handle.completion, { exitCode: 125, signal: null });
assert.equal(fs.existsSync(marker), false);
assert.equal(fs.statSync(filePath).size, 0);
assert.equal(await new CompletionReceiptFileStore(receiptRoot).read(ATTEMPT_ID), undefined);
assert.equal(fs.readdirSync(directory).some((name) => name.endsWith('.fifo')), false);
});
test('capture failure drains output without forging a truncation fact or changing user exit', async (t) => {
const { directory, receiptRoot } = fixture(t);
const bin = path.join(directory, 'failing-bin');
fs.mkdirSync(bin, { mode: 0o700 });
fs.writeFileSync(path.join(bin, 'busybox'), `#!/bin/sh
case " $* " in
*' count=0 '*) exit 0 ;;
esac
printf 'capture failure must not bypass the log quota' >&2
exit 1
`, { mode: 0o700 });
const logArtifactId = `local-${'f'.repeat(30)}`;
const filePath = path.join(directory, `${logArtifactId}.log`);
const launcher = new LocalProcessLauncher(
{ register: async () => undefined },
{ receiptRoot, identityProvider: identityProvider() },
);
const handle = await launcher.start({
runId: RUN_ID, attemptId: ATTEMPT_ID,
callbackSequence: 1, callbackToken: TOKEN,
environment: { PATH: `${bin}:/usr/bin:/bin` },
command: { kind: 'argv', file: process.execPath, args: [
'-e', 'process.stdout.write(Buffer.alloc(256 * 1024), () => process.exit(9));',
] },
output: { filePath, maximumBytes: 65536, logArtifactId },
});
assert.deepEqual(await handle.completion, { exitCode: 9, signal: null });
assert.equal(fs.statSync(filePath).size, 0);
assert.equal(fs.existsSync(path.join(directory, `.${logArtifactId}.log.truncated.json`)), false);
assert.equal((await waitForReceipt(new CompletionReceiptFileStore(receiptRoot))).exitCode, 9);
});