mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
525 lines
16 KiB
JavaScript
525 lines
16 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { createHash } = require('node:crypto');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
createPluginPackageResourceGenerationFromReferences,
|
|
} = require('../dist/plugin-package/pluginPackageResourceGeneration');
|
|
const {
|
|
PLUGIN_PACKAGE_API_VERSION,
|
|
PLUGIN_PACKAGE_KIND,
|
|
planPluginPackageInstall,
|
|
} = require('../dist/plugin-package/pluginPackage');
|
|
const {
|
|
createPluginPackageLock,
|
|
pluginPackageInstallActionDigest,
|
|
pluginPackageInstallPlanDigest,
|
|
serializePluginPackageManifest,
|
|
} = require('../dist/plugin-package/installation/pluginPackageInstall');
|
|
const {
|
|
pluginPackageContentTreeDigest,
|
|
} = require('../dist/plugin-package/pluginPackageBundle');
|
|
const {
|
|
materializePluginPackageResources,
|
|
} = require('../dist/plugin-package/pluginPackageResourceMaterialization');
|
|
const {
|
|
createBuiltInTaskSpecSemanticRegistry,
|
|
} = require('../dist/task-definition/taskSpecSemantic');
|
|
const {
|
|
InvalidProjectToolDefinitionSnapshotError,
|
|
MAX_PROJECT_TOOL_SNAPSHOT_ACTIVE_PACKAGES,
|
|
PROJECT_TOOL_DEFINITION_SNAPSHOT_SCHEMA,
|
|
ProjectToolDefinitionSnapshotConflictError,
|
|
ProjectToolDefinitionSnapshotPublicationCoordinator,
|
|
ProjectToolDefinitionSnapshotRecoveryCoordinator,
|
|
ProjectToolDefinitionSnapshotUnavailableError,
|
|
createProjectToolDefinitionSnapshot,
|
|
normalizeProjectToolDefinitionSnapshot,
|
|
normalizeProjectToolDefinitionSnapshotRecord,
|
|
projectToolDefinitionActiveVectorDigest,
|
|
projectToolDefinitionRegistry,
|
|
projectToolDefinitionSnapshotContribution,
|
|
} = require('@qinglong/runtime-core/project-tool-definition-snapshot');
|
|
|
|
function toolDefinition(packageName, version = '1.0.0') {
|
|
return {
|
|
name: `${packageName}.query`,
|
|
version,
|
|
description: `Queries ${packageName}`,
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {},
|
|
required: [],
|
|
additionalProperties: false,
|
|
},
|
|
effect: 'read',
|
|
risk: 'low',
|
|
requiredPermissions: ['run.read'],
|
|
timeoutSeconds: 30,
|
|
};
|
|
}
|
|
|
|
function fixture(packageName, options = {}) {
|
|
const definitions = options.definitions ?? [toolDefinition(packageName)];
|
|
const toolPaths = definitions.map(
|
|
(_definition, index) => `tools/tool-${index}.json`,
|
|
);
|
|
const resourceBytes = Object.fromEntries(
|
|
definitions.map((definition, index) => [
|
|
toolPaths[index],
|
|
Buffer.from(
|
|
JSON.stringify({
|
|
schema: 'qinglong/plugin-package-tool-resource@v1',
|
|
definition,
|
|
}),
|
|
),
|
|
]),
|
|
);
|
|
const descriptors = Object.entries(resourceBytes)
|
|
.map(([path, material]) => ({
|
|
path,
|
|
bytes: material.byteLength,
|
|
digest: createHash('sha256').update(material).digest('hex'),
|
|
}))
|
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
const contentDigest = pluginPackageContentTreeDigest(descriptors);
|
|
const manifest = {
|
|
apiVersion: PLUGIN_PACKAGE_API_VERSION,
|
|
kind: PLUGIN_PACKAGE_KIND,
|
|
metadata: {
|
|
name: packageName,
|
|
displayName: packageName,
|
|
version: options.packageVersion ?? '1.0.0',
|
|
description: `Package ${packageName}`,
|
|
license: 'Apache-2.0',
|
|
},
|
|
spec: {
|
|
compatibility: {
|
|
qinglong: '>=3.0.0-0 <4.0.0',
|
|
architectures: ['arm64'],
|
|
deploymentProfiles: ['edge', 'standalone'],
|
|
},
|
|
runtimes: [],
|
|
resources: {
|
|
memory: { recommended: '8Mi' },
|
|
disk: { install: '1Mi', working: '1Mi' },
|
|
},
|
|
permissions: {
|
|
network: { allowedHosts: [] },
|
|
secrets: [],
|
|
tools: definitions.length === 0 ? [] : ['run.read'],
|
|
},
|
|
contents: {
|
|
tasks: [],
|
|
workflows: [],
|
|
prompts: [],
|
|
tools: toolPaths,
|
|
},
|
|
},
|
|
};
|
|
const environment = {
|
|
qinglongVersion: '3.0.0-alpha.0',
|
|
architecture: 'arm64',
|
|
deploymentProfile: 'edge',
|
|
runtimes: [],
|
|
availableMemoryBytes: 128 * 1024 * 1024,
|
|
availableDiskBytes: 256 * 1024 * 1024,
|
|
};
|
|
const plan = planPluginPackageInstall(manifest, environment);
|
|
const action = {
|
|
lockId: `lock-${packageName}`,
|
|
projectId: 'project-001',
|
|
manifest,
|
|
plan,
|
|
environment,
|
|
source: {
|
|
kind: 'oci',
|
|
locator:
|
|
`oci://registry.example.com/qinglong/${packageName}@sha256:` +
|
|
'a'.repeat(64),
|
|
artifactDigest: 'b'.repeat(64),
|
|
artifactBytes: 4096,
|
|
contentDigest,
|
|
},
|
|
architecture: 'arm64',
|
|
deploymentProfile: 'edge',
|
|
targetGeneration: options.generation ?? 1,
|
|
};
|
|
const lock = createPluginPackageLock({
|
|
...action,
|
|
approval: {
|
|
requestId: `request-${packageName}`,
|
|
requestVersion: 1,
|
|
dispatchId: `dispatch-${packageName}`,
|
|
actionDigest: pluginPackageInstallActionDigest(action),
|
|
previewDigest: pluginPackageInstallPlanDigest(plan),
|
|
approvedBy: { type: 'user', id: 'owner-001' },
|
|
approvedAtMs: 100,
|
|
expiresAtMs: 1_000,
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
},
|
|
createdAtMs: 200,
|
|
});
|
|
const generation = createPluginPackageResourceGenerationFromReferences({
|
|
installationId: `install-${packageName}`,
|
|
projectId: lock.projectId,
|
|
packageName: lock.packageName,
|
|
lockDigest: lock.lockDigest,
|
|
generation: lock.targetGeneration,
|
|
previousActiveLockDigest: null,
|
|
contentDigest,
|
|
resources: lock.resources,
|
|
});
|
|
const registry = createBuiltInTaskSpecSemanticRegistry();
|
|
return {
|
|
registry,
|
|
revision: materializePluginPackageResources({
|
|
generation,
|
|
lock,
|
|
manifestBytes: Buffer.from(serializePluginPackageManifest(manifest)),
|
|
resources: generation.resources.map((reference) => ({
|
|
reference,
|
|
bytes: resourceBytes[reference.path],
|
|
})),
|
|
taskSpecSemanticRegistry: registry,
|
|
}),
|
|
};
|
|
}
|
|
|
|
function contribution(value) {
|
|
return projectToolDefinitionSnapshotContribution(
|
|
value.revision,
|
|
value.registry,
|
|
);
|
|
}
|
|
|
|
function source(value) {
|
|
const planned = contribution(value);
|
|
return {
|
|
installationId: planned.generation.installationId,
|
|
packageName: planned.generation.packageName,
|
|
generation: planned.generation.generation,
|
|
generationDigest: planned.generation.generationDigest,
|
|
lockDigest: planned.generation.lockDigest,
|
|
revisionDigest: planned.revisionDigest,
|
|
};
|
|
}
|
|
|
|
function sourceAuthority(vectors, pending = []) {
|
|
let observation = 0;
|
|
const pendingProjects = new Set(pending);
|
|
return {
|
|
pendingProjects,
|
|
calls: [],
|
|
async listActiveSourcePage({ projectId, limit, after }) {
|
|
if (!after) observation += 1;
|
|
const vector = vectors[Math.min(observation - 1, vectors.length - 1)];
|
|
const start = after
|
|
? vector.findIndex((item) => item.packageName > after.packageName)
|
|
: 0;
|
|
const page = start < 0 ? [] : vector.slice(start, start + limit);
|
|
const truncated = start >= 0 && start + page.length < vector.length;
|
|
this.calls.push({
|
|
kind: 'sources',
|
|
projectId,
|
|
after: after?.packageName,
|
|
packages: page.map(({ packageName }) => packageName),
|
|
});
|
|
return {
|
|
sources: page,
|
|
truncated,
|
|
...(truncated
|
|
? { next: { packageName: page.at(-1).packageName } }
|
|
: {}),
|
|
};
|
|
},
|
|
async listPendingProjectPage({ limit, after }) {
|
|
const projects = [...pendingProjects]
|
|
.sort()
|
|
.filter((projectId) => !after || projectId > after.projectId);
|
|
const page = projects.slice(0, limit);
|
|
const truncated = page.length < projects.length;
|
|
return {
|
|
projectIds: page,
|
|
truncated,
|
|
...(truncated ? { next: { projectId: page.at(-1) } } : {}),
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
function publicationHarness(values, vectors, pending = []) {
|
|
const authority = sourceAuthority(vectors, pending);
|
|
const revisions = new Map(
|
|
values.map((value) => [
|
|
value.revision.generation.generationDigest,
|
|
value.revision,
|
|
]),
|
|
);
|
|
const records = new Map();
|
|
const repository = {
|
|
publications: 0,
|
|
async findCurrent(projectId) {
|
|
return records.get(projectId) ?? null;
|
|
},
|
|
async publish(snapshot) {
|
|
this.publications += 1;
|
|
const record = Object.freeze({ snapshot, committedAtMs: 500 });
|
|
records.set(snapshot.projectId, record);
|
|
authority.pendingProjects.delete(snapshot.projectId);
|
|
return Object.freeze({ status: 'created', record });
|
|
},
|
|
};
|
|
const coordinator = new ProjectToolDefinitionSnapshotPublicationCoordinator({
|
|
source: authority,
|
|
materializedRepository: {
|
|
calls: [],
|
|
async find(generationDigest) {
|
|
this.calls.push(generationDigest);
|
|
return revisions.get(generationDigest) ?? null;
|
|
},
|
|
},
|
|
repository,
|
|
taskSpecSemanticRegistry:
|
|
values[0]?.registry ?? createBuiltInTaskSpecSemanticRegistry(),
|
|
pageSize: 1,
|
|
});
|
|
return { authority, coordinator, records, repository, revisions };
|
|
}
|
|
|
|
test('builds one Project snapshot from the complete active Package vector', () => {
|
|
const alpha = fixture('alpha');
|
|
const empty = fixture('empty', { definitions: [] });
|
|
const snapshot = createProjectToolDefinitionSnapshot({
|
|
projectId: 'project-001',
|
|
contributions: [contribution(empty), contribution(alpha)],
|
|
});
|
|
|
|
assert.equal(snapshot.schema, PROJECT_TOOL_DEFINITION_SNAPSHOT_SCHEMA);
|
|
assert.deepEqual(
|
|
snapshot.sources.map(({ packageName }) => packageName),
|
|
['alpha', 'empty'],
|
|
);
|
|
assert.deepEqual(
|
|
snapshot.definitions.map(({ packageName, definition }) => ({
|
|
packageName,
|
|
name: definition.name,
|
|
version: definition.version,
|
|
})),
|
|
[{ packageName: 'alpha', name: 'alpha.query', version: '1.0.0' }],
|
|
);
|
|
assert.match(snapshot.activeVectorDigest, /^[0-9a-f]{64}$/);
|
|
assert.equal(
|
|
projectToolDefinitionActiveVectorDigest(
|
|
snapshot.projectId,
|
|
snapshot.sources,
|
|
),
|
|
snapshot.activeVectorDigest,
|
|
);
|
|
assert.match(snapshot.definitionsDigest, /^[0-9a-f]{64}$/);
|
|
assert.match(snapshot.snapshotDigest, /^[0-9a-f]{64}$/);
|
|
assert.equal(Object.isFrozen(snapshot), true);
|
|
assert.deepEqual(normalizeProjectToolDefinitionSnapshot(snapshot), snapshot);
|
|
assert.deepEqual(
|
|
normalizeProjectToolDefinitionSnapshotRecord({
|
|
snapshot,
|
|
committedAtMs: 500,
|
|
}),
|
|
{ snapshot, committedAtMs: 500 },
|
|
);
|
|
assert.equal(
|
|
projectToolDefinitionRegistry(snapshot).resolve('alpha.query', '1.0.0')
|
|
.description,
|
|
'Queries alpha',
|
|
);
|
|
});
|
|
|
|
test('binds empty Packages and source revision changes into snapshot identity', () => {
|
|
const alpha = fixture('alpha');
|
|
const first = createProjectToolDefinitionSnapshot({
|
|
projectId: 'project-001',
|
|
contributions: [contribution(alpha)],
|
|
});
|
|
const empty = fixture('empty', { definitions: [] });
|
|
const withEmpty = createProjectToolDefinitionSnapshot({
|
|
projectId: 'project-001',
|
|
contributions: [contribution(alpha), contribution(empty)],
|
|
});
|
|
assert.notEqual(first.activeVectorDigest, withEmpty.activeVectorDigest);
|
|
assert.notEqual(first.snapshotDigest, withEmpty.snapshotDigest);
|
|
|
|
const alphaV2 = fixture('alpha', {
|
|
packageVersion: '1.0.1',
|
|
definitions: [toolDefinition('alpha', '1.0.1')],
|
|
});
|
|
const next = createProjectToolDefinitionSnapshot({
|
|
projectId: 'project-001',
|
|
contributions: [contribution(alphaV2)],
|
|
});
|
|
assert.notEqual(first.activeVectorDigest, next.activeVectorDigest);
|
|
assert.notEqual(first.snapshotDigest, next.snapshotDigest);
|
|
});
|
|
|
|
test('uses the canonical Project Policy identity boundary', () => {
|
|
const snapshot = createProjectToolDefinitionSnapshot({
|
|
projectId: 'Project / 路由设备',
|
|
contributions: [],
|
|
});
|
|
assert.equal(snapshot.projectId, 'Project / 路由设备');
|
|
assert.throws(
|
|
() =>
|
|
createProjectToolDefinitionSnapshot({
|
|
projectId: 'project\0invalid',
|
|
contributions: [],
|
|
}),
|
|
InvalidProjectToolDefinitionSnapshotError,
|
|
);
|
|
});
|
|
|
|
test('rejects source, ordering, digest and Tool identity drift', () => {
|
|
const alpha = fixture('alpha');
|
|
const empty = fixture('empty', { definitions: [] });
|
|
const snapshot = createProjectToolDefinitionSnapshot({
|
|
projectId: 'project-001',
|
|
contributions: [contribution(alpha), contribution(empty)],
|
|
});
|
|
const mutable = JSON.parse(JSON.stringify(snapshot));
|
|
|
|
assert.throws(
|
|
() =>
|
|
normalizeProjectToolDefinitionSnapshot({
|
|
...mutable,
|
|
sources: [...mutable.sources].reverse(),
|
|
}),
|
|
/uniquely sorted/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeProjectToolDefinitionSnapshot({
|
|
...mutable,
|
|
definitions: [
|
|
{
|
|
...mutable.definitions[0],
|
|
definitionDigest: 'f'.repeat(64),
|
|
},
|
|
],
|
|
}),
|
|
/definition digest does not match/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createProjectToolDefinitionSnapshot({
|
|
projectId: 'project-001',
|
|
contributions: [contribution(alpha), contribution(alpha)],
|
|
}),
|
|
/Package source is duplicated/,
|
|
);
|
|
});
|
|
|
|
test('enforces the active Package budget before normalizing revisions', () => {
|
|
const alpha = fixture('alpha');
|
|
assert.throws(
|
|
() =>
|
|
createProjectToolDefinitionSnapshot({
|
|
projectId: 'project-001',
|
|
contributions: Array.from(
|
|
{ length: MAX_PROJECT_TOOL_SNAPSHOT_ACTIVE_PACKAGES + 1 },
|
|
() => contribution(alpha),
|
|
),
|
|
}),
|
|
InvalidProjectToolDefinitionSnapshotError,
|
|
);
|
|
});
|
|
|
|
test('publishes one current snapshot through paged double observation', async () => {
|
|
const alpha = fixture('alpha');
|
|
const empty = fixture('empty', { definitions: [] });
|
|
const vector = [source(alpha), source(empty)];
|
|
const harness = publicationHarness([alpha, empty], [vector, vector]);
|
|
|
|
const result = await harness.coordinator.publishCurrent('project-001');
|
|
assert.equal(result.status, 'created');
|
|
assert.deepEqual(
|
|
result.record.snapshot.sources.map(({ packageName }) => packageName),
|
|
['alpha', 'empty'],
|
|
);
|
|
assert.deepEqual(
|
|
harness.authority.calls.map(({ packages }) => packages),
|
|
[['alpha'], ['empty'], ['alpha'], ['empty']],
|
|
);
|
|
assert.equal(harness.repository.publications, 1);
|
|
|
|
harness.authority.calls.length = 0;
|
|
const replay = await harness.coordinator.publishCurrent('project-001');
|
|
assert.equal(replay.status, 'existing');
|
|
assert.deepEqual(replay.record, result.record);
|
|
assert.deepEqual(harness.authority.calls, []);
|
|
});
|
|
|
|
test('fails closed when the active vector changes between observations', async () => {
|
|
const alpha = fixture('alpha');
|
|
const empty = fixture('empty', { definitions: [] });
|
|
const harness = publicationHarness(
|
|
[alpha, empty],
|
|
[[source(alpha)], [source(alpha), source(empty)]],
|
|
);
|
|
|
|
await assert.rejects(
|
|
harness.coordinator.publishCurrent('project-001'),
|
|
ProjectToolDefinitionSnapshotConflictError,
|
|
);
|
|
assert.equal(harness.repository.publications, 0);
|
|
});
|
|
|
|
test('fails closed when an observed source has no exact immutable revision', async () => {
|
|
const alpha = fixture('alpha');
|
|
const harness = publicationHarness([], [[source(alpha)], [source(alpha)]]);
|
|
|
|
await assert.rejects(
|
|
harness.coordinator.publishCurrent('project-001'),
|
|
ProjectToolDefinitionSnapshotUnavailableError,
|
|
);
|
|
assert.equal(harness.repository.publications, 0);
|
|
});
|
|
|
|
test('bounded snapshot recovery drains pending Projects and probes from the start', async () => {
|
|
const harness = publicationHarness([], [[], []], ['project-a', 'project-b']);
|
|
const recovery = new ProjectToolDefinitionSnapshotRecoveryCoordinator({
|
|
source: harness.authority,
|
|
publisher: harness.coordinator,
|
|
});
|
|
|
|
assert.deepEqual(await recovery.recover({ pageSize: 1, maxPages: 3 }), {
|
|
pages: 2,
|
|
scanned: 2,
|
|
settled: 2,
|
|
retry: 0,
|
|
manualRequired: 0,
|
|
remaining: false,
|
|
safeToAdmit: true,
|
|
});
|
|
assert.equal(harness.records.has('project-a'), true);
|
|
assert.equal(harness.records.has('project-b'), true);
|
|
});
|
|
|
|
test('publishes snapshot planning only through its explicit subpath', () => {
|
|
assert.equal(
|
|
require('../dist').createProjectToolDefinitionSnapshot,
|
|
undefined,
|
|
);
|
|
assert.equal(
|
|
typeof require('../dist/tool-execution/tool-registry/projectToolDefinitionSnapshot')
|
|
.createProjectToolDefinitionSnapshot,
|
|
'function',
|
|
);
|
|
assert.equal(
|
|
require('../dist').ProjectToolDefinitionSnapshotPublicationCoordinator,
|
|
undefined,
|
|
);
|
|
assert.equal(
|
|
typeof require('../dist/tool-execution/tool-registry/projectToolDefinitionSnapshot')
|
|
.ProjectToolDefinitionSnapshotPublicationCoordinator,
|
|
'function',
|
|
);
|
|
});
|