mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
99 lines
3.3 KiB
JavaScript
99 lines
3.3 KiB
JavaScript
require('ts-node/register/transpile-only');
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
RunDispatchLeaseExpiryScanner,
|
|
} = require('../../back/runtime/application/runDispatchLeaseExpiryScanner');
|
|
|
|
const NOW = 1_761_000_000_000;
|
|
|
|
function candidate(sequence, overrides = {}) {
|
|
return {
|
|
runId: `run-${sequence}`,
|
|
attemptId: `attempt-${sequence}`,
|
|
expiresAtMs: NOW - 100 + sequence,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('isolates one reconciliation failure and exposes a stable resume cursor', async () => {
|
|
const calls = [];
|
|
const scanner = new RunDispatchLeaseExpiryScanner(
|
|
{
|
|
async listExpired(options) {
|
|
calls.push(['list', options]);
|
|
return [candidate(1), candidate(2), candidate(3)];
|
|
},
|
|
},
|
|
{
|
|
async reconcile(runId, attemptId) {
|
|
calls.push(['reconcile', runId, attemptId]);
|
|
if (attemptId === 'attempt-2') throw new Error('transient failure');
|
|
return {
|
|
status:
|
|
attemptId === 'attempt-1' ? 'lost' : 'cancellation_pending',
|
|
};
|
|
},
|
|
},
|
|
{ clock: { now: () => NOW } },
|
|
);
|
|
const summary = await scanner.scan({ limit: 3 });
|
|
assert.equal(summary.scanned, 3);
|
|
assert.equal(summary.counts.lost, 1);
|
|
assert.equal(summary.counts.cancellation_pending, 0);
|
|
assert.equal(summary.failed, 1);
|
|
assert.equal(summary.truncated, true);
|
|
assert.deepEqual(summary.nextCursor, {
|
|
expiresAtMs: NOW - 99,
|
|
attemptId: 'attempt-1',
|
|
});
|
|
assert.equal(calls.filter(([name]) => name === 'reconcile').length, 2);
|
|
});
|
|
|
|
test('rejects oversized, unordered, live and stalled source pages', async () => {
|
|
const service = { async reconcile() { return { status: 'lost' }; } };
|
|
for (const records of [
|
|
[candidate(1), candidate(2)],
|
|
[candidate(2), candidate(1)],
|
|
[candidate(1, { expiresAtMs: NOW + 1 })],
|
|
[candidate(1, { expiresAtMs: NOW - 10, attemptId: 'attempt-0' })],
|
|
]) {
|
|
const scanner = new RunDispatchLeaseExpiryScanner(
|
|
{ async listExpired() { return records; } },
|
|
service,
|
|
{ clock: { now: () => NOW } },
|
|
);
|
|
if (records.length > 1 && records[0].attemptId === 'attempt-1') {
|
|
await assert.rejects(scanner.scan({ limit: 1 }), /exceeded page size/);
|
|
} else if (records[0].expiresAtMs > NOW) {
|
|
await assert.rejects(scanner.scan(), /cursor did not advance/);
|
|
} else if (records[0].attemptId === 'attempt-0') {
|
|
await assert.rejects(
|
|
scanner.scan({
|
|
after: { expiresAtMs: NOW - 10, attemptId: 'attempt-0' },
|
|
}),
|
|
/cursor did not advance/,
|
|
);
|
|
} else {
|
|
await assert.rejects(scanner.scan(), /cursor did not advance/);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('bounds page size and rejects an invalid observation clock', async () => {
|
|
const source = { async listExpired() { return []; } };
|
|
const service = { async reconcile() { return { status: 'not_found' }; } };
|
|
const scanner = new RunDispatchLeaseExpiryScanner(source, service, {
|
|
clock: { now: () => NOW },
|
|
});
|
|
await assert.rejects(scanner.scan({ limit: 0 }), /limit must be between/);
|
|
await assert.rejects(scanner.scan({ limit: 65 }), /limit must be between/);
|
|
await assert.rejects(
|
|
new RunDispatchLeaseExpiryScanner(source, service, {
|
|
clock: { now: () => -1 },
|
|
}).scan(),
|
|
/observedAtMs/,
|
|
);
|
|
});
|