mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
327 lines
8.0 KiB
JavaScript
327 lines
8.0 KiB
JavaScript
require('ts-node/register/transpile-only');
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
activateManualPrimaryRuntime,
|
|
} = require('../../back/runtime/application/manualPrimaryRuntimeActivation');
|
|
const {
|
|
RuntimeRolloutPolicy,
|
|
} = require('../../back/runtime/domain/runtimeRollout');
|
|
|
|
const NOW = 1_750_000_000_000;
|
|
|
|
function loadResult(status, mode = 'off') {
|
|
return {
|
|
status,
|
|
policy: new RuntimeRolloutPolicy({
|
|
defaultMode: 'off',
|
|
origins: mode === 'off' ? {} : { manual: mode },
|
|
allowLegacyFallbackBeforeStart: false,
|
|
}),
|
|
audit: {
|
|
event: 'runtime.rollout_config_evaluated',
|
|
evaluatedAtMs: NOW,
|
|
sourcePath: '/data/config/qinglong3-rollout.json',
|
|
status,
|
|
revision: 'canary-1',
|
|
},
|
|
};
|
|
}
|
|
|
|
function cleanRecovery(overrides = {}) {
|
|
return {
|
|
pages: 1,
|
|
scanned: 0,
|
|
verifiedRunning: 0,
|
|
recoveredRunning: 0,
|
|
completedFromReceipt: 0,
|
|
quarantinedReceipts: 0,
|
|
publishGraceWaits: 0,
|
|
markedLost: 0,
|
|
skipped: 0,
|
|
ambiguous: 0,
|
|
failed: 0,
|
|
stopReason: 'complete',
|
|
remaining: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function router() {
|
|
return {
|
|
ownsNewRuns: () => true,
|
|
async start() {
|
|
throw new Error('not used');
|
|
},
|
|
async stopCron() {
|
|
return { matched: 0, failed: 0 };
|
|
},
|
|
async stopAttempt() {
|
|
return { matched: 0, failed: 0 };
|
|
},
|
|
};
|
|
}
|
|
|
|
function completionLifecycle(calls) {
|
|
return {
|
|
startCompletion() {
|
|
calls.push('start-completion');
|
|
return true;
|
|
},
|
|
async stopCompletion() {
|
|
calls.push('stop-completion');
|
|
return 'drained';
|
|
},
|
|
};
|
|
}
|
|
|
|
test('activation remains inert unless an accepted manifest selects Primary', async () => {
|
|
for (const [status, mode] of [
|
|
['missing', 'off'],
|
|
['disabled', 'off'],
|
|
['rejected', 'off'],
|
|
['accepted', 'shadow'],
|
|
]) {
|
|
const calls = [];
|
|
const result = await activateManualPrimaryRuntime({
|
|
load: async () => loadResult(status, mode),
|
|
create() {
|
|
calls.push('create');
|
|
throw new Error('must remain inert');
|
|
},
|
|
install() {
|
|
calls.push('install');
|
|
return () => undefined;
|
|
},
|
|
audit(record) {
|
|
calls.push(record.activation);
|
|
},
|
|
});
|
|
assert.equal(result.active, false);
|
|
assert.equal(await result.stop(), 'drained');
|
|
assert.deepEqual(calls, ['not_activated']);
|
|
}
|
|
});
|
|
|
|
test('activation reconciles before starting lifecycle and installing ownership', async () => {
|
|
const calls = [];
|
|
const result = await activateManualPrimaryRuntime({
|
|
load: async () => loadResult('accepted', 'primary'),
|
|
create() {
|
|
calls.push('create');
|
|
return {
|
|
router: router(),
|
|
...completionLifecycle(calls),
|
|
async reconcile() {
|
|
calls.push('reconcile');
|
|
return cleanRecovery({
|
|
scanned: 2,
|
|
recoveredRunning: 1,
|
|
markedLost: 1,
|
|
});
|
|
},
|
|
startTimeout() {
|
|
calls.push('start-timeout');
|
|
return true;
|
|
},
|
|
async stopTimeout() {
|
|
calls.push('stop-timeout');
|
|
return 'drained';
|
|
},
|
|
startCancellation() {
|
|
calls.push('start-cancellation');
|
|
return true;
|
|
},
|
|
async stopCancellation() {
|
|
calls.push('stop-cancellation');
|
|
return 'drained';
|
|
},
|
|
};
|
|
},
|
|
install() {
|
|
calls.push('install');
|
|
return () => calls.push('dispose');
|
|
},
|
|
audit(record) {
|
|
calls.push(`audit:${record.activation}`);
|
|
},
|
|
});
|
|
|
|
assert.equal(result.active, true);
|
|
assert.deepEqual(calls, [
|
|
'audit:selected',
|
|
'create',
|
|
'reconcile',
|
|
'audit:reconciled',
|
|
'start-completion',
|
|
'start-timeout',
|
|
'start-cancellation',
|
|
'install',
|
|
'audit:activated',
|
|
]);
|
|
assert.equal(await result.stop(), 'drained');
|
|
assert.equal(await result.stop(), 'drained');
|
|
assert.deepEqual(calls.slice(-5), [
|
|
'dispose',
|
|
'stop-timeout',
|
|
'stop-cancellation',
|
|
'stop-completion',
|
|
'audit:stopped',
|
|
]);
|
|
});
|
|
|
|
test('activation rejects unresolved recovery before starting or installing', async () => {
|
|
const calls = [];
|
|
await assert.rejects(
|
|
activateManualPrimaryRuntime({
|
|
load: async () => loadResult('accepted', 'primary'),
|
|
create() {
|
|
return {
|
|
router: router(),
|
|
...completionLifecycle(calls),
|
|
async reconcile() {
|
|
return cleanRecovery({ ambiguous: 1 });
|
|
},
|
|
startTimeout() {
|
|
calls.push('start-timeout');
|
|
return true;
|
|
},
|
|
async stopTimeout() {
|
|
calls.push('stop-timeout');
|
|
return 'drained';
|
|
},
|
|
startCancellation() {
|
|
calls.push('start');
|
|
return true;
|
|
},
|
|
async stopCancellation() {
|
|
calls.push('stop');
|
|
return 'drained';
|
|
},
|
|
};
|
|
},
|
|
install() {
|
|
calls.push('install');
|
|
return () => calls.push('dispose');
|
|
},
|
|
audit(record) {
|
|
calls.push(`audit:${record.activation}`);
|
|
},
|
|
}),
|
|
/did not converge safely/,
|
|
);
|
|
assert.deepEqual(calls, ['audit:selected', 'audit:failed']);
|
|
});
|
|
|
|
test('activation rolls back router and lifecycle when final audit fails', async () => {
|
|
const calls = [];
|
|
await assert.rejects(
|
|
activateManualPrimaryRuntime({
|
|
load: async () => loadResult('accepted', 'primary'),
|
|
create() {
|
|
return {
|
|
router: router(),
|
|
...completionLifecycle(calls),
|
|
async reconcile() {
|
|
return cleanRecovery();
|
|
},
|
|
startTimeout() {
|
|
calls.push('start-timeout');
|
|
return true;
|
|
},
|
|
async stopTimeout() {
|
|
calls.push('stop-timeout');
|
|
return 'drained';
|
|
},
|
|
startCancellation() {
|
|
calls.push('start');
|
|
return true;
|
|
},
|
|
async stopCancellation() {
|
|
calls.push('stop');
|
|
return 'drained';
|
|
},
|
|
};
|
|
},
|
|
install() {
|
|
calls.push('install');
|
|
return () => calls.push('dispose');
|
|
},
|
|
audit(record) {
|
|
calls.push(`audit:${record.activation}`);
|
|
if (record.activation === 'activated') {
|
|
throw new Error('audit unavailable');
|
|
}
|
|
},
|
|
}),
|
|
/audit unavailable/,
|
|
);
|
|
assert.deepEqual(calls, [
|
|
'audit:selected',
|
|
'audit:reconciled',
|
|
'start-completion',
|
|
'start-timeout',
|
|
'start',
|
|
'install',
|
|
'audit:activated',
|
|
'dispose',
|
|
'stop-timeout',
|
|
'stop',
|
|
'stop-completion',
|
|
'audit:failed',
|
|
]);
|
|
});
|
|
|
|
test('activation stops timeout production when cancellation lifecycle cannot start', async () => {
|
|
const calls = [];
|
|
await assert.rejects(
|
|
activateManualPrimaryRuntime({
|
|
load: async () => loadResult('accepted', 'primary'),
|
|
create() {
|
|
return {
|
|
router: router(),
|
|
...completionLifecycle(calls),
|
|
async reconcile() {
|
|
return cleanRecovery();
|
|
},
|
|
startTimeout() {
|
|
calls.push('start-timeout');
|
|
return true;
|
|
},
|
|
async stopTimeout() {
|
|
calls.push('stop-timeout');
|
|
return 'drained';
|
|
},
|
|
startCancellation() {
|
|
calls.push('start-cancellation');
|
|
return false;
|
|
},
|
|
async stopCancellation() {
|
|
calls.push('stop-cancellation');
|
|
return 'drained';
|
|
},
|
|
};
|
|
},
|
|
install() {
|
|
calls.push('install');
|
|
return () => calls.push('dispose');
|
|
},
|
|
audit(record) {
|
|
calls.push(`audit:${record.activation}`);
|
|
},
|
|
}),
|
|
/cancellation lifecycle did not start/,
|
|
);
|
|
assert.deepEqual(calls, [
|
|
'audit:selected',
|
|
'audit:reconciled',
|
|
'start-completion',
|
|
'start-timeout',
|
|
'start-cancellation',
|
|
'stop-timeout',
|
|
'stop-completion',
|
|
'audit:failed',
|
|
]);
|
|
});
|