mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 18:08:20 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user