feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,672 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
createInitialPluginPackageAutomationPublication,
pluginPackageAutomationDefinitionsFromRevision,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
InvalidPluginPackageLifecycleError,
PluginPackageLifecycleConflictError,
PluginPackageLifecycleUnavailableError,
createPluginPackageLifecycleEvent,
} = require('@qinglong/runtime-core/plugin-package-lifecycle');
const {
createProjectToolDefinitionSnapshot,
projectToolDefinitionSnapshotContribution,
} = require('@qinglong/runtime-core/project-tool-definition-snapshot');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const {
LocalSqliteApprovalRequestRepository,
} = require('../dist/approved-action/approvalRequestRepository');
const { LocalSqliteOperationAuthority } = require('../dist/authority/operationAuthority');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageAutomationPublicationRepository,
} = require('../dist/plugin-package/pluginPackageAutomationPublicationRepository');
const {
EDGE_PLUGIN_PACKAGE_LIFECYCLE_ACTIVE_SOURCE_LIMIT,
LocalSqlitePluginPackageLifecycleRepository,
} = require('../dist/plugin-package/pluginPackageLifecycleRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageTaskReconciliationRepository,
} = require('../dist/plugin-package/pluginPackageTaskReconciliationRepository');
const {
LocalSqliteProjectToolDefinitionSnapshotRepository,
} = require('../dist/tool-execution/projectToolDefinitionSnapshotRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const {
LocalSqliteReadinessError,
auditLocalSqliteReadiness,
} = require('../dist/readiness/readiness');
const OWNER = Object.freeze({ type: 'user', id: 'owner-001' });
const OTHER_OWNER = Object.freeze({ type: 'user', id: 'owner-002' });
const SYSTEM = Object.freeze({ type: 'system', id: 'lifecycle-dispatcher' });
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
function audit(
eventId,
requestId,
operationId,
subject,
authenticationId,
outcome,
projectId,
occurredAtMs,
) {
return {
eventId,
requestId,
operationId,
projectId,
subject,
authenticationId,
outcome,
reasons: [outcome === 'approval_required' ? 'package_review' : 'role_grant'],
fence: FENCE,
occurredAtMs,
};
}
function auditId(sequence, offset) {
return `90000000-0000-4000-8000-${String(sequence * 10 + offset).padStart(
12,
'0',
)}`;
}
async function harness(t, namespace, fixtureOptions = {}) {
const fixture = pluginPackageTaskReconciliationFixture(namespace, {
profile: 'edge',
...fixtureOptions,
});
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
client
.prepare(
`INSERT INTO "QingLong3Projects"
(id, name, slug, status, version, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(fixture.projectId, fixture.projectId, fixture.projectId);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
project_id, subject_type, subject_id, version, state, role,
mutation_id, changed_by_type, changed_by_id, created_at_ms
) VALUES (?, 'user', ?, 1, 'active', 'owner', ?, 'user', ?, 1)`,
)
.run(
fixture.projectId,
OWNER.id,
`grant-${namespace}`,
OWNER.id,
);
const authority = new LocalSqliteOperationAuthority(client);
t.after(() => authority.close());
return {
fixture,
client,
authority,
approval: new LocalSqliteApprovalRequestRepository(authority),
automations:
new LocalSqlitePluginPackageAutomationPublicationRepository(authority),
install: new LocalSqlitePluginPackageInstallRepository(authority),
materialized: new LocalSqlitePluginPackageMaterializedRevisionRepository(
authority,
fixture.registry,
),
reconciliation: new LocalSqlitePluginPackageTaskReconciliationRepository(
authority,
fixture.registry,
),
snapshots: new LocalSqliteProjectToolDefinitionSnapshotRepository(
authority,
),
lifecycle: new LocalSqlitePluginPackageLifecycleRepository(authority, {
registry: fixture.registry,
activeSourceLimit:
EDGE_PLUGIN_PACKAGE_LIFECYCLE_ACTIVE_SOURCE_LIMIT,
}),
approvalSequence: 0,
};
}
async function publishActivePackage(value) {
await activateInstall(value.install, value.fixture);
await value.materialized.publish(value.fixture.revision);
if (
pluginPackageAutomationDefinitionsFromRevision(
value.fixture.revision,
value.fixture.registry,
)
) {
await value.automations.publish(
createInitialPluginPackageAutomationPublication(
value.fixture.revision,
value.fixture.registry,
1_000,
),
);
}
await value.reconciliation.reconcile(value.fixture.revision, {
async findActiveResourceGeneration() {
return value.fixture.revision.generation;
},
});
await value.snapshots.publish(
createProjectToolDefinitionSnapshot({
projectId: value.fixture.projectId,
contributions: [
projectToolDefinitionSnapshotContribution(
value.fixture.revision,
value.fixture.registry,
),
],
}),
);
}
async function approveLifecycleEvent(
value,
impact,
subjects = Object.freeze({
requestedBy: OWNER,
approvedBy: OWNER,
}),
) {
value.approvalSequence += 1;
const sequence = value.approvalSequence;
const requestId = `lifecycle-approval-${sequence}`;
const dispatchId = `lifecycle-dispatch-${sequence}`;
const requestedAtMs = 10_000 * sequence + 1;
const decidedAtMs = requestedAtMs + 1;
const consumedAtMs = requestedAtMs + 2;
const occurredAtMs = requestedAtMs + 3;
const expiresAtMs = requestedAtMs + 1_000;
const action = {
permission: 'package.manage',
actionType: `plugin_package.lifecycle.${impact.action}`,
actionRef: `lifecycle:${impact.impactDigest}`,
actionDigest: require('@qinglong/runtime-core/plugin-package-lifecycle')
.pluginPackageLifecycleActionDigest(impact),
previewDigest: impact.impactDigest,
};
await value.approval.create({
request: createApprovalRequest({
id: requestId,
projectId: value.fixture.projectId,
action,
risk: 'high',
decisionMode: 'human_confirmation',
requestedBy: subjects.requestedBy,
requestedAtMs,
expiresAtMs,
requestFence: FENCE,
}),
audit: audit(
auditId(sequence, 1),
`lifecycle-http-${sequence}`,
'approval.request',
subjects.requestedBy,
`auth-request-${sequence}`,
'approval_required',
value.fixture.projectId,
requestedAtMs,
),
});
await value.approval.decide({
requestId,
expectedVersion: 1,
decisionId: `lifecycle-decision-${sequence}`,
decision: 'approved',
reasonCode: 'reviewed',
principal: {
subject: subjects.approvedBy,
authenticationId: `auth-approve-${sequence}`,
authenticatedAtMs: decidedAtMs - 1,
expiresAtMs,
assurance: 'local_console',
},
decidedAtMs,
authorizationFence: FENCE,
audit: audit(
auditId(sequence, 2),
`lifecycle-http-${sequence}`,
'approval.decide',
subjects.approvedBy,
`auth-approve-${sequence}`,
'allowed',
value.fixture.projectId,
decidedAtMs,
),
});
const consumed = await value.approval.consume({
requestId,
expectedVersion: 2,
consumptionId: `lifecycle-consume-${sequence}`,
dispatchId,
action,
requestedBy: subjects.requestedBy,
consumedBy: SYSTEM,
consumedAtMs,
authorizationFence: FENCE,
audit: audit(
auditId(sequence, 3),
`lifecycle-dispatch-cycle-${sequence}`,
'approval.consume',
SYSTEM,
`auth-dispatch-${sequence}`,
'allowed',
value.fixture.projectId,
consumedAtMs,
),
});
return createPluginPackageLifecycleEvent({
dispatchId: consumed.dispatch.id,
impact,
requestedBy: subjects.requestedBy,
approvedBy: subjects.approvedBy,
authorizationMode: 'human_confirmation',
occurredAtMs,
});
}
function taskHeads(value) {
return value.client
.prepare(
`SELECT head.task_id AS "taskId",
revision.revision,
revision.enabled
FROM "QingLong3TaskDefinitions" AS head
JOIN "QingLong3TaskDefinitionRevisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = ?
ORDER BY head.task_id`,
)
.all(value.fixture.projectId)
.map((row) => ({ ...row }));
}
test('atomically disables, exactly replays and restores only lifecycle Tasks', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-roundtrip');
await publishActivePackage(value);
const before = await value.snapshots.findCurrent(value.fixture.projectId);
assert.equal(before.snapshot.sources.length, 1);
const disableImpact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
assert.equal(disableImpact.expected.disposition, 'active');
assert.deepEqual(disableImpact.taskIds, [
`pkg:${value.fixture.packageName}:alpha`,
`pkg:${value.fixture.packageName}:beta`,
]);
const disableEvent = await approveLifecycleEvent(value, disableImpact);
let authorizationChecks = 0;
const disabled = await value.lifecycle.transition(disableEvent, () => {
authorizationChecks += 1;
});
assert.equal(disabled.status, 'created');
assert.equal(authorizationChecks, 2);
assert.equal(disabled.receipt.lifecycle.disposition, 'disabled');
assert.equal(disabled.receipt.capability.status, 'withdrawn');
assert.equal(disabled.receipt.capability.retainedSourceCount, 0);
assert.deepEqual(taskHeads(value), [
{
taskId: `pkg:${value.fixture.packageName}:alpha`,
revision: 2,
enabled: 0,
},
{
taskId: `pkg:${value.fixture.packageName}:beta`,
revision: 2,
enabled: 0,
},
]);
assert.equal(
(await value.snapshots.findCurrent(value.fixture.projectId)).snapshot.sources
.length,
0,
);
const replay = await value.lifecycle.transition(disableEvent, () => {
authorizationChecks += 1;
});
assert.equal(replay.status, 'existing');
assert.equal(authorizationChecks, 4);
assert.deepEqual(replay.receipt, disabled.receipt);
const enableImpact = await value.lifecycle.plan(
'enable',
value.fixture.projectId,
value.fixture.packageName,
);
assert.deepEqual(enableImpact.taskIds, disableImpact.taskIds);
const enableEvent = await approveLifecycleEvent(value, enableImpact);
const enabled = await value.lifecycle.transition(enableEvent, () => {});
assert.equal(enabled.receipt.lifecycle.disposition, 'active');
assert.equal(enabled.receipt.capability.status, 'restored');
assert.equal(enabled.receipt.capability.retainedSourceCount, 1);
assert.deepEqual(taskHeads(value), [
{
taskId: `pkg:${value.fixture.packageName}:alpha`,
revision: 3,
enabled: 1,
},
{
taskId: `pkg:${value.fixture.packageName}:beta`,
revision: 3,
enabled: 1,
},
]);
assert.equal(
(await value.snapshots.findCurrent(value.fixture.projectId)).snapshot.sources
.length,
1,
);
assert.deepEqual(
await value.lifecycle.findByEventDigest(enableEvent.eventDigest),
enabled.receipt,
);
await auditLocalSqliteReadiness(value.client);
});
test('atomically withdraws and restores Workflow and Prompt publications', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-automation', {
workflows: [
{
schema: 'qinglong/plugin-package-workflow-resource@v1',
id: 'daily',
name: 'Daily workflow',
enabled: true,
steps: [{ id: 'run', task: 'alpha', needs: [] }],
},
],
prompts: [
{
schema: 'qinglong/plugin-package-prompt-resource@v1',
id: 'greeting',
name: 'Greeting prompt',
template: 'Hello {{name}}',
parameters: [{ name: 'name', required: true }],
},
],
});
await publishActivePackage(value);
const initial = await value.automations.findCurrent(
value.fixture.projectId,
value.fixture.packageName,
);
assert.equal(initial.state, 'active');
assert.equal(initial.version, 1);
const disableImpact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
const disableEvent = await approveLifecycleEvent(value, disableImpact);
let checks = 0;
await assert.rejects(
value.lifecycle.transition(disableEvent, () => {
checks += 1;
if (checks === 2) throw new Error('authorization expired');
}),
(error) =>
error instanceof PluginPackageLifecycleUnavailableError &&
error.cause?.message === 'authorization expired',
);
assert.deepEqual(
await value.automations.findCurrent(
value.fixture.projectId,
value.fixture.packageName,
),
initial,
);
assert.deepEqual(
taskHeads(value).map(({ revision, enabled }) => ({ revision, enabled })),
[
{ revision: 1, enabled: 1 },
{ revision: 1, enabled: 1 },
],
);
const disabled = await value.lifecycle.transition(disableEvent, () => {});
const withdrawn = await value.automations.findCurrent(
value.fixture.projectId,
value.fixture.packageName,
);
assert.equal(disabled.status, 'created');
assert.equal(withdrawn.state, 'withdrawn');
assert.equal(withdrawn.version, 2);
assert.equal(withdrawn.lifecycleEventDigest, disableEvent.eventDigest);
assert.equal(withdrawn.previousPublicationDigest, initial.publicationDigest);
assert.deepEqual(withdrawn.definitions, initial.definitions);
const enableImpact = await value.lifecycle.plan(
'enable',
value.fixture.projectId,
value.fixture.packageName,
);
const enableEvent = await approveLifecycleEvent(value, enableImpact);
await value.lifecycle.transition(enableEvent, () => {});
const restored = await value.automations.findCurrent(
value.fixture.projectId,
value.fixture.packageName,
);
assert.equal(restored.state, 'active');
assert.equal(restored.version, 3);
assert.equal(restored.lifecycleEventDigest, enableEvent.eventDigest);
assert.equal(
restored.previousPublicationDigest,
withdrawn.publicationDigest,
);
assert.deepEqual(restored.definitions, initial.definitions);
await auditLocalSqliteReadiness(value.client);
});
test('diagnoses live Run blockers and retires history only after they clear', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-uninstall');
await publishActivePackage(value);
const disableImpact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
await value.lifecycle.transition(
await approveLifecycleEvent(value, disableImpact),
() => {},
);
const task = taskHeads(value)[0];
value.client
.prepare(
`INSERT INTO "Runs" (
id, project_id, task_id, task_revision, trigger_type,
execution_origin, execution_owner, status, version,
event_sequence, priority, created_at_ms
) VALUES (?, ?, ?, ?, 'manual', 'manual', 'runtime', 'queued',
0, 0, 0, 1)`,
)
.run(
'019f9b00-0000-4000-a000-000000000001',
value.fixture.projectId,
task.taskId,
String(task.revision),
);
const blocked = await value.lifecycle.plan(
'uninstall',
value.fixture.projectId,
value.fixture.packageName,
);
assert.deepEqual(
blocked.blockingReferences.map(({ kind, ownerId }) => ({ kind, ownerId })),
[
{
kind: 'execution_recovery',
ownerId: '019f9b00-0000-4000-a000-000000000001',
},
],
);
assert.throws(
() =>
createPluginPackageLifecycleEvent({
dispatchId: 'blocked-dispatch',
impact: blocked,
requestedBy: OWNER,
approvedBy: OWNER,
authorizationMode: 'human_confirmation',
occurredAtMs: 1,
}),
InvalidPluginPackageLifecycleError,
);
value.client
.prepare(`UPDATE "Runs" SET status = 'succeeded' WHERE id = ?`)
.run('019f9b00-0000-4000-a000-000000000001');
const impact = await value.lifecycle.plan(
'uninstall',
value.fixture.projectId,
value.fixture.packageName,
);
assert.deepEqual(impact.blockingReferences, []);
const retired = await value.lifecycle.transition(
await approveLifecycleEvent(value, impact),
() => {},
);
assert.equal(retired.receipt.lifecycle.disposition, 'uninstalled');
assert.equal(retired.receipt.capability.status, 'retired');
assert.deepEqual(retired.receipt.capability.taskTransitions, []);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleEvents"
WHERE project_id = ? AND package_name = ?`,
)
.get(value.fixture.projectId, value.fixture.packageName).count,
2,
);
await auditLocalSqliteReadiness(value.client);
});
test('rejects stale approved impact without leaving Task or lifecycle facts', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-stale');
await publishActivePackage(value);
const impact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
const event = await approveLifecycleEvent(value, impact);
const task = taskHeads(value)[0];
value.client
.prepare(
`INSERT INTO "Runs" (
id, project_id, task_id, task_revision, trigger_type,
execution_origin, execution_owner, status, version,
event_sequence, priority, created_at_ms
) VALUES (?, ?, ?, ?, 'manual', 'manual', 'runtime', 'queued',
0, 0, 0, 1)`,
)
.run(
'019f9b00-0000-4000-a000-000000000002',
value.fixture.projectId,
task.taskId,
String(task.revision),
);
await assert.rejects(
value.lifecycle.transition(event, () => {}),
PluginPackageLifecycleConflictError,
);
assert.deepEqual(taskHeads(value).map(({ revision, enabled }) => ({
revision,
enabled,
})), [
{ revision: 1, enabled: 1 },
{ revision: 1, enabled: 1 },
]);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleEvents"`,
)
.get().count,
0,
);
});
test('fails closed on dispatch subject drift and incomplete Task evidence', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-corrupt');
await publishActivePackage(value);
const impact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
const approved = await approveLifecycleEvent(value, impact);
const drifted = createPluginPackageLifecycleEvent({
dispatchId: approved.dispatchId,
impact,
requestedBy: OTHER_OWNER,
approvedBy: OTHER_OWNER,
authorizationMode: 'human_confirmation',
occurredAtMs: approved.occurredAtMs,
});
await assert.rejects(
value.lifecycle.transition(drifted, () => {}),
PluginPackageLifecycleConflictError,
);
const created = await value.lifecycle.transition(approved, () => {});
value.client.exec('PRAGMA foreign_keys = OFF');
value.client
.prepare(
`DELETE FROM "QingLong3PluginPackageLifecycleTasks"
WHERE event_digest = ? AND task_id = ?`,
)
.run(
approved.eventDigest,
`pkg:${value.fixture.packageName}:alpha`,
);
await assert.rejects(
value.lifecycle.findByEventDigest(approved.eventDigest),
PluginPackageLifecycleUnavailableError,
);
await assert.rejects(
auditLocalSqliteReadiness(value.client),
LocalSqliteReadinessError,
);
assert.equal(created.receipt.capability.taskTransitions.length, 2);
});
test('publishes lifecycle storage only through its explicit subpath', () => {
const entrypoint = require('@qinglong/local-sqlite/plugin-package-lifecycle');
assert.equal(
entrypoint.LocalSqlitePluginPackageLifecycleRepository,
LocalSqlitePluginPackageLifecycleRepository,
);
assert.equal(
require('../dist').LocalSqlitePluginPackageLifecycleRepository,
undefined,
);
});