mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
559 lines
18 KiB
JavaScript
559 lines
18 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
PluginPackageQuarantineConflictError,
|
|
PluginPackageQuarantineUnavailableError,
|
|
createPluginPackageQuarantineEvent,
|
|
} = require('@qinglong/runtime-core/plugin-package-quarantine');
|
|
const {
|
|
createInitialPluginPackageAutomationPublication,
|
|
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
|
|
const {
|
|
RunRepositoryConstraintError,
|
|
} = require('@qinglong/runtime-core/run-repository');
|
|
const {
|
|
createTaskDefinitionRevisionRef,
|
|
} = require('@qinglong/runtime-core/task-definition-execution-compiler');
|
|
const {
|
|
activateInstall,
|
|
pluginPackageTaskReconciliationFixture,
|
|
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
|
|
const {
|
|
LocalSqliteOperationAuthority,
|
|
} = require('../dist/authority/operationAuthority');
|
|
const {
|
|
LocalSqlitePluginPackageInstallRepository,
|
|
} = require('../dist/plugin-package/pluginPackageInstallRepository');
|
|
const {
|
|
LocalSqlitePluginPackageMaterializedRevisionRepository,
|
|
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
|
|
const {
|
|
LocalSqlitePluginPackageAutomationPublicationRepository,
|
|
} = require('../dist/plugin-package/pluginPackageAutomationPublicationRepository');
|
|
const {
|
|
LocalSqlitePluginPackageTaskReconciliationRepository,
|
|
} = require('../dist/plugin-package/pluginPackageTaskReconciliationRepository');
|
|
const {
|
|
LocalSqliteProjectToolDefinitionSnapshotRepository,
|
|
} = require('../dist/tool-execution/projectToolDefinitionSnapshotRepository');
|
|
const { LocalSqliteRunRepository } = require('../dist/run/runRepository');
|
|
const {
|
|
EDGE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT,
|
|
LocalSqlitePluginPackageQuarantineRepository,
|
|
} = require('../dist/plugin-package/pluginPackageQuarantineRepository');
|
|
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
|
|
const {
|
|
LocalSqliteReadinessError,
|
|
auditLocalSqliteReadiness,
|
|
} = require('../dist/readiness/readiness');
|
|
|
|
const digest = (value) => value.repeat(64);
|
|
|
|
async function harness(t, namespace) {
|
|
const fixture = pluginPackageTaskReconciliationFixture(namespace, {
|
|
profile: 'edge',
|
|
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: 'operator',
|
|
name: 'Operator prompt',
|
|
template: 'Run {{task}}',
|
|
parameters: [{ name: 'task', required: true }],
|
|
},
|
|
],
|
|
});
|
|
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);
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
t.after(() => authority.close());
|
|
return {
|
|
fixture,
|
|
client,
|
|
install: new LocalSqlitePluginPackageInstallRepository(authority),
|
|
materialized: new LocalSqlitePluginPackageMaterializedRevisionRepository(
|
|
authority,
|
|
fixture.registry,
|
|
),
|
|
automation: new LocalSqlitePluginPackageAutomationPublicationRepository(
|
|
authority,
|
|
),
|
|
reconciliation: new LocalSqlitePluginPackageTaskReconciliationRepository(
|
|
authority,
|
|
fixture.registry,
|
|
),
|
|
snapshots: new LocalSqliteProjectToolDefinitionSnapshotRepository(
|
|
authority,
|
|
),
|
|
runs: new LocalSqliteRunRepository(authority),
|
|
quarantine: new LocalSqlitePluginPackageQuarantineRepository(authority, {
|
|
registry: fixture.registry,
|
|
activeSourceLimit: EDGE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT,
|
|
}),
|
|
};
|
|
}
|
|
|
|
function quarantineEvent(fixture, record = fixture.install.active) {
|
|
return createPluginPackageQuarantineEvent({
|
|
mutationId: `quarantine-${fixture.namespace}`,
|
|
revocationReceiptDigest: digest('d'),
|
|
impactDigest: digest('e'),
|
|
target: {
|
|
projectId: record.projectId,
|
|
packageName: record.packageName,
|
|
installationId: record.installationId,
|
|
lockDigest: record.lockDigest,
|
|
installState: record.state,
|
|
installVersion: record.version,
|
|
installRecordDigest: record.recordDigest,
|
|
activeLockDigest: record.activeLockDigest,
|
|
},
|
|
proposer: { type: 'user', id: 'owner-a' },
|
|
confirmer: { type: 'user', id: 'owner-b' },
|
|
authorizationMode: 'dual_control',
|
|
reasonCode: 'confirmed_key_compromise',
|
|
occurredAtMs: record.updatedAtMs + 1,
|
|
});
|
|
}
|
|
|
|
async function publishActivePackage(value) {
|
|
await activateInstall(value.install, value.fixture);
|
|
await value.materialized.publish(value.fixture.revision);
|
|
await value.automation.publish(
|
|
createInitialPluginPackageAutomationPublication(
|
|
value.fixture.revision,
|
|
value.fixture.registry,
|
|
value.fixture.install.active.updatedAtMs,
|
|
),
|
|
);
|
|
await value.reconciliation.reconcile(value.fixture.revision, {
|
|
async findActiveResourceGeneration() {
|
|
return value.fixture.revision.generation;
|
|
},
|
|
});
|
|
}
|
|
|
|
test('withdraws active Package Tasks and Tool source in one exact replayable transaction', async (t) => {
|
|
const value = await harness(t, 'sqlite-quarantine-active');
|
|
await publishActivePackage(value);
|
|
const event = quarantineEvent(value.fixture);
|
|
assert.deepEqual(
|
|
await value.quarantine.findTargetsByLockDigest(event.target.lockDigest),
|
|
[event.target],
|
|
);
|
|
|
|
let authorizationChecks = 0;
|
|
const created = await value.quarantine.quarantine(event, () => {
|
|
authorizationChecks += 1;
|
|
});
|
|
assert.equal(authorizationChecks, 2);
|
|
assert.equal(created.status, 'created');
|
|
assert.equal(created.receipt.capability.status, 'withdrawn');
|
|
assert.deepEqual(
|
|
created.receipt.capability.taskWithdrawals.map(
|
|
({ taskId, previousRevision, disabledRevision }) => ({
|
|
taskId,
|
|
previousRevision,
|
|
disabledRevision,
|
|
}),
|
|
),
|
|
[
|
|
{
|
|
taskId: `pkg:${value.fixture.packageName}:alpha`,
|
|
previousRevision: 1,
|
|
disabledRevision: 2,
|
|
},
|
|
{
|
|
taskId: `pkg:${value.fixture.packageName}:beta`,
|
|
previousRevision: 1,
|
|
disabledRevision: 2,
|
|
},
|
|
],
|
|
);
|
|
assert.equal(created.receipt.capability.retainedSourceCount, 0);
|
|
assert.notEqual(
|
|
created.receipt.capability.previousActiveVectorDigest,
|
|
created.receipt.capability.currentActiveVectorDigest,
|
|
);
|
|
assert.deepEqual(
|
|
value.client
|
|
.prepare(
|
|
`SELECT task_id AS "taskId", current_revision AS "revision"
|
|
FROM "QingLong3TaskDefinitions"
|
|
WHERE project_id = ? ORDER BY task_id`,
|
|
)
|
|
.all(value.fixture.projectId)
|
|
.map((row) => ({ ...row })),
|
|
[
|
|
{ taskId: `pkg:${value.fixture.packageName}:alpha`, revision: 2 },
|
|
{ taskId: `pkg:${value.fixture.packageName}:beta`, revision: 2 },
|
|
],
|
|
);
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3TaskDefinitionRevisions"
|
|
WHERE project_id = ? AND enabled = 0`,
|
|
)
|
|
.get(value.fixture.projectId).count,
|
|
2,
|
|
);
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3ProjectToolDefinitionSnapshotSources"
|
|
WHERE project_id = ? AND active_vector_digest = ?`,
|
|
)
|
|
.get(
|
|
value.fixture.projectId,
|
|
created.receipt.capability.currentActiveVectorDigest,
|
|
).count,
|
|
0,
|
|
);
|
|
assert.equal(
|
|
(await value.snapshots.findCurrent(value.fixture.projectId)).snapshot
|
|
.snapshotDigest,
|
|
created.receipt.capability.currentToolSnapshotDigest,
|
|
);
|
|
const automation = await value.automation.findCurrent(
|
|
value.fixture.projectId,
|
|
value.fixture.packageName,
|
|
);
|
|
assert.equal(automation.state, 'withdrawn');
|
|
assert.equal(automation.lifecycleEventDigest, event.eventDigest);
|
|
assert.equal(automation.version, 2);
|
|
|
|
const replay = await value.quarantine.quarantine(event, () => {
|
|
authorizationChecks += 1;
|
|
});
|
|
assert.equal(authorizationChecks, 4);
|
|
assert.equal(replay.status, 'existing');
|
|
assert.deepEqual(replay.receipt, created.receipt);
|
|
assert.deepEqual(
|
|
await value.quarantine.findByEventDigest(event.eventDigest),
|
|
created.receipt,
|
|
);
|
|
});
|
|
|
|
test('records a queued target without inventing Task or Tool withdrawal', async (t) => {
|
|
const value = await harness(t, 'sqlite-quarantine-queued');
|
|
await value.install.create(value.fixture.install.create);
|
|
const event = quarantineEvent(value.fixture, value.fixture.install.queued);
|
|
const created = await value.quarantine.quarantine(event, () => {});
|
|
assert.equal(created.receipt.capability.status, 'not_active');
|
|
assert.deepEqual(created.receipt.capability.taskWithdrawals, []);
|
|
assert.deepEqual(await value.install.listRecoveryPage({ limit: 1 }), {
|
|
records: [],
|
|
truncated: false,
|
|
});
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3ProjectToolDefinitionSnapshots"`,
|
|
)
|
|
.get().count,
|
|
0,
|
|
);
|
|
});
|
|
|
|
test('rolls back every withdrawal fact when the target install advanced', async (t) => {
|
|
const value = await harness(t, 'sqlite-quarantine-stale');
|
|
await publishActivePackage(value);
|
|
const stale = quarantineEvent(value.fixture, {
|
|
...value.fixture.install.active,
|
|
version: value.fixture.install.active.version - 1,
|
|
});
|
|
await assert.rejects(
|
|
value.quarantine.quarantine(stale, () => {}),
|
|
PluginPackageQuarantineConflictError,
|
|
);
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3PluginPackageQuarantineEvents"`,
|
|
)
|
|
.get().count,
|
|
0,
|
|
);
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3TaskDefinitionRevisions"
|
|
WHERE project_id = ? AND enabled = 0`,
|
|
)
|
|
.get(value.fixture.projectId).count,
|
|
0,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await value.automation.findCurrent(
|
|
value.fixture.projectId,
|
|
value.fixture.packageName,
|
|
)
|
|
).state,
|
|
'active',
|
|
);
|
|
});
|
|
|
|
test('fails closed when quarantine automation withdrawal evidence is rewound', async (t) => {
|
|
const value = await harness(t, 'sqlite-quarantine-automation-corrupt');
|
|
await publishActivePackage(value);
|
|
const event = quarantineEvent(value.fixture);
|
|
await value.quarantine.quarantine(event, () => {});
|
|
const active = value.client
|
|
.prepare(
|
|
`SELECT publication_digest AS "publicationDigest"
|
|
FROM "QingLong3PluginPackageAutomationPublications"
|
|
WHERE project_id = ? AND package_name = ? AND state = 'active'`,
|
|
)
|
|
.get(value.fixture.projectId, value.fixture.packageName);
|
|
value.client.exec('PRAGMA foreign_keys = OFF');
|
|
value.client
|
|
.prepare(
|
|
`UPDATE "QingLong3PluginPackageAutomationPublicationHeads"
|
|
SET publication_digest = ?, state = 'active', version = 1
|
|
WHERE project_id = ? AND package_name = ?`,
|
|
)
|
|
.run(
|
|
active.publicationDigest,
|
|
value.fixture.projectId,
|
|
value.fixture.packageName,
|
|
);
|
|
value.client
|
|
.prepare(
|
|
`DELETE FROM "QingLong3PluginPackageAutomationPublications"
|
|
WHERE project_id = ? AND package_name = ? AND state = 'withdrawn'`,
|
|
)
|
|
.run(value.fixture.projectId, value.fixture.packageName);
|
|
await assert.rejects(
|
|
value.quarantine.findByEventDigest(event.eventDigest),
|
|
PluginPackageQuarantineUnavailableError,
|
|
);
|
|
});
|
|
|
|
test('rolls back withdrawal when the in-transaction Owner fence changes before commit', async (t) => {
|
|
const value = await harness(t, 'sqlite-quarantine-owner-fence');
|
|
await publishActivePackage(value);
|
|
let checks = 0;
|
|
await assert.rejects(
|
|
value.quarantine.quarantine(quarantineEvent(value.fixture), () => {
|
|
checks += 1;
|
|
if (checks === 2) throw new Error('Owner fence changed');
|
|
}),
|
|
(error) =>
|
|
error instanceof PluginPackageQuarantineUnavailableError &&
|
|
error.cause?.message === 'Owner fence changed',
|
|
);
|
|
assert.equal(checks, 2);
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3PluginPackageQuarantineEvents"`,
|
|
)
|
|
.get().count,
|
|
0,
|
|
);
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3TaskDefinitionRevisions"
|
|
WHERE project_id = ? AND enabled = 0`,
|
|
)
|
|
.get(value.fixture.projectId).count,
|
|
0,
|
|
);
|
|
});
|
|
|
|
test('fails closed when durable withdrawal task evidence is incomplete', async (t) => {
|
|
const value = await harness(t, 'sqlite-quarantine-corrupt');
|
|
await publishActivePackage(value);
|
|
const event = quarantineEvent(value.fixture);
|
|
await value.quarantine.quarantine(event, () => {});
|
|
value.client.exec('PRAGMA foreign_keys = OFF');
|
|
value.client
|
|
.prepare(
|
|
`DELETE FROM "QingLong3PluginPackageWithdrawalTasks"
|
|
WHERE event_digest = ? AND task_id = ?`,
|
|
)
|
|
.run(event.eventDigest, `pkg:${value.fixture.packageName}:alpha`);
|
|
await assert.rejects(
|
|
value.quarantine.findByEventDigest(event.eventDigest),
|
|
PluginPackageQuarantineUnavailableError,
|
|
);
|
|
await assert.rejects(
|
|
auditLocalSqliteReadiness(value.client),
|
|
LocalSqliteReadinessError,
|
|
);
|
|
});
|
|
|
|
test('rejects dispatch of a Run pinned to the quarantined Package Task revision', async (t) => {
|
|
const value = await harness(t, 'sqlite-quarantine-run-fence');
|
|
await publishActivePackage(value);
|
|
const task = value.client
|
|
.prepare(
|
|
`SELECT head.task_id AS "taskId", revision.revision,
|
|
revision.content_digest AS "contentDigest"
|
|
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 LIMIT 1`,
|
|
)
|
|
.get(value.fixture.projectId);
|
|
const run = {
|
|
id: '019f9a00-0000-4000-a000-000000000001',
|
|
projectId: value.fixture.projectId,
|
|
taskId: task.taskId,
|
|
taskRevision: createTaskDefinitionRevisionRef({
|
|
revision: task.revision,
|
|
contentDigest: task.contentDigest,
|
|
}),
|
|
taskName: 'quarantine fence',
|
|
triggerType: 'manual',
|
|
executionOrigin: 'manual',
|
|
executionOwner: 'runtime',
|
|
status: 'queued',
|
|
version: 0,
|
|
eventSequence: 0,
|
|
priority: 0,
|
|
createdAtMs: 1_000,
|
|
queuedAtMs: 1_001,
|
|
};
|
|
await value.runs.transaction((transaction) => transaction.insertRun(run));
|
|
await value.quarantine.quarantine(quarantineEvent(value.fixture), () => {});
|
|
await assert.rejects(
|
|
value.runs.transaction((transaction) =>
|
|
transaction.compareAndSetRun(
|
|
{ ...run, status: 'dispatching', version: 1 },
|
|
0,
|
|
),
|
|
),
|
|
RunRepositoryConstraintError,
|
|
);
|
|
assert.equal((await value.runs.findRunById(run.id)).status, 'queued');
|
|
});
|
|
|
|
test('rejects a Package Run while lifecycle is disabled and admits it after enable', async (t) => {
|
|
const value = await harness(t, 'sqlite-lifecycle-run-fence');
|
|
await publishActivePackage(value);
|
|
const task = value.client
|
|
.prepare(
|
|
`SELECT head.task_id AS "taskId", revision.revision,
|
|
revision.content_digest AS "contentDigest"
|
|
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 LIMIT 1`,
|
|
)
|
|
.get(value.fixture.projectId);
|
|
const run = {
|
|
id: '019f9a00-0000-4000-a000-000000000002',
|
|
projectId: value.fixture.projectId,
|
|
taskId: task.taskId,
|
|
taskRevision: createTaskDefinitionRevisionRef({
|
|
revision: task.revision,
|
|
contentDigest: task.contentDigest,
|
|
}),
|
|
taskName: 'lifecycle fence',
|
|
triggerType: 'manual',
|
|
executionOrigin: 'manual',
|
|
executionOwner: 'runtime',
|
|
status: 'queued',
|
|
version: 0,
|
|
eventSequence: 0,
|
|
priority: 0,
|
|
createdAtMs: 1_000,
|
|
queuedAtMs: 1_001,
|
|
};
|
|
await value.runs.transaction((transaction) => transaction.insertRun(run));
|
|
value.client.exec('PRAGMA foreign_keys = OFF');
|
|
value.client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3PluginPackageLifecycleHeads" (
|
|
project_id, package_name, installation_id, lock_digest,
|
|
install_record_digest, version, disposition, event_digest,
|
|
updated_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, 1, 'disabled', ?, 2)`,
|
|
)
|
|
.run(
|
|
value.fixture.projectId,
|
|
value.fixture.packageName,
|
|
value.fixture.install.active.installationId,
|
|
value.fixture.install.active.lockDigest,
|
|
value.fixture.install.active.recordDigest,
|
|
digest('f'),
|
|
);
|
|
value.client.exec('PRAGMA foreign_keys = ON');
|
|
|
|
await assert.rejects(
|
|
value.runs.transaction((transaction) =>
|
|
transaction.compareAndSetRun(
|
|
{ ...run, status: 'dispatching', version: 1 },
|
|
0,
|
|
),
|
|
),
|
|
RunRepositoryConstraintError,
|
|
);
|
|
assert.equal((await value.runs.findRunById(run.id)).status, 'queued');
|
|
|
|
value.client
|
|
.prepare(
|
|
`UPDATE "QingLong3PluginPackageLifecycleHeads"
|
|
SET disposition = 'active', version = 2, updated_at_ms = 3
|
|
WHERE project_id = ? AND package_name = ?`,
|
|
)
|
|
.run(value.fixture.projectId, value.fixture.packageName);
|
|
assert.equal(
|
|
await value.runs.transaction((transaction) =>
|
|
transaction.compareAndSetRun(
|
|
{ ...run, status: 'dispatching', version: 1 },
|
|
0,
|
|
),
|
|
),
|
|
true,
|
|
);
|
|
assert.equal((await value.runs.findRunById(run.id)).status, 'dispatching');
|
|
});
|
|
|
|
test('publishes quarantine storage only through its explicit subpath', () => {
|
|
const entrypoint = require('@qinglong/local-sqlite/plugin-package-quarantine');
|
|
assert.equal(
|
|
entrypoint.LocalSqlitePluginPackageQuarantineRepository,
|
|
LocalSqlitePluginPackageQuarantineRepository,
|
|
);
|
|
assert.equal(
|
|
require('../dist').LocalSqlitePluginPackageQuarantineRepository,
|
|
undefined,
|
|
);
|
|
});
|