mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
399 lines
13 KiB
JavaScript
399 lines
13 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
PluginPackageSecretBindingConflictError,
|
|
PluginPackageSecretBindingUnavailableError,
|
|
createPluginPackageSecretBinding,
|
|
} = require('@qinglong/runtime-core/plugin-package-secret-binding');
|
|
const {
|
|
createPluginPackageResourceGeneration,
|
|
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
|
|
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
|
const {
|
|
createPluginPackageSecretBindingTransitionPlan,
|
|
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-plan');
|
|
const {
|
|
createPluginPackageSecretBindingTransitionReceipt,
|
|
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt');
|
|
const {
|
|
LocalSqlitePluginPackageSecretBindingRepository,
|
|
} = require('../dist/plugin-package/secret-binding/repository');
|
|
const {
|
|
LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository,
|
|
} = require('../dist/plugin-package/secret-binding/transitionReceiptRepository');
|
|
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
|
|
|
|
const LOCK_DIGEST = 'a'.repeat(64);
|
|
const MANIFEST = {
|
|
apiVersion: 'qinglong.io/v1alpha1',
|
|
kind: 'Package',
|
|
metadata: {
|
|
name: 'example-monitor',
|
|
displayName: 'Example Monitor',
|
|
version: '1.0.0',
|
|
description: 'Secret binding repository fixture',
|
|
license: 'Apache-2.0',
|
|
},
|
|
spec: {
|
|
compatibility: {
|
|
qinglong: '>=3.0.0-0 <4.0.0',
|
|
architectures: ['arm64'],
|
|
deploymentProfiles: ['edge'],
|
|
},
|
|
runtimes: [],
|
|
resources: {
|
|
memory: { recommended: '32Mi' },
|
|
disk: { install: '4Mi', working: '8Mi' },
|
|
},
|
|
permissions: {
|
|
network: { allowedHosts: [] },
|
|
secrets: [{ name: 'TOKEN', required: true }],
|
|
tools: [],
|
|
},
|
|
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
|
|
},
|
|
};
|
|
|
|
function fixture(boundAtMs = 100) {
|
|
const generation = createPluginPackageResourceGeneration({
|
|
installationId: 'install-1',
|
|
projectId: 'project-1',
|
|
packageName: 'example-monitor',
|
|
lockDigest: LOCK_DIGEST,
|
|
generation: 1,
|
|
previousActiveLockDigest: null,
|
|
contentDigest: 'b'.repeat(64),
|
|
contents: MANIFEST.spec.contents,
|
|
});
|
|
const binding = createPluginPackageSecretBinding({
|
|
generation,
|
|
manifest: MANIFEST,
|
|
assignments: [
|
|
{
|
|
name: 'TOKEN',
|
|
secretRef: createSecretRef({
|
|
projectId: 'project-1',
|
|
name: 'runtime-token',
|
|
version: 2,
|
|
}),
|
|
},
|
|
],
|
|
authority: {
|
|
kind: 'local-owner-confirmation',
|
|
evidenceDigest: 'c'.repeat(64),
|
|
},
|
|
boundAtMs,
|
|
});
|
|
return { binding, generation };
|
|
}
|
|
|
|
async function harness(state = 'active') {
|
|
const client = new DatabaseSync(':memory:');
|
|
client.exec('PRAGMA foreign_keys = ON');
|
|
await migrateLocalSqliteDatabase(client);
|
|
const isActive = state === 'active';
|
|
const isStaged = state === 'staged';
|
|
const source = fixture();
|
|
const previousLockDigest = 'f'.repeat(64);
|
|
const generation = isStaged
|
|
? createPluginPackageResourceGeneration({
|
|
installationId: 'install-2',
|
|
projectId: 'project-1',
|
|
packageName: 'example-monitor',
|
|
lockDigest: LOCK_DIGEST,
|
|
generation: 2,
|
|
previousActiveLockDigest: previousLockDigest,
|
|
contentDigest: 'b'.repeat(64),
|
|
contents: MANIFEST.spec.contents,
|
|
})
|
|
: source.generation;
|
|
const binding = isStaged
|
|
? createPluginPackageSecretBinding({
|
|
generation,
|
|
manifest: MANIFEST,
|
|
assignments: source.binding.entries.map(({ name, secretRef }) => ({
|
|
name,
|
|
secretRef,
|
|
})),
|
|
authority: source.binding.authority,
|
|
boundAtMs: source.binding.boundAtMs,
|
|
})
|
|
: source.binding;
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3Projects"
|
|
(id, name, slug, status, version, created_at_ms, updated_at_ms)
|
|
VALUES ('project-1', 'Project 1', 'project-1', 'active', 1, 1, 1)`,
|
|
)
|
|
.run();
|
|
const recordDigest = 'd'.repeat(64);
|
|
const lockJson = JSON.stringify({
|
|
lockDigest: LOCK_DIGEST,
|
|
projectId: 'project-1',
|
|
packageName: 'example-monitor',
|
|
manifestDigest: binding.target.manifestDigest,
|
|
});
|
|
const recordJson = JSON.stringify({
|
|
installationId: isStaged ? 'install-2' : 'install-1',
|
|
projectId: 'project-1',
|
|
packageName: 'example-monitor',
|
|
lockDigest: LOCK_DIGEST,
|
|
state,
|
|
version: 1,
|
|
recordDigest,
|
|
});
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3PluginPackageInstalls" (
|
|
installation_id, project_id, package_name, package_version,
|
|
operation, lock_digest, target_generation,
|
|
previous_active_lock_digest, active_lock_digest, state, version,
|
|
last_mutation_id, last_mutation_digest, lock_json, record_json,
|
|
record_digest, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, '1.0.0', 'install', ?, ?, ?, ?, ?, 1,
|
|
'mutation-1', ?, ?, ?, ?, 1, 1)`,
|
|
)
|
|
.run(
|
|
isStaged ? 'install-2' : 'install-1',
|
|
'project-1',
|
|
'example-monitor',
|
|
LOCK_DIGEST,
|
|
generation.generation,
|
|
isStaged ? previousLockDigest : null,
|
|
isActive ? LOCK_DIGEST : isStaged ? previousLockDigest : null,
|
|
state,
|
|
'e'.repeat(64),
|
|
lockJson,
|
|
recordJson,
|
|
recordDigest,
|
|
);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3PluginPackageInstallHeads"
|
|
(project_id, package_name, installation_id)
|
|
VALUES ('project-1', 'example-monitor', ?)`,
|
|
)
|
|
.run(isStaged ? 'install-2' : 'install-1');
|
|
if (isStaged) {
|
|
const previousRecordDigest = '9'.repeat(64);
|
|
const previousLockJson = JSON.stringify({
|
|
lockDigest: previousLockDigest,
|
|
projectId: 'project-1',
|
|
packageName: 'example-monitor',
|
|
manifestDigest: '8'.repeat(64),
|
|
});
|
|
const previousRecordJson = JSON.stringify({
|
|
installationId: 'install-1',
|
|
projectId: 'project-1',
|
|
packageName: 'example-monitor',
|
|
lockDigest: previousLockDigest,
|
|
state: 'active',
|
|
version: 1,
|
|
recordDigest: previousRecordDigest,
|
|
});
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3PluginPackageInstalls" (
|
|
installation_id, project_id, package_name, package_version,
|
|
operation, lock_digest, target_generation,
|
|
previous_active_lock_digest, active_lock_digest, state, version,
|
|
last_mutation_id, last_mutation_digest, lock_json, record_json,
|
|
record_digest, created_at_ms, updated_at_ms
|
|
) VALUES ('install-1', 'project-1', 'example-monitor', '0.9.0',
|
|
'install', ?, 1, NULL, ?, 'active', 1,
|
|
'mutation-previous', ?, ?, ?, ?, 0, 0)`,
|
|
)
|
|
.run(
|
|
previousLockDigest,
|
|
previousLockDigest,
|
|
'7'.repeat(64),
|
|
previousLockJson,
|
|
previousRecordJson,
|
|
previousRecordDigest,
|
|
);
|
|
}
|
|
return {
|
|
client,
|
|
binding,
|
|
generation,
|
|
repository: new LocalSqlitePluginPackageSecretBindingRepository(client),
|
|
};
|
|
}
|
|
|
|
test('publishes and exact-replays one active generation binding', async (t) => {
|
|
const value = await harness();
|
|
t.after(() => value.client.close());
|
|
const created = await value.repository.publish(value.binding);
|
|
assert.equal(created.status, 'created');
|
|
assert.deepEqual(created.binding, value.binding);
|
|
const replay = await value.repository.publish(value.binding);
|
|
assert.equal(replay.status, 'existing');
|
|
assert.deepEqual(
|
|
await value.repository.find(value.generation.generationDigest),
|
|
value.binding,
|
|
);
|
|
});
|
|
|
|
test('rejects inactive targets and conflicting content', async (t) => {
|
|
const inactive = await harness('failed');
|
|
t.after(() => inactive.client.close());
|
|
await assert.rejects(
|
|
inactive.repository.publish(inactive.binding),
|
|
PluginPackageSecretBindingConflictError,
|
|
);
|
|
|
|
const active = await harness();
|
|
t.after(() => active.client.close());
|
|
await active.repository.publish(active.binding);
|
|
await assert.rejects(
|
|
active.repository.publish(fixture(101).binding),
|
|
PluginPackageSecretBindingConflictError,
|
|
);
|
|
});
|
|
|
|
test('publishes a reviewed current staged generation but rejects post-stage states', async (t) => {
|
|
const staged = await harness('staged');
|
|
t.after(() => staged.client.close());
|
|
assert.equal(
|
|
(await staged.repository.publish(staged.binding)).status,
|
|
'created',
|
|
);
|
|
|
|
for (const state of ['queued', 'activating']) {
|
|
const rejected = await harness(state);
|
|
t.after(() => rejected.client.close());
|
|
await assert.rejects(
|
|
rejected.repository.publish(rejected.binding),
|
|
PluginPackageSecretBindingConflictError,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('persists one exact staged transition receipt and rejects durable drift', async (t) => {
|
|
const value = await harness('staged');
|
|
t.after(() => value.client.close());
|
|
const previousGeneration = createPluginPackageResourceGeneration({
|
|
installationId: 'install-1',
|
|
projectId: 'project-1',
|
|
packageName: 'example-monitor',
|
|
lockDigest: 'f'.repeat(64),
|
|
generation: 1,
|
|
previousActiveLockDigest: null,
|
|
contentDigest: '9'.repeat(64),
|
|
contents: MANIFEST.spec.contents,
|
|
});
|
|
const previousBinding = createPluginPackageSecretBinding({
|
|
generation: previousGeneration,
|
|
manifest: MANIFEST,
|
|
assignments: value.binding.entries.map(({ name, secretRef }) => ({
|
|
name,
|
|
secretRef,
|
|
})),
|
|
authority: value.binding.authority,
|
|
boundAtMs: 90,
|
|
});
|
|
value.client
|
|
.prepare(
|
|
`UPDATE "QingLong3PluginPackageInstalls"
|
|
SET lock_json = json_set(lock_json, '$.manifestDigest', ?)
|
|
WHERE installation_id = 'install-1'`,
|
|
)
|
|
.run(previousBinding.target.manifestDigest);
|
|
value.client
|
|
.prepare(
|
|
`UPDATE "QingLong3PluginPackageInstalls"
|
|
SET lock_json = json_set(lock_json, '$.manifestDigest', ?)
|
|
WHERE installation_id = 'install-2'`,
|
|
)
|
|
.run(value.binding.target.manifestDigest);
|
|
value.client
|
|
.prepare(
|
|
`UPDATE "QingLong3PluginPackageInstallHeads"
|
|
SET installation_id = 'install-1'
|
|
WHERE project_id = 'project-1' AND package_name = 'example-monitor'`,
|
|
)
|
|
.run();
|
|
await value.repository.publish(previousBinding);
|
|
value.client
|
|
.prepare(
|
|
`UPDATE "QingLong3PluginPackageInstallHeads"
|
|
SET installation_id = 'install-2'
|
|
WHERE project_id = 'project-1' AND package_name = 'example-monitor'`,
|
|
)
|
|
.run();
|
|
await value.repository.publish(value.binding);
|
|
const plan = createPluginPackageSecretBindingTransitionPlan({
|
|
previousTarget: previousBinding.target,
|
|
previousBinding,
|
|
previousAttemptGeneration: 1,
|
|
nextGeneration: value.generation,
|
|
nextManifest: MANIFEST,
|
|
assignments: value.binding.entries.map(({ name, secretRef }) => ({
|
|
name,
|
|
secretRef,
|
|
})),
|
|
plannedAtMs: 95,
|
|
});
|
|
const receipt = createPluginPackageSecretBindingTransitionReceipt({
|
|
transitionPlan: plan,
|
|
authority: value.binding.authority,
|
|
binding: value.binding,
|
|
committedAtMs: value.binding.boundAtMs,
|
|
});
|
|
const receipts =
|
|
new LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository(
|
|
value.client,
|
|
);
|
|
assert.equal(receipts.publishInTransaction(receipt).status, 'created');
|
|
assert.equal(receipts.publishInTransaction(receipt).status, 'existing');
|
|
assert.deepEqual(
|
|
await receipts.find(value.generation.generationDigest),
|
|
receipt,
|
|
);
|
|
|
|
value.client.exec('PRAGMA ignore_check_constraints = ON');
|
|
value.client
|
|
.prepare(
|
|
`UPDATE "QingLong3PluginPackageSecretBindingTransitionReceipts"
|
|
SET receipt_json = json_set(receipt_json, '$.committedAtMs', 999)
|
|
WHERE generation_digest = ?`,
|
|
)
|
|
.run(value.generation.generationDigest);
|
|
await assert.rejects(
|
|
receipts.find(value.generation.generationDigest),
|
|
PluginPackageSecretBindingUnavailableError,
|
|
);
|
|
});
|
|
|
|
test('fails closed when durable binding JSON is changed in place', async (t) => {
|
|
const value = await harness();
|
|
t.after(() => value.client.close());
|
|
await value.repository.publish(value.binding);
|
|
value.client.exec('PRAGMA ignore_check_constraints = ON');
|
|
value.client
|
|
.prepare(
|
|
`UPDATE "QingLong3PluginPackageSecretBindings"
|
|
SET binding_json = json_set(binding_json, '$.boundAtMs', 999)
|
|
WHERE generation_digest = ?`,
|
|
)
|
|
.run(value.generation.generationDigest);
|
|
await assert.rejects(
|
|
value.repository.find(value.generation.generationDigest),
|
|
PluginPackageSecretBindingUnavailableError,
|
|
);
|
|
});
|
|
|
|
test('publishes storage only through the explicit subpath', () => {
|
|
assert.equal(
|
|
require('@qinglong/local-sqlite/plugin-package-secret-binding')
|
|
.LocalSqlitePluginPackageSecretBindingRepository,
|
|
LocalSqlitePluginPackageSecretBindingRepository,
|
|
);
|
|
assert.equal(
|
|
require('../dist').LocalSqlitePluginPackageSecretBindingRepository,
|
|
undefined,
|
|
);
|
|
});
|