Files
qinglong/packages/ql3-runtime-core/test/pluginPackageInstall.test.cjs

694 lines
21 KiB
JavaScript

const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { join } = require('node:path');
const { test } = require('node:test');
const {
InvalidPluginPackageInstallError,
InvalidPluginPackageLockError,
MAX_PLUGIN_PACKAGE_INSTALL_INVENTORY_PAGE_SIZE,
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE,
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
PluginPackageInstallMutationConflictError,
PluginPackageInstallTransitionConflictError,
assertPluginPackageInstallInventoryPageSize,
assertPluginPackageInstallRecoveryPageSize,
createPluginPackageInstall,
createPluginPackageLock,
normalizePluginPackageInstallRecord,
normalizePluginPackageInstallInventoryCursor,
normalizePluginPackageInstallRecoveryCursor,
normalizePluginPackageInstallCreate,
normalizePluginPackageLock,
planPluginPackageInstall,
pluginPackageInstallActionDigest,
pluginPackageActivationIntentDigest,
pluginPackageInstallCommit,
pluginPackageInstallCreate,
pluginPackageInstallPlanDigest,
pluginPackageInstallRecoveryAction,
transitionPluginPackageInstall,
} = require('../dist');
test('bounds and canonicalizes current installation inventory pages', () => {
assert.doesNotThrow(() =>
assertPluginPackageInstallInventoryPageSize(
MAX_PLUGIN_PACKAGE_INSTALL_INVENTORY_PAGE_SIZE,
),
);
assert.throws(
() =>
assertPluginPackageInstallInventoryPageSize(
MAX_PLUGIN_PACKAGE_INSTALL_INVENTORY_PAGE_SIZE + 1,
),
InvalidPluginPackageInstallError,
);
const cursor = normalizePluginPackageInstallInventoryCursor({
packageName: 'example-monitor',
});
assert.deepEqual(cursor, { packageName: 'example-monitor' });
assert.equal(Object.isFrozen(cursor), true);
assert.throws(
() =>
normalizePluginPackageInstallInventoryCursor({
packageName: '../escape',
}),
InvalidPluginPackageInstallError,
);
assert.throws(
() =>
normalizePluginPackageInstallInventoryCursor({
packageName: 'example-monitor',
installationId: 'unexpected',
}),
InvalidPluginPackageInstallError,
);
});
const ARTIFACT_DIGEST = 'a'.repeat(64);
const OCI_MANIFEST_DIGEST = 'f'.repeat(64);
const CONTENT_DIGEST = 'b'.repeat(64);
const PREVIOUS_LOCK_DIGEST = 'c'.repeat(64);
function manifest(overrides = {}) {
const value = {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: 'example-monitor',
displayName: 'Example Monitor',
version: '1.2.0',
description: 'Collects one bounded report',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64', 'amd64'],
deploymentProfiles: ['standalone', 'edge'],
},
runtimes: [{ name: 'python', version: '>=3.10.0 <4.0.0' }],
resources: {
memory: { recommended: '128Mi' },
disk: { install: '20Mi', working: '100Mi' },
},
permissions: {
network: { allowedHosts: ['api.example.com'] },
secrets: [{ name: 'EXAMPLE_TOKEN', required: true }],
tools: ['notification.send'],
},
contents: {
tasks: ['tasks/collect.yaml'],
workflows: ['workflows/daily-report.yaml'],
prompts: ['prompts/analyze-error.md'],
tools: ['tools/query-data.yaml'],
},
},
};
return {
...value,
...overrides,
metadata: { ...value.metadata, ...overrides.metadata },
spec: {
...value.spec,
...overrides.spec,
compatibility: {
...value.spec.compatibility,
...overrides.spec?.compatibility,
},
resources: {
...value.spec.resources,
...overrides.spec?.resources,
memory: {
...value.spec.resources.memory,
...overrides.spec?.resources?.memory,
},
disk: {
...value.spec.resources.disk,
...overrides.spec?.resources?.disk,
},
},
permissions: {
...value.spec.permissions,
...overrides.spec?.permissions,
network: {
...value.spec.permissions.network,
...overrides.spec?.permissions?.network,
},
},
contents: {
...value.spec.contents,
...overrides.spec?.contents,
},
},
};
}
function environment(overrides = {}) {
return {
qinglongVersion: '3.0.0-alpha.0',
architecture: 'arm64',
deploymentProfile: 'edge',
runtimes: [{ name: 'python', version: '3.12.4' }],
availableMemoryBytes: 256 * 1024 * 1024,
availableDiskBytes: 512 * 1024 * 1024,
...overrides,
};
}
function lockInput(overrides = {}) {
const candidate = overrides.manifest ?? manifest();
const previous = overrides.previousManifest;
const installEnvironment = {
...environment(),
...overrides.environment,
};
const plan =
overrides.plan ??
planPluginPackageInstall(candidate, installEnvironment, previous);
const operation = plan.operation;
const targetGeneration =
overrides.targetGeneration ?? (operation === 'install' ? 1 : 2);
const previousLockDigest =
overrides.previousLockDigest ??
(operation === 'install' ? undefined : PREVIOUS_LOCK_DIGEST);
const source = {
kind: 'oci',
locator: `oci://registry.example.com/qinglong/example-monitor@sha256:${OCI_MANIFEST_DIGEST}`,
artifactDigest: ARTIFACT_DIGEST,
artifactBytes: 1024,
contentDigest: CONTENT_DIGEST,
...overrides.source,
};
const actionInput = {
lockId: overrides.lockId ?? 'lock-001',
projectId: overrides.projectId ?? 'project-001',
manifest: candidate,
plan,
environment: installEnvironment,
...(previous === undefined ? {} : { previousManifest: previous }),
source,
architecture: overrides.architecture ?? 'arm64',
deploymentProfile: overrides.deploymentProfile ?? 'edge',
targetGeneration,
...(previousLockDigest === undefined ? {} : { previousLockDigest }),
};
const actionDigest = pluginPackageInstallActionDigest(actionInput);
const previewDigest = pluginPackageInstallPlanDigest(plan);
return {
...actionInput,
approval: {
requestId: 'approval-001',
requestVersion: 2,
dispatchId: 'dispatch-001',
actionDigest,
previewDigest,
approvedBy: { type: 'user', id: 'owner-001' },
approvedAtMs: 100,
expiresAtMs: 1_000,
fence: { projectVersion: 3, bindingVersion: 4 },
...overrides.approval,
},
createdAtMs: overrides.createdAtMs ?? 200,
};
}
function installFixture(lockOverrides = {}) {
const lock = createPluginPackageLock(lockInput(lockOverrides));
const install = createPluginPackageInstall(lock, {
installationId: 'install-001',
mutationId: 'mutation-create',
occurredAtMs: 201,
});
return { lock, install };
}
function stageEvent(lock, overrides = {}) {
return {
type: 'stage_completed',
mutationId: 'mutation-stage',
occurredAtMs: 202,
stageRef: 'stage-001',
artifactDigest: lock.source.artifactDigest,
manifestDigest: lock.manifestDigest,
contentDigest: lock.source.contentDigest,
evidenceDigest: 'e'.repeat(64),
...overrides,
};
}
test('creates one immutable OCI PackageLock bound to plan and approval', () => {
const input = lockInput();
const lock = createPluginPackageLock(input);
assert.equal(lock.schema, 'qinglong/plugin-package-lock@v2');
assert.equal(lock.operation, 'install');
assert.equal(lock.targetGeneration, 1);
assert.equal(lock.approval.actionDigest, lock.actionDigest);
assert.equal(lock.approval.previewDigest, lock.planDigest);
assert.match(lock.environmentDigest, /^[0-9a-f]{64}$/);
assert.match(lock.lockDigest, /^[0-9a-f]{64}$/);
assert.equal(Object.isFrozen(lock), true);
assert.equal(Object.isFrozen(lock.source), true);
assert.deepEqual(lock.resources, [
{ kind: 'prompt', path: 'prompts/analyze-error.md' },
{ kind: 'task', path: 'tasks/collect.yaml' },
{ kind: 'tool', path: 'tools/query-data.yaml' },
{ kind: 'workflow', path: 'workflows/daily-report.yaml' },
]);
assert.equal(
lock.source.locator,
`oci://registry.example.com/qinglong/example-monitor@sha256:${OCI_MANIFEST_DIGEST}`,
);
assert.notEqual(OCI_MANIFEST_DIGEST, lock.source.artifactDigest);
assert.deepEqual(normalizePluginPackageLock(lock), lock);
assert.throws(
() =>
normalizePluginPackageLock({
...lock,
schema: 'qinglong/plugin-package-lock@v1',
}),
/lock vocabulary is invalid/,
);
});
test('accepts content-addressed offline bundles without persisting a host path', () => {
const input = lockInput({
source: {
kind: 'offline',
locator: `offline:sha256:${ARTIFACT_DIGEST}`,
},
});
const lock = createPluginPackageLock(input);
assert.equal(lock.source.kind, 'offline');
assert.equal(lock.source.locator, `offline:sha256:${ARTIFACT_DIGEST}`);
assert.equal(lock.source.locator.includes('/'), false);
});
test('rejects mutable sources and offline source digest mismatches', () => {
assert.throws(
() =>
lockInput({
source: {
locator: 'oci://registry.example.com/qinglong/example-monitor:latest',
},
}),
InvalidPluginPackageLockError,
);
assert.throws(
() =>
lockInput({
source: {
kind: 'offline',
locator: `oci://registry.example.com/qinglong/example-monitor@sha256:${'d'.repeat(
64,
)}`,
},
}),
/source locator is not immutable/,
);
assert.throws(
() =>
lockInput({
source: {
kind: 'offline',
locator: `offline:sha256:${'d'.repeat(64)}`,
},
}),
/offline source locator does not match its artifact digest/,
);
for (const locator of [
`oci://registry..example.com/qinglong/example-monitor@sha256:${ARTIFACT_DIGEST}`,
`oci://registry.example.com:99999/qinglong/example-monitor@sha256:${ARTIFACT_DIGEST}`,
]) {
assert.throws(
() => lockInput({ source: { locator } }),
InvalidPluginPackageLockError,
);
}
});
test('rejects expired or digest-detached approval bindings', () => {
const detached = lockInput();
detached.approval.actionDigest = 'd'.repeat(64);
assert.throws(
() => createPluginPackageLock(detached),
/approval is not bound/,
);
const expired = lockInput();
expired.approval.expiresAtMs = expired.createdAtMs;
assert.throws(() => createPluginPackageLock(expired), /is not active/);
const automated = lockInput();
automated.approval.approvedBy = { type: 'agent', id: 'agent-001' };
assert.throws(
() => createPluginPackageLock(automated),
/requires a human approval/,
);
});
test('recomputes the approved plan from the exact environment and previous manifest', () => {
const candidate = manifest();
const reviewed = planPluginPackageInstall(candidate, environment());
assert.throws(
() => lockInput({ plan: { ...reviewed, risk: 'low' } }),
/manifest, environment or previous manifest does not match/,
);
assert.throws(
() => lockInput({ architecture: 'amd64' }),
/does not match the install environment/,
);
});
test('requires a previous immutable lock for upgrade and rollback generations', () => {
const previous = manifest({ metadata: { version: '1.1.0' } });
const upgrade = createPluginPackageLock(
lockInput({ previousManifest: previous }),
);
assert.equal(upgrade.operation, 'upgrade');
assert.equal(upgrade.targetGeneration, 2);
assert.equal(upgrade.previousLockDigest, PREVIOUS_LOCK_DIGEST);
const rollbackManifest = manifest({ metadata: { version: '1.1.0' } });
const rollback = createPluginPackageLock(
lockInput({
manifest: rollbackManifest,
previousManifest: manifest(),
}),
);
assert.equal(rollback.operation, 'rollback');
assert.equal(rollback.previousLockDigest, PREVIOUS_LOCK_DIGEST);
assert.throws(
() =>
createPluginPackageLock(
lockInput({
previousManifest: previous,
previousLockDigest: undefined,
targetGeneration: 1,
}),
),
/operation does not match/,
);
});
test('creates a queued durable record while preserving the previous active lock', () => {
const previous = manifest({ metadata: { version: '1.1.0' } });
const { install } = installFixture({ previousManifest: previous });
assert.equal(install.state, 'queued');
assert.equal(install.version, 1);
assert.equal(install.previousActiveLockDigest, PREVIOUS_LOCK_DIGEST);
assert.equal(install.activeLockDigest, PREVIOUS_LOCK_DIGEST);
assert.equal(pluginPackageInstallRecoveryAction(install), 'resume_stage');
assert.match(install.recordDigest, /^[0-9a-f]{64}$/);
});
test('builds an exact first-create envelope and fences replacement heads', () => {
const { lock, install } = installFixture();
const initial = pluginPackageInstallCreate(lock, install, null);
assert.deepEqual(initial, {
installationId: install.installationId,
mutationId: install.lastMutationId,
mutationDigest: initial.mutationDigest,
expectedHead: null,
lock,
record: install,
});
assert.match(initial.mutationDigest, /^[0-9a-f]{64}$/);
assert.notEqual(initial.mutationDigest, install.lastMutationDigest);
assert.equal(Object.isFrozen(initial), true);
const failed = transitionPluginPackageInstall(lock, install, {
type: 'failed',
mutationId: 'mutation-fail',
occurredAtMs: 202,
reason: 'stage_failed',
});
const replacementLock = createPluginPackageLock(
lockInput({ lockId: 'lock-002' }),
);
const replacement = createPluginPackageInstall(replacementLock, {
installationId: 'install-002',
mutationId: 'mutation-retry',
occurredAtMs: 203,
});
const retry = pluginPackageInstallCreate(
replacementLock,
replacement,
failed,
);
assert.deepEqual(retry.expectedHead, {
installationId: failed.installationId,
version: failed.version,
recordDigest: failed.recordDigest,
});
assert.throws(
() => pluginPackageInstallCreate(replacementLock, replacement, install),
PluginPackageInstallTransitionConflictError,
);
assert.throws(
() =>
pluginPackageInstallCreate(
replacementLock,
{ ...replacement, lastMutationDigest: 'f'.repeat(64) },
failed,
),
InvalidPluginPackageInstallError,
);
assert.throws(
() =>
normalizePluginPackageInstallCreate({
...initial,
expectedHead: retry.expectedHead,
}),
InvalidPluginPackageInstallError,
);
});
test('records exact staging evidence before activation can begin', () => {
const { lock, install } = installFixture();
const staged = transitionPluginPackageInstall(
lock,
install,
stageEvent(lock),
);
assert.equal(staged.state, 'staged');
assert.equal(staged.version, 2);
assert.equal(staged.activeLockDigest, null);
assert.equal(staged.stageReceipt.contentDigest, CONTENT_DIGEST);
assert.match(staged.stageReceipt.receiptDigest, /^[0-9a-f]{64}$/);
assert.equal(pluginPackageInstallRecoveryAction(staged), 'resume_activation');
});
test('rejects staging evidence detached from the immutable lock', () => {
const { lock, install } = installFixture();
assert.throws(
() =>
transitionPluginPackageInstall(
lock,
install,
stageEvent(lock, { contentDigest: 'd'.repeat(64) }),
),
PluginPackageInstallTransitionConflictError,
);
});
test('keeps the previous active lock through activating and swaps only on commit', () => {
const previous = manifest({ metadata: { version: '1.1.0' } });
const { lock, install } = installFixture({ previousManifest: previous });
const staged = transitionPluginPackageInstall(
lock,
install,
stageEvent(lock),
);
const activating = transitionPluginPackageInstall(lock, staged, {
type: 'activation_started',
mutationId: 'mutation-activate',
occurredAtMs: 203,
});
assert.equal(activating.state, 'activating');
assert.equal(activating.activeLockDigest, PREVIOUS_LOCK_DIGEST);
assert.equal(
pluginPackageInstallRecoveryAction(activating),
'inspect_activation',
);
const active = transitionPluginPackageInstall(lock, activating, {
type: 'activation_committed',
mutationId: 'mutation-commit',
occurredAtMs: 204,
activationRef: 'activation-generation-2',
intentDigest: pluginPackageActivationIntentDigest(lock, activating),
generation: 2,
contentDigest: CONTENT_DIGEST,
});
assert.equal(active.state, 'active');
assert.equal(active.activeLockDigest, lock.lockDigest);
assert.equal(active.activationReceipt.generation, 2);
assert.equal(pluginPackageInstallRecoveryAction(active), 'none');
});
test('fails closed without replacing the prior active generation', () => {
const previous = manifest({ metadata: { version: '1.1.0' } });
const { lock, install } = installFixture({ previousManifest: previous });
const staged = transitionPluginPackageInstall(
lock,
install,
stageEvent(lock),
);
const failed = transitionPluginPackageInstall(lock, staged, {
type: 'failed',
mutationId: 'mutation-fail',
occurredAtMs: 203,
reason: 'activation_failed',
});
assert.equal(failed.state, 'failed');
assert.equal(failed.activeLockDigest, PREVIOUS_LOCK_DIGEST);
assert.equal(failed.failure.failedFrom, 'staged');
assert.equal(pluginPackageInstallRecoveryAction(failed), 'none');
});
test('makes the last mutation replay idempotent and rejects mutation reuse', () => {
const { lock, install } = installFixture();
const event = stageEvent(lock);
const staged = transitionPluginPackageInstall(lock, install, event);
assert.deepEqual(transitionPluginPackageInstall(lock, staged, event), staged);
assert.throws(
() =>
transitionPluginPackageInstall(lock, staged, {
...event,
stageRef: 'different-stage',
}),
PluginPackageInstallMutationConflictError,
);
});
test('rejects illegal transitions, time reversal and tampered durable records', () => {
const { lock, install } = installFixture();
assert.throws(
() =>
transitionPluginPackageInstall(lock, install, {
type: 'activation_started',
mutationId: 'mutation-skip-stage',
occurredAtMs: 202,
}),
PluginPackageInstallTransitionConflictError,
);
assert.throws(
() =>
transitionPluginPackageInstall(
lock,
install,
stageEvent(lock, { occurredAtMs: 199 }),
),
PluginPackageInstallTransitionConflictError,
);
assert.throws(
() =>
normalizePluginPackageInstallRecord({
...install,
activeLockDigest: lock.lockDigest,
}),
InvalidPluginPackageInstallError,
);
});
test('builds an exact CAS commit envelope for durable adapters', () => {
const { lock, install } = installFixture();
const staged = transitionPluginPackageInstall(
lock,
install,
stageEvent(lock),
);
const commit = pluginPackageInstallCommit(install, staged);
assert.deepEqual(commit, {
installationId: 'install-001',
expectedVersion: 1,
expectedRecordDigest: install.recordDigest,
mutationId: 'mutation-stage',
mutationDigest: staged.lastMutationDigest,
record: staged,
});
assert.equal(Object.isFrozen(commit), true);
const otherLock = createPluginPackageLock(
lockInput({ projectId: 'different-project' }),
);
const otherInstall = createPluginPackageInstall(otherLock, {
installationId: 'install-001',
mutationId: 'mutation-create',
occurredAtMs: 201,
});
const otherStaged = transitionPluginPackageInstall(
otherLock,
otherInstall,
stageEvent(otherLock),
);
assert.throws(
() => pluginPackageInstallCommit(install, otherStaged),
PluginPackageInstallTransitionConflictError,
);
});
test('bounds recovery scans and publishes root plus dedicated subpath', () => {
assert.doesNotThrow(() =>
assertPluginPackageInstallRecoveryPageSize(
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE,
),
);
assert.throws(
() =>
assertPluginPackageInstallRecoveryPageSize(
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE + 1,
),
InvalidPluginPackageInstallError,
);
assert.deepEqual(
normalizePluginPackageInstallRecoveryCursor({
packageName: 'example-monitor',
installationId: 'install-001',
}),
{
packageName: 'example-monitor',
installationId: 'install-001',
},
);
assert.throws(
() =>
normalizePluginPackageInstallRecoveryCursor({
packageName: 'example-monitor',
installationId: '../escape',
}),
InvalidPluginPackageInstallError,
);
const root = require('../dist');
const subpath = require('@qinglong/runtime-core/plugin-package-install');
assert.equal(
subpath.transitionPluginPackageInstall,
root.transitionPluginPackageInstall,
);
assert.equal(subpath.createPluginPackageLock, createPluginPackageLock);
const source = readFileSync(
join(
__dirname,
'../src/plugin-package/installation/pluginPackageInstall.ts',
),
'utf8',
);
for (const forbidden of [
"from 'node:child_process'",
"from 'node:fs'",
"from 'node:http'",
"from 'node:https'",
"from 'node:net'",
"from 'node:timers'",
'setInterval(',
'setTimeout(',
'fetch(',
]) {
assert.equal(source.includes(forbidden), false, forbidden);
}
});