mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
480 lines
15 KiB
JavaScript
480 lines
15 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { createHash } = require('node:crypto');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
PLUGIN_PACKAGE_API_VERSION,
|
|
PLUGIN_PACKAGE_KIND,
|
|
planPluginPackageInstall,
|
|
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackage');
|
|
const {
|
|
createPluginPackageLock,
|
|
createPluginPackageInstall,
|
|
pluginPackageActivationIntentDigest,
|
|
pluginPackageInstallActionDigest,
|
|
pluginPackageInstallCommit,
|
|
pluginPackageInstallCreate,
|
|
pluginPackageInstallPlanDigest,
|
|
serializePluginPackageManifest,
|
|
transitionPluginPackageInstall,
|
|
} = require('../../packages/ql3-runtime-core/dist/plugin-package/installation/pluginPackageInstall');
|
|
const {
|
|
pluginPackageContentTreeDigest,
|
|
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageBundle');
|
|
const {
|
|
createPluginPackagePublisherProvenance,
|
|
} = require('../../packages/ql3-runtime-core/dist/plugin-package/publisher/pluginPackagePublisherProvenance');
|
|
const {
|
|
createPluginPackageResourceGenerationFromReferences,
|
|
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageResourceGeneration');
|
|
const {
|
|
materializePluginPackageResources,
|
|
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageResourceMaterialization');
|
|
const {
|
|
createBuiltInTaskSpecSemanticRegistry,
|
|
} = require('../../packages/ql3-runtime-core/dist/task-definition/taskSpecSemantic');
|
|
|
|
function environment(profile) {
|
|
return {
|
|
qinglongVersion: '3.0.0-alpha.0',
|
|
architecture: 'arm64',
|
|
deploymentProfile: profile,
|
|
runtimes: [],
|
|
availableMemoryBytes: 128 * 1024 * 1024,
|
|
availableDiskBytes: 256 * 1024 * 1024,
|
|
};
|
|
}
|
|
|
|
function taskValue(id, argument = id) {
|
|
const command =
|
|
typeof argument === 'string'
|
|
? {
|
|
kind: 'argv',
|
|
file: '/usr/bin/printf',
|
|
args: [argument],
|
|
}
|
|
: argument;
|
|
return {
|
|
schema: 'qinglong/plugin-package-task-resource@v1',
|
|
id,
|
|
name: `Task ${id}`,
|
|
labels: { 'plugin.qinglong.io/source': 'contract' },
|
|
enabled: true,
|
|
kind: 'command',
|
|
spec: {
|
|
schema: 'qinglong/command@v1',
|
|
config: {
|
|
command,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function activeRecord(lock, namespace, previousHead = null) {
|
|
const queued = createPluginPackageInstall(lock, {
|
|
installationId: `install-${namespace}-${lock.targetGeneration}`,
|
|
mutationId: `create-${namespace}-${lock.targetGeneration}`,
|
|
occurredAtMs: 1_000 + lock.targetGeneration * 10,
|
|
});
|
|
const staged = transitionPluginPackageInstall(lock, queued, {
|
|
type: 'stage_completed',
|
|
mutationId: `stage-${namespace}-${lock.targetGeneration}`,
|
|
occurredAtMs: queued.updatedAtMs + 1,
|
|
stageRef: `stage:${lock.lockDigest}`,
|
|
artifactDigest: lock.source.artifactDigest,
|
|
manifestDigest: lock.manifestDigest,
|
|
contentDigest: lock.source.contentDigest,
|
|
evidenceDigest: createHash('sha256')
|
|
.update(`evidence-${namespace}-${lock.targetGeneration}`)
|
|
.digest('hex'),
|
|
});
|
|
const activating = transitionPluginPackageInstall(lock, staged, {
|
|
type: 'activation_started',
|
|
mutationId: `activate-${namespace}-${lock.targetGeneration}`,
|
|
occurredAtMs: staged.updatedAtMs + 1,
|
|
});
|
|
const active = transitionPluginPackageInstall(lock, activating, {
|
|
type: 'activation_committed',
|
|
mutationId: `commit-${namespace}-${lock.targetGeneration}`,
|
|
occurredAtMs: activating.updatedAtMs + 1,
|
|
activationRef: `activation:${lock.lockDigest}`,
|
|
intentDigest: pluginPackageActivationIntentDigest(lock, activating),
|
|
generation: lock.targetGeneration,
|
|
contentDigest: lock.source.contentDigest,
|
|
});
|
|
return {
|
|
queued,
|
|
staged,
|
|
activating,
|
|
active,
|
|
create: pluginPackageInstallCreate(lock, queued, previousHead),
|
|
commits: [
|
|
pluginPackageInstallCommit(queued, staged),
|
|
pluginPackageInstallCommit(staged, activating),
|
|
pluginPackageInstallCommit(activating, active),
|
|
],
|
|
};
|
|
}
|
|
|
|
function fixture(namespace, options = {}) {
|
|
const profile = options.profile ?? 'edge';
|
|
const previous = options.previous ?? null;
|
|
const generation = previous ? previous.generation + 1 : 1;
|
|
const packageName = `package-${namespace}`;
|
|
const projectId = `project-${namespace}`;
|
|
const tasks = options.tasks ?? [
|
|
['alpha', 'alpha'],
|
|
['beta', 'beta'],
|
|
];
|
|
const workflows = options.workflows ?? [];
|
|
const prompts = options.prompts ?? [];
|
|
const tools = options.tools ?? [];
|
|
const taskResources = tasks.map(([id, argument]) => {
|
|
const path = `tasks/${id}.json`;
|
|
const bytes = Buffer.from(JSON.stringify(taskValue(id, argument)));
|
|
return {
|
|
reference: { kind: 'task', path },
|
|
bytes,
|
|
descriptor: {
|
|
path,
|
|
bytes: bytes.byteLength,
|
|
digest: createHash('sha256').update(bytes).digest('hex'),
|
|
},
|
|
};
|
|
});
|
|
const toolResources = tools.map((definition, index) => {
|
|
const path = `tools/tool-${index}.json`;
|
|
const bytes = Buffer.from(
|
|
JSON.stringify({
|
|
schema: 'qinglong/plugin-package-tool-resource@v1',
|
|
definition,
|
|
}),
|
|
);
|
|
return {
|
|
reference: { kind: 'tool', path },
|
|
bytes,
|
|
descriptor: {
|
|
path,
|
|
bytes: bytes.byteLength,
|
|
digest: createHash('sha256').update(bytes).digest('hex'),
|
|
},
|
|
};
|
|
});
|
|
const workflowResources = workflows.map((value) => {
|
|
const path = `workflows/${value.id}.json`;
|
|
const bytes = Buffer.from(JSON.stringify(value));
|
|
return {
|
|
reference: { kind: 'workflow', path },
|
|
bytes,
|
|
descriptor: {
|
|
path,
|
|
bytes: bytes.byteLength,
|
|
digest: createHash('sha256').update(bytes).digest('hex'),
|
|
},
|
|
};
|
|
});
|
|
const promptResources = prompts.map((value) => {
|
|
const path = `prompts/${value.id}.json`;
|
|
const bytes = Buffer.from(JSON.stringify(value));
|
|
return {
|
|
reference: { kind: 'prompt', path },
|
|
bytes,
|
|
descriptor: {
|
|
path,
|
|
bytes: bytes.byteLength,
|
|
digest: createHash('sha256').update(bytes).digest('hex'),
|
|
},
|
|
};
|
|
});
|
|
const resources = [
|
|
...taskResources,
|
|
...workflowResources,
|
|
...promptResources,
|
|
...toolResources,
|
|
].sort((left, right) =>
|
|
left.reference.path.localeCompare(right.reference.path),
|
|
);
|
|
const contentDigest = pluginPackageContentTreeDigest(
|
|
resources.map(({ descriptor }) => descriptor),
|
|
);
|
|
const manifest = {
|
|
apiVersion: PLUGIN_PACKAGE_API_VERSION,
|
|
kind: PLUGIN_PACKAGE_KIND,
|
|
metadata: {
|
|
name: packageName,
|
|
displayName: packageName,
|
|
version: `${generation}.0.0`,
|
|
description: 'Task reconciliation contract package',
|
|
license: 'Apache-2.0',
|
|
},
|
|
spec: {
|
|
compatibility: {
|
|
qinglong: '>=3.0.0-0 <4.0.0',
|
|
architectures: ['arm64'],
|
|
deploymentProfiles: [profile],
|
|
},
|
|
runtimes: [],
|
|
resources: {
|
|
memory: { recommended: '16Mi' },
|
|
disk: { install: '4Mi', working: '8Mi' },
|
|
},
|
|
permissions: {
|
|
network: { allowedHosts: [] },
|
|
secrets: [],
|
|
tools: [
|
|
...new Set([
|
|
'system.command',
|
|
...tools.flatMap(
|
|
({ requiredPermissions = [] }) => requiredPermissions,
|
|
),
|
|
]),
|
|
].sort(),
|
|
},
|
|
contents: {
|
|
tasks: taskResources.map(({ reference }) => reference.path),
|
|
workflows: workflowResources.map(({ reference }) => reference.path),
|
|
prompts: promptResources.map(({ reference }) => reference.path),
|
|
tools: toolResources.map(({ reference }) => reference.path),
|
|
},
|
|
},
|
|
};
|
|
const installEnvironment = environment(profile);
|
|
const plan = planPluginPackageInstall(
|
|
manifest,
|
|
installEnvironment,
|
|
previous?.manifest,
|
|
);
|
|
const action = {
|
|
lockId: `lock-${namespace}-${generation}`,
|
|
projectId,
|
|
manifest,
|
|
plan,
|
|
environment: installEnvironment,
|
|
...(previous ? { previousManifest: previous.manifest } : {}),
|
|
source: {
|
|
kind: 'offline',
|
|
locator: `offline:sha256:${createHash('sha256')
|
|
.update(`artifact-${namespace}-${generation}`)
|
|
.digest('hex')}`,
|
|
artifactDigest: createHash('sha256')
|
|
.update(`artifact-${namespace}-${generation}`)
|
|
.digest('hex'),
|
|
artifactBytes: 2048,
|
|
contentDigest,
|
|
},
|
|
architecture: 'arm64',
|
|
deploymentProfile: profile,
|
|
targetGeneration: generation,
|
|
...(previous ? { previousLockDigest: previous.lock.lockDigest } : {}),
|
|
};
|
|
const lock = createPluginPackageLock({
|
|
...action,
|
|
approval: {
|
|
requestId: `approval-${namespace}-${generation}`,
|
|
requestVersion: 1,
|
|
dispatchId: `dispatch-${namespace}-${generation}`,
|
|
actionDigest: pluginPackageInstallActionDigest(action),
|
|
previewDigest: pluginPackageInstallPlanDigest(plan),
|
|
approvedBy: { type: 'user', id: 'owner-001' },
|
|
approvedAtMs: 100,
|
|
expiresAtMs: 100_000,
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
},
|
|
createdAtMs: 200 + generation,
|
|
});
|
|
const generationRecord = createPluginPackageResourceGenerationFromReferences({
|
|
installationId: `install-${namespace}-${generation}`,
|
|
projectId,
|
|
packageName,
|
|
lockDigest: lock.lockDigest,
|
|
generation,
|
|
previousActiveLockDigest: previous?.lock.lockDigest ?? null,
|
|
contentDigest,
|
|
resources: lock.resources,
|
|
});
|
|
const registry =
|
|
previous?.registry ?? createBuiltInTaskSpecSemanticRegistry();
|
|
const revision = materializePluginPackageResources({
|
|
generation: generationRecord,
|
|
lock,
|
|
manifestBytes: Buffer.from(serializePluginPackageManifest(manifest)),
|
|
resources: resources.map(({ reference, bytes }) => ({ reference, bytes })),
|
|
taskSpecSemanticRegistry: registry,
|
|
});
|
|
const install = activeRecord(
|
|
lock,
|
|
namespace,
|
|
previous?.install.active ?? null,
|
|
);
|
|
return {
|
|
namespace,
|
|
profile,
|
|
projectId,
|
|
packageName,
|
|
generation,
|
|
manifest,
|
|
lock,
|
|
revision,
|
|
registry,
|
|
install,
|
|
manifestBytes: Buffer.from(serializePluginPackageManifest(manifest)),
|
|
resourceEntries: resources.map(({ reference, bytes }) => ({
|
|
reference,
|
|
bytes,
|
|
})),
|
|
};
|
|
}
|
|
|
|
async function activateInstall(repository, value) {
|
|
await repository.create(value.install.create);
|
|
for (const command of value.install.commits) {
|
|
await repository.commit(command);
|
|
}
|
|
}
|
|
|
|
function publisherProvenanceInstallRepository(repository, provenance) {
|
|
return Object.freeze({
|
|
find: (...args) => repository.find(...args),
|
|
findLock: (...args) => repository.findLock(...args),
|
|
create: (...args) => repository.create(...args),
|
|
listRecoveryPage: (...args) => repository.listRecoveryPage(...args),
|
|
async commit(command) {
|
|
if (command.record.state !== 'staged') {
|
|
return repository.commit(command);
|
|
}
|
|
const lock = await repository.findLock(command.record.lockDigest);
|
|
assert.ok(lock);
|
|
assert.ok(command.record.stageReceipt);
|
|
return provenance.commitStage(
|
|
command,
|
|
createPluginPackagePublisherProvenance({
|
|
projectId: command.record.projectId,
|
|
packageName: command.record.packageName,
|
|
installationId: command.record.installationId,
|
|
lockDigest: command.record.lockDigest,
|
|
artifactDigest: command.record.stageReceipt.artifactDigest,
|
|
manifestDigest: command.record.stageReceipt.manifestDigest,
|
|
contentDigest: command.record.stageReceipt.contentDigest,
|
|
stageEvidenceDigest: command.record.stageReceipt.evidenceDigest,
|
|
signature: {
|
|
publisher: 'packages.contract.qinglong.dev',
|
|
keyId: 'contract-key-1',
|
|
signatureDigest: createHash('sha256')
|
|
.update(`signature:${command.record.installationId}`)
|
|
.digest('hex'),
|
|
keyNotBeforeMs: 0,
|
|
keyNotAfterMs: 100_000,
|
|
verifiedAtMs: command.record.updatedAtMs,
|
|
},
|
|
}),
|
|
'cluster',
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
function registerPluginPackageTaskReconciliationRepositoryContract(options) {
|
|
test(`${options.name} reconciles one complete generation and exact replay`, async (t) => {
|
|
const first = fixture(`${options.namespace}-create`, {
|
|
profile: options.profile,
|
|
});
|
|
const harness = await options.createRepository(t, first);
|
|
t.after(() => harness.close?.());
|
|
await activateInstall(harness.installRepository, first);
|
|
await harness.materializedRepository.publish(first.revision);
|
|
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
|
|
candidates: [
|
|
{ projectId: first.projectId, packageName: first.packageName },
|
|
],
|
|
truncated: false,
|
|
});
|
|
const source = {
|
|
async findActiveResourceGeneration() {
|
|
return first.revision.generation;
|
|
},
|
|
};
|
|
const created = await harness.repository.reconcile(first.revision, source);
|
|
assert.equal(created.status, 'created');
|
|
assert.deepEqual(
|
|
created.receipt.items.map(({ disposition }) => disposition),
|
|
['created', 'created'],
|
|
);
|
|
assert.equal(
|
|
(await harness.repository.reconcile(first.revision, source)).status,
|
|
'existing',
|
|
);
|
|
assert.deepEqual(
|
|
await harness.repository.find(first.revision.generation.generationDigest),
|
|
created.receipt,
|
|
);
|
|
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
|
|
candidates: [],
|
|
truncated: false,
|
|
});
|
|
await options.assertGenericWriteRejected?.(harness, first);
|
|
});
|
|
|
|
test(`${options.name} retains, disables and creates as one next generation`, async (t) => {
|
|
const first = fixture(`${options.namespace}-upgrade`, {
|
|
profile: options.profile,
|
|
});
|
|
const harness = await options.createRepository(t, first);
|
|
t.after(() => harness.close?.());
|
|
await activateInstall(harness.installRepository, first);
|
|
await harness.materializedRepository.publish(first.revision);
|
|
await harness.repository.reconcile(first.revision, {
|
|
async findActiveResourceGeneration() {
|
|
return first.revision.generation;
|
|
},
|
|
});
|
|
|
|
const second = fixture(first.namespace, {
|
|
profile: options.profile,
|
|
previous: first,
|
|
tasks: [
|
|
['alpha', 'alpha'],
|
|
['gamma', 'gamma'],
|
|
],
|
|
});
|
|
await activateInstall(harness.installRepository, second);
|
|
await harness.materializedRepository.publish(second.revision);
|
|
const reconciled = await harness.repository.reconcile(second.revision, {
|
|
async findActiveResourceGeneration() {
|
|
return second.revision.generation;
|
|
},
|
|
});
|
|
assert.deepEqual(
|
|
reconciled.receipt.items.map(({ taskId, revision, disposition }) => ({
|
|
taskId,
|
|
revision,
|
|
disposition,
|
|
})),
|
|
[
|
|
{
|
|
taskId: `pkg:${first.packageName}:alpha`,
|
|
revision: 1,
|
|
disposition: 'retained',
|
|
},
|
|
{
|
|
taskId: `pkg:${first.packageName}:beta`,
|
|
revision: 2,
|
|
disposition: 'disabled',
|
|
},
|
|
{
|
|
taskId: `pkg:${first.packageName}:gamma`,
|
|
revision: 1,
|
|
disposition: 'created',
|
|
},
|
|
],
|
|
);
|
|
await options.assertDurableUpgrade?.(harness, second);
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
activateInstall,
|
|
pluginPackageTaskReconciliationFixture: fixture,
|
|
publisherProvenanceInstallRepository,
|
|
registerPluginPackageTaskReconciliationRepositoryContract,
|
|
};
|