mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
694 lines
20 KiB
JavaScript
694 lines
20 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
PLUGIN_PACKAGE_API_VERSION,
|
|
PLUGIN_PACKAGE_KIND,
|
|
planPluginPackageInstall,
|
|
} = require('../dist/plugin-package/pluginPackage');
|
|
const {
|
|
PluginPackageInstallTransitionConflictError,
|
|
createPluginPackageInstall,
|
|
createPluginPackageLock,
|
|
pluginPackageInstallActionDigest,
|
|
pluginPackageInstallPlanDigest,
|
|
transitionPluginPackageInstall,
|
|
} = require('../dist/plugin-package/installation/pluginPackageInstall');
|
|
const {
|
|
createPluginPackageActivationIntent,
|
|
} = require('../dist/plugin-package/installation/pluginPackageActivation');
|
|
const {
|
|
PluginPackageRecoveryCoordinator,
|
|
} = require('../dist/plugin-package/installation/pluginPackageRecovery');
|
|
const {
|
|
sequencePluginPackageActivationPrerequisites,
|
|
} = require('../dist/plugin-package/installation/pluginPackageInstallation');
|
|
|
|
const ARTIFACT_DIGEST = 'a'.repeat(64);
|
|
const CONTENT_DIGEST = 'b'.repeat(64);
|
|
const PREVIOUS_LOCK_DIGEST = 'c'.repeat(64);
|
|
|
|
function fixture(
|
|
packageName = 'example-monitor',
|
|
installationId = 'install-001',
|
|
options = {},
|
|
) {
|
|
const manifest = {
|
|
apiVersion: PLUGIN_PACKAGE_API_VERSION,
|
|
kind: PLUGIN_PACKAGE_KIND,
|
|
metadata: {
|
|
name: packageName,
|
|
displayName: packageName,
|
|
version: '1.2.0',
|
|
description: 'One bounded package',
|
|
license: 'Apache-2.0',
|
|
},
|
|
spec: {
|
|
compatibility: {
|
|
qinglong: '>=3.0.0-0 <4.0.0',
|
|
architectures: ['arm64'],
|
|
deploymentProfiles: ['edge'],
|
|
},
|
|
runtimes: [],
|
|
resources: {
|
|
memory: { recommended: '16Mi' },
|
|
disk: { install: '4Mi', working: '16Mi' },
|
|
},
|
|
permissions: {
|
|
network: { allowedHosts: [] },
|
|
secrets: [],
|
|
tools: [],
|
|
},
|
|
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
|
|
},
|
|
};
|
|
const environment = {
|
|
qinglongVersion: '3.0.0-alpha.0',
|
|
architecture: 'arm64',
|
|
deploymentProfile: 'edge',
|
|
runtimes: [],
|
|
availableMemoryBytes: 128 * 1024 * 1024,
|
|
availableDiskBytes: 256 * 1024 * 1024,
|
|
};
|
|
const previousManifest = options.upgrade
|
|
? {
|
|
...manifest,
|
|
metadata: { ...manifest.metadata, version: '1.1.0' },
|
|
}
|
|
: undefined;
|
|
const plan = planPluginPackageInstall(
|
|
manifest,
|
|
environment,
|
|
previousManifest,
|
|
);
|
|
const action = {
|
|
lockId: `lock-${packageName}`,
|
|
projectId: 'default',
|
|
manifest,
|
|
plan,
|
|
environment,
|
|
...(previousManifest === undefined ? {} : { previousManifest }),
|
|
source: {
|
|
kind: 'offline',
|
|
locator: `offline:sha256:${ARTIFACT_DIGEST}`,
|
|
artifactDigest: ARTIFACT_DIGEST,
|
|
artifactBytes: 2048,
|
|
contentDigest: CONTENT_DIGEST,
|
|
},
|
|
architecture: 'arm64',
|
|
deploymentProfile: 'edge',
|
|
targetGeneration: options.upgrade ? 2 : 1,
|
|
...(options.upgrade ? { previousLockDigest: PREVIOUS_LOCK_DIGEST } : {}),
|
|
};
|
|
const lock = createPluginPackageLock({
|
|
...action,
|
|
approval: {
|
|
requestId: `approval-${packageName}`,
|
|
requestVersion: 1,
|
|
dispatchId: `dispatch-${packageName}`,
|
|
actionDigest: pluginPackageInstallActionDigest(action),
|
|
previewDigest: pluginPackageInstallPlanDigest(plan),
|
|
approvedBy: { type: 'user', id: 'owner-001' },
|
|
approvedAtMs: 100,
|
|
expiresAtMs: 10_000,
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
},
|
|
createdAtMs: 200,
|
|
});
|
|
const queued = createPluginPackageInstall(lock, {
|
|
installationId,
|
|
mutationId: `mutation-create-${packageName}`,
|
|
occurredAtMs: 201,
|
|
});
|
|
return { lock, queued };
|
|
}
|
|
|
|
function stageEvidence(lock) {
|
|
return {
|
|
stageRef: `local-stage:${lock.lockDigest}`,
|
|
artifactDigest: lock.source.artifactDigest,
|
|
manifestDigest: lock.manifestDigest,
|
|
contentDigest: lock.source.contentDigest,
|
|
evidenceDigest: 'e'.repeat(64),
|
|
};
|
|
}
|
|
|
|
function stagedFixture(packageName, installationId, options) {
|
|
const value = fixture(packageName, installationId, options);
|
|
const staged = transitionPluginPackageInstall(value.lock, value.queued, {
|
|
type: 'stage_completed',
|
|
mutationId: `mutation-stage-${value.lock.packageName}`,
|
|
occurredAtMs: 202,
|
|
...stageEvidence(value.lock),
|
|
});
|
|
return { ...value, staged };
|
|
}
|
|
|
|
function activatingFixture(packageName, installationId) {
|
|
const value = stagedFixture(packageName, installationId);
|
|
const activating = transitionPluginPackageInstall(value.lock, value.staged, {
|
|
type: 'activation_started',
|
|
mutationId: `mutation-activate-${value.lock.packageName}`,
|
|
occurredAtMs: 203,
|
|
});
|
|
return { ...value, activating };
|
|
}
|
|
|
|
function publishedReceipt(lock, record, activatedAtMs = 204) {
|
|
const intent = createPluginPackageActivationIntent(lock, record);
|
|
return transitionPluginPackageInstall(lock, record, {
|
|
type: 'activation_committed',
|
|
mutationId: 'receipt-preview',
|
|
occurredAtMs: activatedAtMs,
|
|
activationRef: `active:${lock.lockDigest}`,
|
|
intentDigest: intent.intentDigest,
|
|
generation: lock.targetGeneration,
|
|
contentDigest: lock.source.contentDigest,
|
|
}).activationReceipt;
|
|
}
|
|
|
|
function compareRecords(left, right) {
|
|
return (
|
|
left.packageName.localeCompare(right.packageName) ||
|
|
left.installationId.localeCompare(right.installationId)
|
|
);
|
|
}
|
|
|
|
class MemoryRepository {
|
|
constructor(values) {
|
|
this.locks = new Map(values.map(({ lock }) => [lock.lockDigest, lock]));
|
|
this.records = new Map(
|
|
values.map(({ queued, record = queued }) => [
|
|
`${record.projectId}\0${record.packageName}`,
|
|
record,
|
|
]),
|
|
);
|
|
this.commits = [];
|
|
this.commitHook = undefined;
|
|
this.listHook = undefined;
|
|
this.listCalls = 0;
|
|
}
|
|
|
|
key(projectId, packageName) {
|
|
return `${projectId}\0${packageName}`;
|
|
}
|
|
|
|
async find(projectId, packageName) {
|
|
return this.records.get(this.key(projectId, packageName)) ?? null;
|
|
}
|
|
|
|
async findLock(lockDigest) {
|
|
return this.locks.get(lockDigest) ?? null;
|
|
}
|
|
|
|
async commit(command) {
|
|
if (this.commitHook) {
|
|
const hooked = await this.commitHook(command);
|
|
if (hooked) return hooked;
|
|
}
|
|
const key = this.key(command.record.projectId, command.record.packageName);
|
|
const current = this.records.get(key);
|
|
if (
|
|
!current ||
|
|
current.installationId !== command.installationId ||
|
|
current.version !== command.expectedVersion ||
|
|
current.recordDigest !== command.expectedRecordDigest
|
|
) {
|
|
throw new PluginPackageInstallTransitionConflictError();
|
|
}
|
|
this.records.set(key, command.record);
|
|
this.commits.push(command);
|
|
return { status: 'committed', record: command.record };
|
|
}
|
|
|
|
async listRecoveryPage(options) {
|
|
this.listCalls += 1;
|
|
if (this.listHook) await this.listHook(this.listCalls);
|
|
const records = [...this.records.values()]
|
|
.filter((record) =>
|
|
['queued', 'staged', 'activating'].includes(record.state),
|
|
)
|
|
.sort(compareRecords)
|
|
.filter(
|
|
(record) =>
|
|
!options.after ||
|
|
compareRecords(record, {
|
|
packageName: options.after.packageName,
|
|
installationId: options.after.installationId,
|
|
}) > 0,
|
|
);
|
|
const selected = records.slice(0, options.limit);
|
|
const truncated = records.length > selected.length;
|
|
const last = selected.at(-1);
|
|
return {
|
|
records: selected,
|
|
truncated,
|
|
...(truncated
|
|
? {
|
|
next: {
|
|
packageName: last.packageName,
|
|
installationId: last.installationId,
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
}
|
|
|
|
function publisherFor(repository, calls, external = new Map()) {
|
|
return {
|
|
async publish(intent) {
|
|
calls.publish += 1;
|
|
const record = await repository.find(
|
|
intent.projectId,
|
|
intent.packageName,
|
|
);
|
|
const lock = await repository.findLock(record.lockDigest);
|
|
const receipt = publishedReceipt(lock, record, 300 + calls.publish);
|
|
external.set(intent.intentDigest, receipt);
|
|
return receipt;
|
|
},
|
|
async inspect(intent) {
|
|
calls.inspect += 1;
|
|
const receipt = external.get(intent.intentDigest);
|
|
return receipt
|
|
? { status: 'published', receipt }
|
|
: { status: 'not_published' };
|
|
},
|
|
};
|
|
}
|
|
|
|
test('recovers queued install through stage and activation without consuming approval', async () => {
|
|
const value = fixture();
|
|
const repository = new MemoryRepository([value]);
|
|
const calls = { stage: 0, publish: 0, inspect: 0 };
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: {
|
|
async stage(lock) {
|
|
calls.stage += 1;
|
|
return stageEvidence(lock);
|
|
},
|
|
},
|
|
publisher: publisherFor(repository, calls),
|
|
now: async () => 250,
|
|
});
|
|
|
|
const cycle = await coordinator.recover({ pageSize: 1, maxPages: 2 });
|
|
|
|
assert.deepEqual(cycle, {
|
|
pages: 1,
|
|
scanned: 1,
|
|
settled: 1,
|
|
retry: 0,
|
|
manualRequired: 0,
|
|
superseded: 0,
|
|
deferred: 0,
|
|
remaining: false,
|
|
safeToAdmit: true,
|
|
});
|
|
assert.equal(
|
|
(await repository.find('default', 'example-monitor')).state,
|
|
'active',
|
|
);
|
|
assert.deepEqual(calls, { stage: 1, publish: 1, inspect: 0 });
|
|
});
|
|
|
|
test('stages but defers activation until the exact prerequisite is ready', async () => {
|
|
const value = fixture();
|
|
const repository = new MemoryRepository([value]);
|
|
const calls = { stage: 0, publish: 0, inspect: 0, prerequisite: 0 };
|
|
let ready = false;
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: {
|
|
async stage(lock) {
|
|
calls.stage += 1;
|
|
return stageEvidence(lock);
|
|
},
|
|
},
|
|
publisher: publisherFor(repository, calls),
|
|
activationPrerequisite: {
|
|
async inspect() {
|
|
calls.prerequisite += 1;
|
|
return ready
|
|
? { status: 'ready' }
|
|
: {
|
|
status: 'deferred',
|
|
reason: 'secret_binding_transition_required',
|
|
};
|
|
},
|
|
},
|
|
now: () => 250,
|
|
});
|
|
|
|
const deferred = await coordinator.recoverPage({ limit: 1 });
|
|
assert.equal(deferred.items[0].status, 'deferred');
|
|
assert.equal(
|
|
(await repository.find('default', 'example-monitor')).state,
|
|
'staged',
|
|
);
|
|
assert.deepEqual(calls, {
|
|
stage: 1,
|
|
publish: 0,
|
|
inspect: 0,
|
|
prerequisite: 2,
|
|
});
|
|
|
|
ready = true;
|
|
const settled = await coordinator.recoverPage({ limit: 1 });
|
|
assert.equal(settled.items[0].status, 'settled');
|
|
assert.equal(
|
|
(await repository.find('default', 'example-monitor')).state,
|
|
'active',
|
|
);
|
|
assert.deepEqual(calls, {
|
|
stage: 1,
|
|
publish: 1,
|
|
inspect: 0,
|
|
prerequisite: 3,
|
|
});
|
|
});
|
|
|
|
test('orders activation prerequisites and stops before later authorities', async () => {
|
|
const value = stagedFixture();
|
|
const calls = [];
|
|
let first = {
|
|
status: 'deferred',
|
|
reason: 'secret_binding_transition_required',
|
|
};
|
|
const sequence = sequencePluginPackageActivationPrerequisites([
|
|
{
|
|
async inspect() {
|
|
calls.push('secret');
|
|
return first;
|
|
},
|
|
},
|
|
{
|
|
async inspect() {
|
|
calls.push('candidate');
|
|
return {
|
|
status: 'rejected',
|
|
reason: 'activation_fact_conflict',
|
|
};
|
|
},
|
|
},
|
|
]);
|
|
|
|
assert.deepEqual(await sequence.inspect(value.staged, value.lock), first);
|
|
assert.deepEqual(calls, ['secret']);
|
|
|
|
first = { status: 'ready' };
|
|
assert.deepEqual(await sequence.inspect(value.staged, value.lock), {
|
|
status: 'rejected',
|
|
reason: 'activation_fact_conflict',
|
|
});
|
|
assert.deepEqual(calls, ['secret', 'secret', 'candidate']);
|
|
});
|
|
|
|
test('rejects an invalid upgrade before publication and retains the previous active lock', async () => {
|
|
const value = stagedFixture('example-monitor', 'install-upgrade', {
|
|
upgrade: true,
|
|
});
|
|
const repository = new MemoryRepository([{ ...value, record: value.staged }]);
|
|
const calls = { stage: 0, publish: 0, inspect: 0 };
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: {
|
|
async stage() {
|
|
calls.stage += 1;
|
|
throw new Error('stage must not run');
|
|
},
|
|
},
|
|
publisher: publisherFor(repository, calls),
|
|
activationPrerequisite: {
|
|
async inspect() {
|
|
return {
|
|
status: 'rejected',
|
|
reason: 'activation_fact_conflict',
|
|
};
|
|
},
|
|
},
|
|
now: () => 250,
|
|
});
|
|
|
|
const page = await coordinator.recoverPage({ limit: 1 });
|
|
const durable = await repository.find('default', 'example-monitor');
|
|
|
|
assert.equal(page.items[0].status, 'settled');
|
|
assert.equal(durable.state, 'failed');
|
|
assert.equal(durable.previousActiveLockDigest, PREVIOUS_LOCK_DIGEST);
|
|
assert.equal(durable.activeLockDigest, PREVIOUS_LOCK_DIGEST);
|
|
assert.equal(durable.failure.reason, 'activation_fact_conflict');
|
|
assert.equal(durable.failure.failedFrom, 'staged');
|
|
assert.deepEqual(calls, { stage: 0, publish: 0, inspect: 0 });
|
|
});
|
|
|
|
test('inspects an activating install without republishing it', async () => {
|
|
const value = activatingFixture();
|
|
const repository = new MemoryRepository([
|
|
{ ...value, record: value.activating },
|
|
]);
|
|
const calls = { stage: 0, publish: 0, inspect: 0 };
|
|
const intent = createPluginPackageActivationIntent(
|
|
value.lock,
|
|
value.activating,
|
|
);
|
|
const external = new Map([
|
|
[intent.intentDigest, publishedReceipt(value.lock, value.activating)],
|
|
]);
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: {
|
|
async stage() {
|
|
calls.stage += 1;
|
|
throw new Error('stage must not run');
|
|
},
|
|
},
|
|
publisher: publisherFor(repository, calls, external),
|
|
now: () => 250,
|
|
});
|
|
|
|
const page = await coordinator.recoverPage({ limit: 1 });
|
|
|
|
assert.equal(page.items[0].status, 'settled');
|
|
assert.equal(page.items[0].action, 'inspect_activation');
|
|
assert.equal(
|
|
(await repository.find('default', 'example-monitor')).state,
|
|
'active',
|
|
);
|
|
assert.deepEqual(calls, { stage: 0, publish: 0, inspect: 1 });
|
|
});
|
|
|
|
test('recovers publication response loss by inspecting on the next pass', async () => {
|
|
const value = stagedFixture();
|
|
const repository = new MemoryRepository([{ ...value, record: value.staged }]);
|
|
const calls = { stage: 0, publish: 0, inspect: 0 };
|
|
const external = new Map();
|
|
const basePublisher = publisherFor(repository, calls, external);
|
|
let loseResponse = true;
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: {
|
|
async stage() {
|
|
throw new Error('stage must not run');
|
|
},
|
|
},
|
|
publisher: {
|
|
async publish(intent) {
|
|
const receipt = await basePublisher.publish(intent);
|
|
if (loseResponse) {
|
|
loseResponse = false;
|
|
throw new Error('simulated response loss');
|
|
}
|
|
return receipt;
|
|
},
|
|
inspect: basePublisher.inspect,
|
|
},
|
|
now: () => 250,
|
|
});
|
|
|
|
const first = await coordinator.recover();
|
|
assert.equal(first.retry, 1);
|
|
assert.equal(first.safeToAdmit, false);
|
|
assert.equal(
|
|
(await repository.find('default', 'example-monitor')).state,
|
|
'activating',
|
|
);
|
|
|
|
const second = await coordinator.recover();
|
|
assert.equal(second.settled, 1);
|
|
assert.equal(second.safeToAdmit, true);
|
|
assert.equal(
|
|
(await repository.find('default', 'example-monitor')).state,
|
|
'active',
|
|
);
|
|
assert.deepEqual(calls, { stage: 0, publish: 1, inspect: 1 });
|
|
});
|
|
|
|
test('does not inspect or publish when another recovery advances queued state', async () => {
|
|
const value = fixture();
|
|
const repository = new MemoryRepository([value]);
|
|
const calls = { stage: 0, publish: 0, inspect: 0 };
|
|
repository.commitHook = async (command) => {
|
|
if (command.record.state !== 'staged') return undefined;
|
|
const activating = transitionPluginPackageInstall(
|
|
value.lock,
|
|
command.record,
|
|
{
|
|
type: 'activation_started',
|
|
mutationId: 'other-recovery-activation',
|
|
occurredAtMs: 251,
|
|
},
|
|
);
|
|
repository.records.set(
|
|
repository.key(activating.projectId, activating.packageName),
|
|
activating,
|
|
);
|
|
repository.commitHook = undefined;
|
|
return { status: 'existing', record: activating };
|
|
};
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: {
|
|
async stage(lock) {
|
|
calls.stage += 1;
|
|
return stageEvidence(lock);
|
|
},
|
|
},
|
|
publisher: publisherFor(repository, calls),
|
|
now: () => 250,
|
|
});
|
|
|
|
const page = await coordinator.recoverPage({ limit: 1 });
|
|
|
|
assert.equal(page.items[0].status, 'retry');
|
|
assert.equal(page.items[0].state, 'activating');
|
|
assert.deepEqual(calls, { stage: 1, publish: 0, inspect: 0 });
|
|
});
|
|
|
|
test('keeps unavailable stage recoverable and blocks admission', async () => {
|
|
const value = fixture();
|
|
const repository = new MemoryRepository([value]);
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: {
|
|
async stage() {
|
|
throw new Error('artifact store unavailable');
|
|
},
|
|
},
|
|
publisher: publisherFor(repository, { publish: 0, inspect: 0 }),
|
|
now: () => 250,
|
|
});
|
|
|
|
const cycle = await coordinator.recover();
|
|
|
|
assert.equal(cycle.retry, 1);
|
|
assert.equal(cycle.remaining, true);
|
|
assert.equal(cycle.safeToAdmit, false);
|
|
assert.equal(
|
|
(await repository.find('default', 'example-monitor')).state,
|
|
'queued',
|
|
);
|
|
});
|
|
|
|
test('marks invalid durable stage evidence for manual recovery', async () => {
|
|
const value = fixture();
|
|
const repository = new MemoryRepository([value]);
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: {
|
|
async stage(lock) {
|
|
return { ...stageEvidence(lock), evidenceDigest: 'not-a-digest' };
|
|
},
|
|
},
|
|
publisher: publisherFor(repository, { publish: 0, inspect: 0 }),
|
|
now: () => 250,
|
|
});
|
|
|
|
const cycle = await coordinator.recover();
|
|
|
|
assert.equal(cycle.manualRequired, 1);
|
|
assert.equal(cycle.safeToAdmit, false);
|
|
});
|
|
|
|
test('uses a final head probe to catch recovery work inserted before the cursor', async () => {
|
|
const first = stagedFixture('middle-package', 'install-middle');
|
|
const last = stagedFixture('zulu-package', 'install-zulu');
|
|
const inserted = stagedFixture('alpha-package', 'install-alpha');
|
|
const repository = new MemoryRepository([
|
|
{ ...first, record: first.staged },
|
|
{ ...last, record: last.staged },
|
|
]);
|
|
const calls = { stage: 0, publish: 0, inspect: 0 };
|
|
repository.listHook = async (listCall) => {
|
|
if (listCall !== 2) return;
|
|
repository.locks.set(inserted.lock.lockDigest, inserted.lock);
|
|
repository.records.set(
|
|
repository.key(inserted.staged.projectId, inserted.staged.packageName),
|
|
inserted.staged,
|
|
);
|
|
};
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: {
|
|
async stage() {
|
|
throw new Error('stage must not run');
|
|
},
|
|
},
|
|
publisher: publisherFor(repository, calls),
|
|
now: () => 250,
|
|
});
|
|
|
|
const cycle = await coordinator.recover({ pageSize: 1, maxPages: 2 });
|
|
|
|
assert.equal(cycle.pages, 2);
|
|
assert.equal(cycle.settled, 2);
|
|
assert.equal(cycle.remaining, true);
|
|
assert.equal(cycle.safeToAdmit, false);
|
|
assert.equal(
|
|
(await repository.find('default', 'alpha-package')).state,
|
|
'staged',
|
|
);
|
|
});
|
|
|
|
test('rejects malformed recovery page ordering and continuation', async () => {
|
|
const first = stagedFixture('alpha-package', 'install-alpha');
|
|
const last = stagedFixture('zulu-package', 'install-zulu');
|
|
const repository = new MemoryRepository([
|
|
{ ...first, record: first.staged },
|
|
{ ...last, record: last.staged },
|
|
]);
|
|
repository.listRecoveryPage = async () => ({
|
|
records: [last.staged, first.staged],
|
|
truncated: true,
|
|
next: {
|
|
packageName: first.staged.packageName,
|
|
installationId: first.staged.installationId,
|
|
},
|
|
});
|
|
const coordinator = new PluginPackageRecoveryCoordinator({
|
|
repository,
|
|
stageProvider: { async stage() {} },
|
|
publisher: {
|
|
async publish() {},
|
|
async inspect() {},
|
|
},
|
|
now: () => 250,
|
|
});
|
|
|
|
await assert.rejects(
|
|
coordinator.recoverPage({ limit: 2 }),
|
|
/recovery page records are invalid/,
|
|
);
|
|
});
|
|
|
|
test('publishes recovery authority only through its explicit subpath', () => {
|
|
assert.equal(require('../dist').PluginPackageRecoveryCoordinator, undefined);
|
|
assert.equal(
|
|
require('@qinglong/runtime-core/plugin-package-recovery')
|
|
.PluginPackageRecoveryCoordinator,
|
|
PluginPackageRecoveryCoordinator,
|
|
);
|
|
});
|