mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidPluginPackageTaskPublicationError,
|
||||
PluginPackageTaskPublicationConflictError,
|
||||
PluginPackageTaskPublicationCoordinator,
|
||||
PluginPackageTaskPublicationRecoveryCoordinator,
|
||||
PluginPackageTaskPublicationUnavailableError,
|
||||
} = require('../dist/plugin-package/pluginPackageTaskPublication');
|
||||
const {
|
||||
pluginPackageTaskReconciliationFixture,
|
||||
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
|
||||
|
||||
function authorities(namespace) {
|
||||
const fixture = pluginPackageTaskReconciliationFixture(namespace);
|
||||
let generation = fixture.revision.generation;
|
||||
let durableRevision = null;
|
||||
let durableReceipt = null;
|
||||
let openCount = 0;
|
||||
const bytesByPath = new Map([
|
||||
['package.json', fixture.manifestBytes],
|
||||
...fixture.resourceEntries.map(({ reference, bytes }) => [
|
||||
reference.path,
|
||||
bytes,
|
||||
]),
|
||||
]);
|
||||
const generationSource = {
|
||||
async findActiveResourceGeneration() {
|
||||
return generation;
|
||||
},
|
||||
};
|
||||
const materializedRepository = {
|
||||
async find(digest) {
|
||||
return durableRevision?.generation.generationDigest === digest
|
||||
? durableRevision
|
||||
: null;
|
||||
},
|
||||
async publish(revision) {
|
||||
const status = durableRevision === null ? 'created' : 'existing';
|
||||
durableRevision ??= revision;
|
||||
return { status, revision: durableRevision };
|
||||
},
|
||||
};
|
||||
const reconciliationRepository = {
|
||||
async find(digest) {
|
||||
return durableReceipt?.generationDigest === digest ? durableReceipt : null;
|
||||
},
|
||||
async reconcile(revision) {
|
||||
const status = durableReceipt === null ? 'created' : 'existing';
|
||||
durableReceipt ??= Object.freeze({
|
||||
schema: 'qinglong/plugin-package-task-reconciliation@v1',
|
||||
projectId: revision.generation.projectId,
|
||||
packageName: revision.generation.packageName,
|
||||
generation: revision.generation.generation,
|
||||
generationDigest: revision.generation.generationDigest,
|
||||
materializedRevisionDigest: revision.revisionDigest,
|
||||
lockDigest: revision.generation.lockDigest,
|
||||
previousLockDigest: revision.generation.previousActiveLockDigest,
|
||||
committedAtMs: 1,
|
||||
items: Object.freeze([]),
|
||||
receiptDigest: 'a'.repeat(64),
|
||||
});
|
||||
return { status, receipt: durableReceipt };
|
||||
},
|
||||
};
|
||||
const coordinator = new PluginPackageTaskPublicationCoordinator({
|
||||
generationSource,
|
||||
lockSource: {
|
||||
async findLock(digest) {
|
||||
return digest === fixture.lock.lockDigest ? fixture.lock : null;
|
||||
},
|
||||
},
|
||||
byteSource: {
|
||||
async open() {
|
||||
openCount += 1;
|
||||
let closed = false;
|
||||
return {
|
||||
async read(path, maximumBytes) {
|
||||
assert.equal(closed, false);
|
||||
const value = bytesByPath.get(path);
|
||||
assert.ok(value);
|
||||
assert.ok(value.byteLength <= maximumBytes);
|
||||
return value;
|
||||
},
|
||||
close() {
|
||||
closed = true;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
materializedRepository,
|
||||
reconciliationRepository,
|
||||
taskSpecSemanticRegistry: fixture.registry,
|
||||
});
|
||||
return {
|
||||
fixture,
|
||||
coordinator,
|
||||
materializedRepository,
|
||||
reconciliationRepository,
|
||||
get generation() {
|
||||
return generation;
|
||||
},
|
||||
set generation(value) {
|
||||
generation = value;
|
||||
},
|
||||
get openCount() {
|
||||
return openCount;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('materializes, durably publishes and reconciles one active generation', async () => {
|
||||
const value = authorities('task-publication-create');
|
||||
const created = await value.coordinator.publishActive(
|
||||
value.fixture.projectId,
|
||||
value.fixture.packageName,
|
||||
);
|
||||
assert.equal(created.status, 'current');
|
||||
assert.equal(created.materialized, 'created');
|
||||
assert.equal(created.reconciled, 'created');
|
||||
assert.equal(value.openCount, 1);
|
||||
|
||||
const replay = await value.coordinator.publishActive(
|
||||
value.fixture.projectId,
|
||||
value.fixture.packageName,
|
||||
);
|
||||
assert.equal(replay.status, 'current');
|
||||
assert.equal(replay.materialized, 'existing');
|
||||
assert.equal(replay.reconciled, 'existing');
|
||||
assert.equal(value.openCount, 1);
|
||||
});
|
||||
|
||||
test('reports a final generation switch without treating the old receipt as current', async () => {
|
||||
const value = authorities('task-publication-switch');
|
||||
let observations = 0;
|
||||
const original =
|
||||
value.coordinator;
|
||||
const source = {
|
||||
async findActiveResourceGeneration() {
|
||||
observations += 1;
|
||||
return observations < 4 ? value.generation : null;
|
||||
},
|
||||
};
|
||||
const switched = new PluginPackageTaskPublicationCoordinator({
|
||||
generationSource: source,
|
||||
lockSource: {
|
||||
async findLock() {
|
||||
return value.fixture.lock;
|
||||
},
|
||||
},
|
||||
byteSource: {
|
||||
async open() {
|
||||
return {
|
||||
async read(path) {
|
||||
if (path === 'package.json') return value.fixture.manifestBytes;
|
||||
return value.fixture.resourceEntries.find(
|
||||
({ reference }) => reference.path === path,
|
||||
).bytes;
|
||||
},
|
||||
close() {},
|
||||
};
|
||||
},
|
||||
},
|
||||
materializedRepository: value.materializedRepository,
|
||||
reconciliationRepository: value.reconciliationRepository,
|
||||
taskSpecSemanticRegistry: value.fixture.registry,
|
||||
});
|
||||
assert.ok(original);
|
||||
assert.deepEqual(
|
||||
await switched.publishActive(
|
||||
value.fixture.projectId,
|
||||
value.fixture.packageName,
|
||||
),
|
||||
{
|
||||
status: 'superseded',
|
||||
generationDigest: value.fixture.revision.generation.generationDigest,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('bounded recovery converges pending candidates and probes from the start', async () => {
|
||||
const value = authorities('task-publication-recovery');
|
||||
let pending = [
|
||||
{
|
||||
projectId: value.fixture.projectId,
|
||||
packageName: value.fixture.packageName,
|
||||
},
|
||||
];
|
||||
const source = {
|
||||
async listPendingPage({ limit, after }) {
|
||||
const candidates = pending
|
||||
.filter(
|
||||
(candidate) =>
|
||||
!after ||
|
||||
candidate.projectId > after.projectId ||
|
||||
(candidate.projectId === after.projectId &&
|
||||
candidate.packageName > after.packageName),
|
||||
)
|
||||
.slice(0, limit);
|
||||
return { candidates, truncated: false };
|
||||
},
|
||||
};
|
||||
const publisher = {
|
||||
async publishActive(projectId, packageName) {
|
||||
const result = await value.coordinator.publishActive(projectId, packageName);
|
||||
if (result.status === 'current') pending = [];
|
||||
return result;
|
||||
},
|
||||
};
|
||||
Object.setPrototypeOf(
|
||||
publisher,
|
||||
Object.getPrototypeOf(value.coordinator),
|
||||
);
|
||||
const recovery = new PluginPackageTaskPublicationRecoveryCoordinator({
|
||||
source,
|
||||
publisher,
|
||||
});
|
||||
assert.deepEqual(await recovery.recover({ pageSize: 1, maxPages: 1 }), {
|
||||
pages: 1,
|
||||
scanned: 1,
|
||||
settled: 1,
|
||||
retry: 0,
|
||||
manualRequired: 0,
|
||||
superseded: 0,
|
||||
remaining: false,
|
||||
safeToAdmit: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('recovery keeps conflicts manual and availability failures retryable', async () => {
|
||||
const fixture = pluginPackageTaskReconciliationFixture(
|
||||
'task-publication-errors',
|
||||
);
|
||||
const candidates = [
|
||||
{ projectId: fixture.projectId, packageName: fixture.packageName },
|
||||
{ projectId: fixture.projectId, packageName: 'package-z-retry' },
|
||||
];
|
||||
const publisher = Object.create(
|
||||
PluginPackageTaskPublicationCoordinator.prototype,
|
||||
);
|
||||
publisher.publishActive = async (_projectId, packageName) => {
|
||||
if (packageName === fixture.packageName) {
|
||||
throw new PluginPackageTaskPublicationConflictError('conflict');
|
||||
}
|
||||
throw new PluginPackageTaskPublicationUnavailableError();
|
||||
};
|
||||
const recovery = new PluginPackageTaskPublicationRecoveryCoordinator({
|
||||
source: {
|
||||
async listPendingPage({ limit }) {
|
||||
const page = candidates.slice(0, limit);
|
||||
const truncated = candidates.length > limit;
|
||||
const last = page.at(-1);
|
||||
return {
|
||||
candidates: page,
|
||||
truncated,
|
||||
...(truncated
|
||||
? {
|
||||
next: {
|
||||
projectId: last.projectId,
|
||||
packageName: last.packageName,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
},
|
||||
publisher,
|
||||
});
|
||||
const result = await recovery.recover({ pageSize: 2, maxPages: 1 });
|
||||
assert.equal(result.manualRequired, 1);
|
||||
assert.equal(result.retry, 1);
|
||||
assert.equal(result.remaining, true);
|
||||
assert.equal(result.safeToAdmit, false);
|
||||
await assert.rejects(
|
||||
() => recovery.recover({ pageSize: 0 }),
|
||||
InvalidPluginPackageTaskPublicationError,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user