mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
264 lines
7.6 KiB
JavaScript
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')),
|
|
[],
|
|
);
|
|
});
|