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

264 lines
7.6 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 { 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,
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')),
[],
);
});