mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): gate local secret transitions before activation
This commit is contained in:
@@ -146,9 +146,11 @@ test('creates a reviewed edge database and opens runtime only after readiness',
|
||||
'0094-capability-v47',
|
||||
'0095-plugin-package-secret-binding-target-guard',
|
||||
'0096-capability-v48',
|
||||
'0097-plugin-package-secret-binding-transition-receipts',
|
||||
'0098-capability-v49',
|
||||
]);
|
||||
assert.equal(migrated.readiness.contractName, 'local-control-core');
|
||||
assert.equal(migrated.readiness.contractVersion, 48);
|
||||
assert.equal(migrated.readiness.contractVersion, 49);
|
||||
assert.equal(migrated.readiness.journalMode, 'delete');
|
||||
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
|
||||
|
||||
@@ -594,8 +596,8 @@ test('backfills v14 execution revisions with a verified independent digest', asy
|
||||
.get(),
|
||||
},
|
||||
{
|
||||
contract_version: 48,
|
||||
migration_id: '0095-plugin-package-secret-binding-target-guard',
|
||||
contract_version: 49,
|
||||
migration_id: '0097-plugin-package-secret-binding-transition-receipts',
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
@@ -782,19 +784,19 @@ test('excludes reviewed optional feature tables while preserving unknown table d
|
||||
const options = { databasePath, profile: 'edge' };
|
||||
await migrateLocalSqlitePath(options);
|
||||
const client = new DatabaseSync(databasePath);
|
||||
assert.equal((await auditLocalSqlitePath(options)).tableCount, 80);
|
||||
assert.equal((await auditLocalSqlitePath(options)).tableCount, 81);
|
||||
client.exec(
|
||||
'CREATE TABLE "ModelInvocationFeatureHead" (feature_id TEXT PRIMARY KEY)',
|
||||
);
|
||||
client.close();
|
||||
|
||||
assert.equal((await auditLocalSqlitePath(options)).tableCount, 80);
|
||||
assert.equal((await auditLocalSqlitePath(options)).tableCount, 81);
|
||||
|
||||
const unknownClient = new DatabaseSync(databasePath);
|
||||
unknownClient.exec('CREATE TABLE "UserExtensionData" (id TEXT PRIMARY KEY)');
|
||||
unknownClient.close();
|
||||
|
||||
assert.equal((await auditLocalSqlitePath(options)).tableCount, 81);
|
||||
assert.equal((await auditLocalSqlitePath(options)).tableCount, 82);
|
||||
|
||||
const triggerClient = new DatabaseSync(databasePath);
|
||||
triggerClient.exec(`
|
||||
|
||||
@@ -11,9 +11,18 @@ 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);
|
||||
@@ -247,7 +256,10 @@ test('rejects inactive targets and conflicting content', async (t) => {
|
||||
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');
|
||||
assert.equal(
|
||||
(await staged.repository.publish(staged.binding)).status,
|
||||
'created',
|
||||
);
|
||||
|
||||
for (const state of ['queued', 'activating']) {
|
||||
const rejected = await harness(state);
|
||||
@@ -259,6 +271,102 @@ test('publishes a reviewed current staged generation but rejects post-stage stat
|
||||
}
|
||||
});
|
||||
|
||||
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());
|
||||
|
||||
@@ -156,7 +156,7 @@ test('atomically admits one generation-bound Workflow Run and exactly replays it
|
||||
},
|
||||
{ runs: 1, steps: 2, events: 3, mutations: 2, admissions: 1 },
|
||||
);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 48);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 49);
|
||||
});
|
||||
|
||||
test('runs an optional authorization guard inside new and replay transactions', async (t) => {
|
||||
@@ -288,7 +288,7 @@ test('exactly replays immutable admission after the Workflow StepRun advances',
|
||||
},
|
||||
{ status: 'running', version: 5, eventSequence: 5 },
|
||||
);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 48);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 49);
|
||||
});
|
||||
|
||||
test('fails closed before writing when the exact installation is not active', async (t) => {
|
||||
|
||||
+1
-1
@@ -231,7 +231,7 @@ test('atomically admits the exact reconciled local Task revision and replays it'
|
||||
stepAttemptCount: 0,
|
||||
},
|
||||
);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 48);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 49);
|
||||
});
|
||||
|
||||
test('bounds candidate paging before SQL and fences cancellation', async (t) => {
|
||||
|
||||
@@ -40,9 +40,9 @@ test('creates and exactly replays a reviewed rollout backup', async (t) => {
|
||||
await migrateLocalSqlitePath(state);
|
||||
const prepared = await createLocalSqliteRolloutBackup(state);
|
||||
assert.equal(prepared.status, 'prepared');
|
||||
assert.equal(prepared.contractVersion, 48);
|
||||
assert.equal(prepared.writeContractVersion, 48);
|
||||
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 48);
|
||||
assert.equal(prepared.contractVersion, 49);
|
||||
assert.equal(prepared.writeContractVersion, 49);
|
||||
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 49);
|
||||
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
|
||||
assert.equal(prepared.bytes > 0, true);
|
||||
assert.equal(prepared.pageCount > 0, true);
|
||||
|
||||
Reference in New Issue
Block a user