mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
LocalCompletionReceiptCleanupLifecycle,
|
||||
LocalCompletionReceiptCleanupScanner,
|
||||
} = require('../dist');
|
||||
|
||||
function journal(candidates) {
|
||||
const resolved = [];
|
||||
return {
|
||||
resolved,
|
||||
async listCandidates() {
|
||||
return {
|
||||
candidates,
|
||||
truncated: false,
|
||||
...(candidates.length === 0
|
||||
? {}
|
||||
: {
|
||||
nextCursor: {
|
||||
updatedAtMs: candidates.at(-1).updatedAtMs,
|
||||
attemptId: candidates.at(-1).attemptId,
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
async resolve(attemptId) {
|
||||
resolved.push(attemptId);
|
||||
return true;
|
||||
},
|
||||
async register() {},
|
||||
async markQuarantined() {},
|
||||
};
|
||||
}
|
||||
|
||||
function candidate(attemptId, state, attemptStatus, extra = {}) {
|
||||
return {
|
||||
attemptId,
|
||||
runId: `run-${attemptId}`,
|
||||
state,
|
||||
attemptStatus,
|
||||
executorType: 'local_process',
|
||||
registeredAtMs: 1,
|
||||
updatedAtMs: 2,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
test('cleans only terminal database-indexed receipts and preserves active work', async () => {
|
||||
const active = '019f70e0-0000-7000-8000-000000000010';
|
||||
const terminal = '019f70e0-0000-7000-8000-000000000011';
|
||||
const missing = '019f70e0-0000-7000-8000-000000000012';
|
||||
const source = journal([
|
||||
candidate(active, 'pending', 'running'),
|
||||
candidate(terminal, 'pending', 'succeeded', { finishedAtMs: 5 }),
|
||||
candidate(missing, 'pending', 'failed', { finishedAtMs: 5 }),
|
||||
]);
|
||||
const removed = [];
|
||||
const scanner = new LocalCompletionReceiptCleanupScanner(
|
||||
source,
|
||||
{
|
||||
async remove(attemptId) {
|
||||
removed.push(attemptId);
|
||||
return attemptId === terminal;
|
||||
},
|
||||
async read() {},
|
||||
async publish() {},
|
||||
},
|
||||
{ clock: { now: () => 100 }, terminalMissingRetentionMs: 10 },
|
||||
);
|
||||
assert.deepEqual(await scanner.scan(), {
|
||||
scanned: 3,
|
||||
removed: 1,
|
||||
expiredMissing: 1,
|
||||
purgedQuarantines: 0,
|
||||
remaining: 1,
|
||||
failed: 0,
|
||||
truncated: false,
|
||||
nextCursor: { updatedAtMs: 2, attemptId: missing },
|
||||
});
|
||||
assert.deepEqual(removed, [terminal, missing]);
|
||||
assert.deepEqual(source.resolved, [terminal, missing]);
|
||||
});
|
||||
|
||||
test('completes a durable quarantine intent before purging it', async () => {
|
||||
const attemptId = '019f70e0-0000-7000-8000-000000000013';
|
||||
const source = journal([
|
||||
candidate(attemptId, 'quarantined', 'failed', {
|
||||
quarantineRef: `.quarantine/01/${attemptId}.json`,
|
||||
purgeAfterMs: 5,
|
||||
finishedAtMs: 4,
|
||||
}),
|
||||
]);
|
||||
const calls = [];
|
||||
const scanner = new LocalCompletionReceiptCleanupScanner(source, {
|
||||
async remove() {
|
||||
return false;
|
||||
},
|
||||
async read() {},
|
||||
async publish() {},
|
||||
async quarantine(value) {
|
||||
calls.push(`quarantine:${value}`);
|
||||
},
|
||||
async purgeQuarantine(value) {
|
||||
calls.push(`purge:${value}`);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const summary = await scanner.scan();
|
||||
assert.equal(summary.purgedQuarantines, 1);
|
||||
assert.deepEqual(calls, [`quarantine:${attemptId}`, `purge:${attemptId}`]);
|
||||
assert.deepEqual(source.resolved, [attemptId]);
|
||||
});
|
||||
|
||||
test('cleanup lifecycle is explicit and stop is idempotent without a live timer', async () => {
|
||||
const scanner = new LocalCompletionReceiptCleanupScanner(journal([]), {
|
||||
async remove() {
|
||||
return false;
|
||||
},
|
||||
async read() {},
|
||||
async publish() {},
|
||||
});
|
||||
const lifecycle = new LocalCompletionReceiptCleanupLifecycle(scanner, {
|
||||
intervalMs: 60_000,
|
||||
pageSize: 8,
|
||||
});
|
||||
assert.equal((await lifecycle.runOnce()).scanned, 0);
|
||||
lifecycle.start();
|
||||
assert.equal(await lifecycle.stop(), 'stopped');
|
||||
assert.equal(await lifecycle.stop(), 'stopped');
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
LocalProcessController,
|
||||
createLocalProcessDurableHandle,
|
||||
} = require('../dist');
|
||||
|
||||
const IDENTITY = Object.freeze({
|
||||
platform: 'linux',
|
||||
bootId: '11111111-2222-3333-4444-555555555555',
|
||||
pid: 1234,
|
||||
processGroupId: 1234,
|
||||
startTimeTicks: '987654',
|
||||
});
|
||||
const HANDLE = createLocalProcessDurableHandle('handle-1', IDENTITY);
|
||||
|
||||
function fixture(inspections) {
|
||||
let now = 0;
|
||||
const signals = [];
|
||||
let index = 0;
|
||||
const controller = new LocalProcessController({
|
||||
identityProvider: {
|
||||
async inspect(identity) {
|
||||
assert.deepEqual(identity, IDENTITY);
|
||||
return inspections[Math.min(index++, inspections.length - 1)];
|
||||
},
|
||||
},
|
||||
terminateGraceMs: 10,
|
||||
killGraceMs: 10,
|
||||
pollIntervalMs: 5,
|
||||
clock: { now: () => now },
|
||||
wait: async (delayMs) => {
|
||||
now += delayMs;
|
||||
},
|
||||
signalProcessGroup(processGroupId, signal) {
|
||||
signals.push({ processGroupId, signal });
|
||||
},
|
||||
});
|
||||
return { controller, signals };
|
||||
}
|
||||
|
||||
test('stops only after exact durable identity inspection', async () => {
|
||||
const value = fixture([
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'not_running', identityPid: 1234 },
|
||||
]);
|
||||
assert.deepEqual(await value.controller.stop(HANDLE), {
|
||||
status: 'stopped',
|
||||
signal: 'SIGTERM',
|
||||
});
|
||||
assert.deepEqual(value.signals, [
|
||||
{ processGroupId: 1234, signal: 'SIGTERM' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('revalidates identity before escalating to SIGKILL', async () => {
|
||||
const value = fixture([
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'not_running', identityPid: 1234 },
|
||||
]);
|
||||
assert.deepEqual(await value.controller.stop(HANDLE), {
|
||||
status: 'stopped',
|
||||
signal: 'SIGKILL',
|
||||
});
|
||||
assert.deepEqual(value.signals, [
|
||||
{ processGroupId: 1234, signal: 'SIGTERM' },
|
||||
{ processGroupId: 1234, signal: 'SIGKILL' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not escalate after identity is no longer running', async () => {
|
||||
const value = fixture([
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'running', identityPid: 1234 },
|
||||
{ status: 'not_running', identityPid: 1234 },
|
||||
]);
|
||||
assert.deepEqual(await value.controller.stop(HANDLE), {
|
||||
status: 'stopped',
|
||||
signal: 'SIGTERM',
|
||||
});
|
||||
assert.deepEqual(value.signals, [
|
||||
{ processGroupId: 1234, signal: 'SIGTERM' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('rejects an unparseable handle without signaling', async () => {
|
||||
const value = fixture([{ status: 'running', identityPid: 1234 }]);
|
||||
assert.deepEqual(await value.controller.stop('not-a-durable-handle'), {
|
||||
status: 'unknown',
|
||||
reason: 'invalid_handle',
|
||||
});
|
||||
assert.deepEqual(value.signals, []);
|
||||
});
|
||||
|
||||
test('maps identity provider failure to unknown without signaling', async () => {
|
||||
const signals = [];
|
||||
const controller = new LocalProcessController({
|
||||
identityProvider: {
|
||||
inspect: async () => Promise.reject(new Error('proc unavailable')),
|
||||
},
|
||||
signalProcessGroup(processGroupId, signal) {
|
||||
signals.push({ processGroupId, signal });
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await controller.stop(HANDLE), {
|
||||
status: 'unknown',
|
||||
reason: 'provider_unavailable',
|
||||
});
|
||||
assert.deepEqual(signals, []);
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
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 {
|
||||
CompletionReceiptAlreadyExistsError,
|
||||
CompletionReceiptFileStore,
|
||||
InvalidCompletionReceiptError,
|
||||
LocalProcessPersistedExecutionInspector,
|
||||
createLocalProcessDurableHandle,
|
||||
parseLocalProcessDurableHandle,
|
||||
} = require('../dist');
|
||||
|
||||
const RUN_ID = '019f70c0-0000-7000-8000-000000000001';
|
||||
const ATTEMPT_ID = '019f70c0-0000-7000-8000-000000000002';
|
||||
|
||||
function receipt() {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
runId: RUN_ID,
|
||||
attemptId: ATTEMPT_ID,
|
||||
callbackSequence: 1,
|
||||
token: 'A'.repeat(32),
|
||||
startedAtMs: 1,
|
||||
finishedAtMs: 2,
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
|
||||
test('publishes one immutable bounded receipt and never overwrites it', async (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-run-receipt-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const store = new CompletionReceiptFileStore(root);
|
||||
|
||||
await store.publish(receipt());
|
||||
assert.deepEqual(await store.read(ATTEMPT_ID), receipt());
|
||||
await assert.rejects(
|
||||
store.publish(receipt()),
|
||||
CompletionReceiptAlreadyExistsError,
|
||||
);
|
||||
assert.equal(await store.remove(ATTEMPT_ID), true);
|
||||
assert.equal(await store.read(ATTEMPT_ID), undefined);
|
||||
});
|
||||
|
||||
test('publishes a Workflow portable receipt identity without widening paths', async (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-run-receipt-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const store = new CompletionReceiptFileStore(root);
|
||||
const workflowReceipt = {
|
||||
...receipt(),
|
||||
runId: 'workflow-run-1',
|
||||
attemptId: 'wta:0123456789abcdef0123456789abcdef',
|
||||
};
|
||||
|
||||
await store.publish(workflowReceipt);
|
||||
assert.deepEqual(
|
||||
await store.read(workflowReceipt.attemptId),
|
||||
workflowReceipt,
|
||||
);
|
||||
for (const attemptId of [
|
||||
'../attempt-1',
|
||||
'attempt/1',
|
||||
'attempt\\1',
|
||||
`attempt-${'a'.repeat(36)}`,
|
||||
]) {
|
||||
await assert.rejects(store.read(attemptId), InvalidCompletionReceiptError);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects a filesystem root as a receipt authority', () => {
|
||||
assert.throws(
|
||||
() => new CompletionReceiptFileStore(path.parse(process.cwd()).root),
|
||||
/bounded non-root absolute path/,
|
||||
);
|
||||
});
|
||||
|
||||
test('refuses a symbolic-link receipt without following it', async (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-run-receipt-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const directory = path.join(root, ATTEMPT_ID.slice(0, 2));
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
const source = path.join(root, 'untrusted.json');
|
||||
fs.writeFileSync(source, JSON.stringify(receipt()));
|
||||
fs.symlinkSync(source, path.join(directory, `${ATTEMPT_ID}.json`));
|
||||
|
||||
await assert.rejects(
|
||||
new CompletionReceiptFileStore(root).read(ATTEMPT_ID),
|
||||
InvalidCompletionReceiptError,
|
||||
);
|
||||
});
|
||||
|
||||
test('durable process handles are exact and evidence preserves the bound PID', async () => {
|
||||
const identity = {
|
||||
platform: 'linux',
|
||||
bootId: 'boot-1',
|
||||
pid: 123,
|
||||
processGroupId: 123,
|
||||
startTimeTicks: '456',
|
||||
};
|
||||
const handle = createLocalProcessDurableHandle('handle-1', identity);
|
||||
assert.deepEqual(parseLocalProcessDurableHandle(handle), {
|
||||
handleId: 'handle-1',
|
||||
identity,
|
||||
});
|
||||
assert.equal(parseLocalProcessDurableHandle('invalid'), null);
|
||||
|
||||
const inspector = new LocalProcessPersistedExecutionInspector({
|
||||
inspect: async (value) => {
|
||||
assert.deepEqual(value, identity);
|
||||
return { status: 'running', identityPid: value.pid };
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await inspector.inspect(handle), {
|
||||
status: 'running',
|
||||
identityPid: 123,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,263 @@
|
||||
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')),
|
||||
[],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user