mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
263 lines
6.8 KiB
JavaScript
263 lines
6.8 KiB
JavaScript
require('ts-node/register/transpile-only');
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
PrimaryCompletionReceiptLifecycle,
|
|
} = require('../../back/runtime/application/primaryCompletionReceiptLifecycle');
|
|
const {
|
|
PrimaryCompletionReceiptScanner,
|
|
} = require('../../back/runtime/application/primaryCompletionReceiptScanner');
|
|
const {
|
|
PrimaryCompletionReceiptSupervisor,
|
|
} = require('../../back/runtime/application/primaryCompletionReceiptSupervisor');
|
|
|
|
function candidate(runId, attemptId, executorType = 'local_process') {
|
|
return {
|
|
runId,
|
|
attempts: attemptId ? [{ attemptId, executorType }] : [],
|
|
};
|
|
}
|
|
|
|
function page(overrides = {}) {
|
|
return {
|
|
candidates: [],
|
|
truncated: false,
|
|
unsafeAttemptOverflow: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function scan(overrides = {}) {
|
|
return {
|
|
scanned: 1,
|
|
applied: 0,
|
|
alreadyTerminal: 0,
|
|
quarantined: 0,
|
|
purgedQuarantines: 0,
|
|
expiredMissing: 0,
|
|
missing: 1,
|
|
cleanupPending: 0,
|
|
skipped: 0,
|
|
ambiguous: 0,
|
|
failed: 0,
|
|
truncated: false,
|
|
unsafeAttemptOverflow: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('scans only database-discovered local Attempts and isolates failures', async () => {
|
|
const consumed = [];
|
|
const scanner = new PrimaryCompletionReceiptScanner(
|
|
{
|
|
async listCandidates() {
|
|
return page({
|
|
candidates: [
|
|
candidate('run-1', 'applied'),
|
|
candidate('run-2', 'terminal'),
|
|
candidate('run-3', 'missing'),
|
|
candidate('run-4', 'cleanup-pending'),
|
|
candidate('run-5', 'failed'),
|
|
candidate('run-6', 'quarantined'),
|
|
candidate('run-7', 'remote', 'remote_worker'),
|
|
candidate('run-8'),
|
|
{
|
|
runId: 'run-9',
|
|
attempts: [
|
|
{ attemptId: 'one', executorType: 'local_process' },
|
|
{ attemptId: 'two', executorType: 'local_process' },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
},
|
|
},
|
|
{
|
|
async consume(attemptId) {
|
|
consumed.push(attemptId);
|
|
if (attemptId === 'failed') throw new Error('invalid receipt');
|
|
if (attemptId === 'missing') {
|
|
return { status: 'missing', cleaned: false };
|
|
}
|
|
if (attemptId === 'terminal') {
|
|
return { status: 'already_terminal', cleaned: true };
|
|
}
|
|
if (attemptId === 'quarantined') {
|
|
return {
|
|
status: 'quarantined',
|
|
cleaned: true,
|
|
quarantineRef: '.quarantine/quarantined.json',
|
|
};
|
|
}
|
|
return {
|
|
status: 'applied',
|
|
cleaned: attemptId !== 'cleanup-pending',
|
|
};
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(await scanner.scanBatch({ limit: 8 }), {
|
|
scanned: 9,
|
|
applied: 2,
|
|
alreadyTerminal: 1,
|
|
quarantined: 1,
|
|
purgedQuarantines: 0,
|
|
expiredMissing: 0,
|
|
missing: 1,
|
|
cleanupPending: 1,
|
|
skipped: 1,
|
|
ambiguous: 2,
|
|
failed: 1,
|
|
truncated: false,
|
|
unsafeAttemptOverflow: false,
|
|
});
|
|
assert.deepEqual(consumed, [
|
|
'applied',
|
|
'terminal',
|
|
'missing',
|
|
'cleanup-pending',
|
|
'failed',
|
|
'quarantined',
|
|
]);
|
|
});
|
|
|
|
test('supervisor aggregates bounded pages and exposes unsafe pagination', async () => {
|
|
const cursors = [];
|
|
const responses = [
|
|
scan({
|
|
applied: 1,
|
|
missing: 0,
|
|
truncated: true,
|
|
nextCursor: { createdAtMs: 10, runId: 'run-1' },
|
|
}),
|
|
scan({ alreadyTerminal: 1, cleanupPending: 1 }),
|
|
];
|
|
const supervisor = new PrimaryCompletionReceiptSupervisor({
|
|
async scanBatch(options) {
|
|
cursors.push(options.cursor);
|
|
return responses.shift();
|
|
},
|
|
});
|
|
assert.deepEqual(await supervisor.run({ pageSize: 8, maxPages: 4 }), {
|
|
pages: 2,
|
|
scanned: 2,
|
|
applied: 1,
|
|
alreadyTerminal: 1,
|
|
quarantined: 0,
|
|
purgedQuarantines: 0,
|
|
expiredMissing: 0,
|
|
missing: 1,
|
|
cleanupPending: 1,
|
|
skipped: 0,
|
|
ambiguous: 0,
|
|
failed: 0,
|
|
stopReason: 'complete',
|
|
remaining: false,
|
|
});
|
|
assert.deepEqual(cursors, [undefined, { createdAtMs: 10, runId: 'run-1' }]);
|
|
|
|
const overflow = new PrimaryCompletionReceiptSupervisor({
|
|
async scanBatch() {
|
|
return scan({ unsafeAttemptOverflow: true, truncated: true });
|
|
},
|
|
});
|
|
assert.equal((await overflow.run()).stopReason, 'unsafe_attempt_overflow');
|
|
await assert.rejects(supervisor.run({ pageSize: 0 }), RangeError);
|
|
await assert.rejects(supervisor.run({ maxPages: 65 }), RangeError);
|
|
});
|
|
|
|
test('lifecycle is explicit, unrefed, and never overlaps a slow scan', async () => {
|
|
const scheduled = [];
|
|
const scheduler = {
|
|
setTimeout(callback, delayMs) {
|
|
const timer = {
|
|
callback,
|
|
delayMs,
|
|
unrefCalled: false,
|
|
unref() {
|
|
this.unrefCalled = true;
|
|
},
|
|
};
|
|
scheduled.push(timer);
|
|
return timer;
|
|
},
|
|
clearTimeout(timer) {
|
|
timer.cleared = true;
|
|
},
|
|
};
|
|
let release;
|
|
let runs = 0;
|
|
const inFlight = new Promise((resolve) => {
|
|
release = resolve;
|
|
});
|
|
const lifecycle = new PrimaryCompletionReceiptLifecycle(
|
|
{
|
|
async run() {
|
|
runs += 1;
|
|
await inFlight;
|
|
return {};
|
|
},
|
|
},
|
|
{ intervalMs: 1_000, scheduler },
|
|
);
|
|
|
|
assert.equal(lifecycle.start(), true);
|
|
assert.equal(lifecycle.start(), false);
|
|
assert.equal(scheduled.length, 1);
|
|
assert.equal(scheduled[0].unrefCalled, true);
|
|
scheduled[0].callback();
|
|
scheduled[0].callback();
|
|
assert.equal(runs, 1);
|
|
release();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(scheduled.length, 2);
|
|
assert.equal(await lifecycle.stop(), 'drained');
|
|
assert.equal(scheduled[1].cleared, true);
|
|
});
|
|
|
|
test('lifecycle resumes after a page limit and resets after reaching the tail', async () => {
|
|
const scheduled = [];
|
|
const scheduler = {
|
|
setTimeout(callback) {
|
|
const timer = { callback, unref() {} };
|
|
scheduled.push(timer);
|
|
return timer;
|
|
},
|
|
clearTimeout() {},
|
|
};
|
|
const cursors = [];
|
|
let runs = 0;
|
|
const lifecycle = new PrimaryCompletionReceiptLifecycle(
|
|
{
|
|
async run(options) {
|
|
runs += 1;
|
|
cursors.push(options.cursor);
|
|
if (runs === 1) {
|
|
return {
|
|
remaining: true,
|
|
nextCursor: { createdAtMs: 10, runId: 'run-10' },
|
|
};
|
|
}
|
|
return { remaining: false };
|
|
},
|
|
},
|
|
{ intervalMs: 1_000, scheduler },
|
|
);
|
|
|
|
lifecycle.start();
|
|
scheduled.shift().callback();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
scheduled.shift().callback();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
scheduled.shift().callback();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
await lifecycle.stop();
|
|
assert.deepEqual(cursors, [
|
|
undefined,
|
|
{ createdAtMs: 10, runId: 'run-10' },
|
|
undefined,
|
|
]);
|
|
});
|