mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
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',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user