Files

366 lines
9.9 KiB
JavaScript

const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { join } = require('node:path');
const { test } = require('node:test');
const {
InvalidPluginPackageInstallEnvironmentError,
InvalidPluginPackageManifestError,
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
normalizePluginPackageManifest,
planPluginPackageInstall,
} = require('../dist/plugin-package/pluginPackage');
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 a 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,
};
}
test('normalizes and deeply freezes one bounded Package manifest', () => {
const normalized = normalizePluginPackageManifest(manifest());
assert.equal(Object.isFrozen(normalized), true);
assert.equal(Object.isFrozen(normalized.spec.permissions.secrets), true);
assert.deepEqual(normalized.spec.compatibility.architectures, [
'amd64',
'arm64',
]);
assert.deepEqual(normalized.spec.compatibility.deploymentProfiles, [
'edge',
'standalone',
]);
assert.deepEqual(normalized.spec.contents, {
tasks: ['tasks/collect.yaml'],
workflows: ['workflows/daily-report.yaml'],
prompts: ['prompts/analyze-error.md'],
tools: ['tools/query-data.yaml'],
});
});
test('keeps the reviewed Node 24 candidate architecture vocabulary', () => {
const normalized = normalizePluginPackageManifest(
manifest({
spec: {
compatibility: {
architectures: ['s390x', 'ppc64le', 'arm/v7', 'arm64', 'amd64'],
},
},
}),
);
assert.deepEqual(normalized.spec.compatibility.architectures, [
'amd64',
'arm/v7',
'arm64',
'ppc64le',
's390x',
]);
});
test('publishes the contract through the root and plugin-package subpath', () => {
const root = require('../dist');
const subpath = require('@qinglong/runtime-core/plugin-package');
assert.equal(
root.normalizePluginPackageManifest,
normalizePluginPackageManifest,
);
assert.equal(subpath.planPluginPackageInstall, planPluginPackageInstall);
});
test('rejects unknown fields, unsupported kinds and non-canonical versions', () => {
assert.throws(
() => normalizePluginPackageManifest({ ...manifest(), extra: true }),
InvalidPluginPackageManifestError,
);
assert.throws(
() => normalizePluginPackageManifest(manifest({ kind: 'Extension' })),
/apiVersion or kind is unsupported/,
);
assert.throws(
() =>
normalizePluginPackageManifest(
manifest({ metadata: { version: 'v1.2.0' } }),
),
/metadata version is invalid/,
);
});
test('rejects traversal, wildcard hosts, duplicate authority and core migrations', () => {
const invalid = [
manifest({ spec: { contents: { tasks: ['tasks/../migration.sql'] } } }),
manifest({
spec: {
contents: {
tasks: ['migrations/0001.sql'],
},
},
}),
manifest({
spec: {
permissions: {
network: { allowedHosts: ['*.example.com'] },
},
},
}),
manifest({
spec: {
permissions: {
secrets: [
{ name: 'TOKEN', required: true },
{ name: 'TOKEN', required: false },
],
},
},
}),
manifest({
spec: {
runtimes: [
{ name: 'python', version: '>=3.10.0' },
{ name: 'python', version: '>=3.11.0' },
],
},
}),
];
for (const value of invalid) {
assert.throws(
() => normalizePluginPackageManifest(value),
InvalidPluginPackageManifestError,
);
}
});
test('rejects unreviewed profiles, architectures, permissions and resource units', () => {
const invalid = [
manifest({
spec: { compatibility: { architectures: ['386'] } },
}),
manifest({
spec: { compatibility: { deploymentProfiles: ['control'] } },
}),
manifest({
spec: { permissions: { tools: ['database.superuser'] } },
}),
manifest({
spec: { resources: { memory: { recommended: '128MB' } } },
}),
manifest({
spec: { resources: { disk: { working: '2048Gi' } } },
}),
];
for (const value of invalid) {
assert.throws(
() => normalizePluginPackageManifest(value),
InvalidPluginPackageManifestError,
);
}
});
test('plans a compatible install with explicit resources and permissions', () => {
const plan = planPluginPackageInstall(manifest(), environment());
assert.deepEqual(plan, {
package: {
name: 'example-monitor',
toVersion: '1.2.0',
},
operation: 'install',
compatible: true,
risk: 'medium',
approvalRequired: true,
permissionReapprovalRequired: true,
permissionDelta: {
added: [
'network:api.example.com',
'secret:EXAMPLE_TOKEN:required',
'tool:notification.send',
],
removed: [],
},
resources: {
memoryRecommendedBytes: 128 * 1024 * 1024,
diskInstallBytes: 20 * 1024 * 1024,
diskWorkingBytes: 100 * 1024 * 1024,
},
contents: {
tasks: 1,
workflows: 1,
prompts: 1,
tools: 1,
},
findings: [],
});
assert.equal(Object.isFrozen(plan.permissionDelta.added), true);
});
test('fails compatibility for runtime, profile, version and disk without hiding warnings', () => {
const plan = planPluginPackageInstall(
manifest(),
environment({
qinglongVersion: '4.0.0',
architecture: 's390x',
deploymentProfile: 'worker',
runtimes: [{ name: 'python', version: '2.7.18' }],
availableMemoryBytes: 64 * 1024 * 1024,
availableDiskBytes: 64 * 1024 * 1024,
}),
);
assert.equal(plan.compatible, false);
assert.deepEqual(
plan.findings.map(({ code, severity }) => [code, severity]),
[
['architecture_unsupported', 'error'],
['deployment_profile_unsupported', 'error'],
['disk_insufficient', 'error'],
['qinglong_version_unsupported', 'error'],
['runtime_version_unsupported', 'error'],
['memory_below_recommendation', 'warning'],
],
);
});
test('detects upgrade permission expansion and permits reviewed rollback planning', () => {
const previous = manifest({
metadata: { version: '1.1.0' },
spec: {
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: [],
},
},
});
const upgrade = planPluginPackageInstall(manifest(), environment(), previous);
assert.equal(upgrade.operation, 'upgrade');
assert.equal(upgrade.permissionReapprovalRequired, true);
assert.deepEqual(upgrade.package, {
name: 'example-monitor',
fromVersion: '1.1.0',
toVersion: '1.2.0',
});
const rollback = planPluginPackageInstall(
previous,
environment(),
manifest(),
);
assert.equal(rollback.operation, 'rollback');
assert.equal(rollback.permissionReapprovalRequired, false);
assert.deepEqual(rollback.permissionDelta.added, []);
assert.equal(rollback.permissionDelta.removed.length, 3);
});
test('rejects ambiguous install environments and cross-package upgrades', () => {
assert.throws(
() =>
planPluginPackageInstall(manifest(), {
...environment(),
extra: true,
}),
InvalidPluginPackageInstallEnvironmentError,
);
assert.throws(
() =>
planPluginPackageInstall(
manifest(),
environment(),
manifest({ metadata: { name: 'different-package' } }),
),
/package names differ/,
);
});
test('keeps the core contract free of filesystem, process, timer and network authority', () => {
const source = readFileSync(
join(__dirname, '../src/plugin-package/pluginPackage.ts'),
'utf8',
);
for (const authority of [
"from 'node:child_process'",
"from 'node:fs'",
"from 'node:http'",
"from 'node:https'",
'setInterval(',
'setTimeout(',
]) {
assert.equal(source.includes(authority), false, authority);
}
});