mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
673 lines
21 KiB
JavaScript
673 lines
21 KiB
JavaScript
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,
|
|
);
|
|
});
|