Files
qinglong/test/back/runDispatchLeaseExpiryScanner.test.cjs
T

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/,
);
});