mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
170 lines
4.7 KiB
JavaScript
170 lines
4.7 KiB
JavaScript
require('ts-node/register/transpile-only');
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
WorkerExecutionOfferInboxLifecycle,
|
|
WorkerExecutionOfferInboxRecoveryIncompleteError,
|
|
WorkerExecutionOfferInboxStartupIncompleteError,
|
|
} = require('../../back/runtime/application/workerExecutionOfferInboxLifecycle');
|
|
|
|
const START = 1_760_600_000_000;
|
|
|
|
function session() {
|
|
return {
|
|
id: 'worker-edge',
|
|
sessionId: '019f8000-0000-7000-8000-000000000001',
|
|
generation: 1,
|
|
};
|
|
}
|
|
|
|
function audit(status) {
|
|
return {
|
|
status,
|
|
observedAtMs: START,
|
|
pagesScanned: 1,
|
|
recordsScanned: 0,
|
|
counts: {},
|
|
entries: [],
|
|
};
|
|
}
|
|
|
|
function fixture(status = 'ready', releaseResult = 'released', recovery) {
|
|
const calls = [];
|
|
const ownership = {
|
|
ownershipState() {
|
|
return 'owned';
|
|
},
|
|
async acquireOwnership() {
|
|
calls.push('acquire');
|
|
return 'acquired';
|
|
},
|
|
async releaseOwnership() {
|
|
calls.push('release');
|
|
return releaseResult;
|
|
},
|
|
};
|
|
const auditor = {
|
|
async audit() {
|
|
calls.push('audit');
|
|
return audit(status);
|
|
},
|
|
};
|
|
return {
|
|
calls,
|
|
lifecycle: new WorkerExecutionOfferInboxLifecycle(
|
|
ownership,
|
|
auditor,
|
|
recovery,
|
|
),
|
|
};
|
|
}
|
|
|
|
test('holds one ownership lease after a complete startup audit', async () => {
|
|
const context = fixture();
|
|
const started = await context.lifecycle.start(session());
|
|
assert.equal(started.status, 'ready');
|
|
assert.strictEqual(context.lifecycle.currentAudit(), started.audit);
|
|
assert.equal(
|
|
(await context.lifecycle.start(session())).status,
|
|
'already_started',
|
|
);
|
|
assert.deepEqual(context.calls, ['acquire', 'audit']);
|
|
assert.equal(await context.lifecycle.stop(), 'stopped');
|
|
assert.deepEqual(context.calls, ['acquire', 'audit', 'release']);
|
|
assert.equal(await context.lifecycle.stop(), 'not_started');
|
|
});
|
|
|
|
test('keeps ownership while execution reconciliation is required', async () => {
|
|
const context = fixture('reconciliation_required');
|
|
assert.equal(
|
|
(await context.lifecycle.start(session())).status,
|
|
'reconciliation_required',
|
|
);
|
|
assert.deepEqual(context.calls, ['acquire', 'audit']);
|
|
assert.equal(await context.lifecycle.stop(), 'stopped');
|
|
});
|
|
|
|
test('releases ownership when the startup scan is incomplete or fails', async () => {
|
|
const exhausted = fixture('scan_budget_exhausted');
|
|
await assert.rejects(
|
|
exhausted.lifecycle.start(session()),
|
|
WorkerExecutionOfferInboxStartupIncompleteError,
|
|
);
|
|
assert.deepEqual(exhausted.calls, ['acquire', 'audit', 'release']);
|
|
|
|
const calls = [];
|
|
const failed = new WorkerExecutionOfferInboxLifecycle(
|
|
{
|
|
ownershipState() {
|
|
return 'owned';
|
|
},
|
|
async acquireOwnership() {
|
|
calls.push('acquire');
|
|
return 'acquired';
|
|
},
|
|
async releaseOwnership() {
|
|
calls.push('release');
|
|
return 'released';
|
|
},
|
|
},
|
|
{
|
|
async audit() {
|
|
calls.push('audit');
|
|
throw new Error('corrupt journal page');
|
|
},
|
|
},
|
|
);
|
|
await assert.rejects(failed.start(session()), /corrupt journal page/);
|
|
assert.deepEqual(calls, ['acquire', 'audit', 'release']);
|
|
});
|
|
|
|
test('reports a compromised owner instead of claiming a clean stop', async () => {
|
|
const context = fixture('ready', 'compromised');
|
|
await context.lifecycle.start(session());
|
|
assert.equal(await context.lifecycle.stop(), 'ownership_compromised');
|
|
});
|
|
|
|
test('runs bounded recovery and re-audits before holding ownership', async () => {
|
|
const recoveryCalls = [];
|
|
const context = fixture('reconciliation_required', 'released', {
|
|
async recover(value) {
|
|
recoveryCalls.push(value.status);
|
|
return {
|
|
status: 'recovered',
|
|
actionsPlanned: 1,
|
|
actionsAttempted: 1,
|
|
entries: [],
|
|
};
|
|
},
|
|
});
|
|
const started = await context.lifecycle.start(session());
|
|
assert.equal(started.status, 'reconciliation_required');
|
|
assert.equal(started.recovery.status, 'recovered');
|
|
assert.deepEqual(context.calls, ['acquire', 'audit', 'audit']);
|
|
assert.deepEqual(recoveryCalls, ['reconciliation_required']);
|
|
assert.equal(
|
|
(await context.lifecycle.start(session())).recovery.status,
|
|
'recovered',
|
|
);
|
|
await context.lifecycle.stop();
|
|
});
|
|
|
|
test('releases ownership when the recovery action budget is exhausted', async () => {
|
|
const context = fixture('reconciliation_required', 'released', {
|
|
async recover() {
|
|
return {
|
|
status: 'action_budget_exhausted',
|
|
actionsPlanned: 65,
|
|
actionsAttempted: 0,
|
|
entries: [],
|
|
};
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
context.lifecycle.start(session()),
|
|
WorkerExecutionOfferInboxRecoveryIncompleteError,
|
|
);
|
|
assert.deepEqual(context.calls, ['acquire', 'audit', 'release']);
|
|
});
|