const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const { DatabaseSync } = require('node:sqlite'); const { test } = require('node:test'); const { runMigrationStream, } = require('@qinglong/runtime-core/migration-stream'); const { createLocalExecutionContextRecipe, createLocalTaskExecutionRevision, } = require('@qinglong/runtime-core/local-dispatch'); const { createTaskDefinitionRecord, } = require('@qinglong/runtime-core/task-definition'); const { createBuiltInTaskSpecSemanticRegistry, } = require('@qinglong/runtime-core/task-spec-semantic'); const { compileLocalCommandTaskDefinition, } = require('@qinglong/runtime-core/task-definition-execution-compiler'); const { LocalSqliteConfigurationError, LocalSqliteReadinessError, auditLocalSqlitePath, migrateLocalSqlitePath, openLocalSqliteRuntimeDatabase, } = require('../dist'); const { localSqliteMigrationDefinition, } = require('../dist/migration/migration'); const { LocalSqliteMigrationStreamStore, } = require('../dist/migration/migrationStreamStore'); function fixture(t) { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-sqlite-')); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); return { directory, databasePath: path.join(directory, 'qinglong3.sqlite'), }; } test('creates a reviewed edge database and opens runtime only after readiness', async (t) => { const { databasePath } = fixture(t); const options = { databasePath, profile: 'edge' }; const migrated = await migrateLocalSqlitePath(options); assert.deepEqual(migrated.readiness.migrationIds, [ '0001-run-core', '0002-capability', '0003-completion-receipt-journal', '0004-capability-v2', '0005-local-dispatch-plan', '0006-capability-v3', '0007-local-secret-envelopes', '0008-capability-v4', '0009-local-project-policy-audit', '0010-capability-v5', '0011-local-identity-credential', '0012-capability-v6', '0013-local-owner-bootstrap', '0014-capability-v7', '0015-local-owner-delivery-acknowledgements', '0016-capability-v8', '0017-api-credential-pepper-bindings', '0018-capability-v9', '0019-local-owner-pepper-catalog', '0020-capability-v10', '0021-local-owner-credential-recovery', '0022-capability-v11', '0023-local-owner-pepper-material-gc', '0024-capability-v12', '0025-local-owner-delivery-acknowledgement-gc', '0026-capability-v13', '0027-task-definitions', '0028-capability-v14', '0029-local-execution-revision-digest', '0030-capability-v15', '0031-trigger-definitions', '0032-capability-v16', '0033-legacy-adoption-ledger', '0034-capability-v17', '0035-local-scheduler', '0036-capability-v18', '0037-plugin-package-installs', '0038-capability-v19', '0039-approved-actions', '0040-capability-v20', '0041-plugin-package-admission-receipts', '0042-capability-v21', '0043-approved-action-executions-and-package-proposals', '0044-capability-v22', '0045-plugin-package-materialized-revisions', '0046-capability-v23', '0047-plugin-package-task-reconciliations', '0048-capability-v24', '0049-project-tool-definition-snapshots', '0050-capability-v25', '0051-step-runs', '0052-capability-v26', '0053-tool-execution-evidence', '0054-capability-v27', '0055-tool-execution-start-barriers', '0056-capability-v28', '0057-tool-invocation-artifacts', '0058-capability-v29', '0059-tool-execution-artifact-bindings', '0060-capability-v30', '0061-tool-execution-completions', '0062-capability-v31', '0063-tool-execution-failure-completions', '0064-capability-v32', '0065-tool-result-key-catalog', '0066-capability-v33', '0067-tool-result-rekey-overlays', '0068-capability-v34', '0069-plugin-package-quarantine', '0070-capability-v35', '0071-local-identity-credential-administration', '0072-capability-v36', '0073-local-project-administration', '0074-capability-v37', '0075-security-audit-compactions', '0076-capability-v38', '0077-plugin-package-lifecycle', '0078-capability-v39', '0079-plugin-package-automation-publications', '0080-capability-v40', '0081-plugin-package-workflow-admissions', '0082-capability-v41', '0083-plugin-package-workflow-task-attempt-admissions', '0084-capability-v42', '0085-plugin-package-workflow-run-list-index', '0086-capability-v43', '0087-run-attempt-log-retention', '0088-capability-v44', '0089-plugin-package-automation-disposition-events', '0090-capability-v45', '0091-plugin-package-secret-bindings', '0092-capability-v46', '0093-plugin-package-secret-materialization-guard', '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, 49); assert.equal(migrated.readiness.journalMode, 'delete'); assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600); assert.deepEqual(await auditLocalSqlitePath(options), migrated.readiness); const runtime = await openLocalSqliteRuntimeDatabase(options); assert.equal(runtime.profile, 'edge'); assert.deepEqual(runtime.readiness, migrated.readiness); assert.deepEqual(Object.keys(runtime.localDispatch).sort(), [ 'appendLocalExecutionContextRecipe', 'appendLocalTaskExecutionRevision', 'listLocalDispatchCandidates', 'resolveLocalExecutionContextRecipe', 'resolveLocalTaskExecutionRevision', ]); assert.deepEqual(Object.keys(runtime.executionControl).sort(), [ 'listLocalActiveExecutions', 'listLocalExecutionControlCandidates', ]); assert.deepEqual(Object.keys(runtime.startupRecovery), ['inspectCandidates']); assert.deepEqual(Object.keys(runtime.completionReceipts).sort(), [ 'listCandidates', 'markQuarantined', 'register', 'resolve', ]); for (const capability of [ runtime.localDispatch, runtime.executionControl, runtime.startupRecovery, runtime.completionReceipts, ]) { assert.equal(Object.isFrozen(capability), true); assert.notEqual(capability, runtime.runRepository); } assert.equal(runtime.runRepository.register, undefined); assert.equal(runtime.runRepository.listLocalDispatchCandidates, undefined); assert.equal( typeof runtime.localSecrets.resolveLocalSecretEnvelopes, 'function', ); assert.equal(typeof runtime.projectPolicy.resolve, 'function'); assert.equal( typeof runtime.localSecretAdministration .appendAuthorizedLocalSecretEnvelope, 'function', ); assert.equal(typeof runtime.securityAudit.record, 'function'); assert.equal(typeof runtime.apiCredentials.resolve, 'function'); assert.equal( typeof runtime.taskDefinitions.findCurrentTaskDefinition, 'function', ); assert.equal(typeof runtime.triggers.findCurrentTrigger, 'function'); assert.equal( typeof (await runtime.projectToolDefinitionSnapshots()).findCurrent, 'function', ); assert.equal( typeof (await runtime.pluginPackageAutomationPublications()) .listPendingPage, 'function', ); await Promise.all([runtime.close(), runtime.close()]); for (const operation of [ runtime.runRepository.findRunById('closed-run'), runtime.localDispatch.listLocalDispatchCandidates({ limit: 1 }), runtime.executionControl.listLocalExecutionControlCandidates({ observedAtMs: 1, limit: 1, }), runtime.startupRecovery.inspectCandidates({ limit: 1 }), runtime.completionReceipts.listCandidates({ observedAtMs: 1, limit: 1, }), ]) { await assert.rejects( operation, (error) => error?.name === 'RunRepositoryOperationError', ); } }); test('rejects malformed and unbound Package Secret materialized revisions at the database boundary', async () => { const client = new DatabaseSync(':memory:'); try { client.exec('PRAGMA foreign_keys = ON'); await runMigrationStream({ stream: localSqliteMigrationDefinition, store: new LocalSqliteMigrationStreamStore(client), }); const insert = client.prepare( `INSERT INTO "QingLong3PluginPackageMaterializedRevisions" ( generation_digest, project_id, package_name, generation, lock_digest, manifest_digest, revision_digest, revision_json, created_at_ms ) VALUES (?, 'default', 'secret-guard', 1, ?, ?, ?, ?, 1)`, ); const digest = 'a'.repeat(64); const revision = ({ manifest, resources = [] }) => ({ schema: 'qinglong/plugin-package-materialized-revision@v1', generation: { installationId: 'install-secret-guard', projectId: 'default', packageName: 'secret-guard', lockDigest: digest, generation: 1, generationDigest: digest, }, manifestDigest: digest, manifest, resources, revisionDigest: digest, createdAtMs: 1, }); assert.throws( () => insert.run( digest, digest, digest, digest, JSON.stringify(revision({ manifest: {} })), ), /permission declarations are malformed/, ); assert.throws( () => insert.run( digest, digest, digest, digest, JSON.stringify( revision({ manifest: { spec: { permissions: { secrets: [] } } }, resources: [ { kind: 'task', value: { spec: { config: { environment: [ { name: 'TOKEN', kind: 'secret', secretRef: 'qlsecret://default/runtime-token?version=1', }, ], }, }, }, }, ], }), ), ), /outside Package binding/, ); } finally { client.close(); } }); test('standalone opts into bounded WAL while edge keeps rollback journal', async (t) => { const { databasePath } = fixture(t); const options = { databasePath, profile: 'standalone' }; const migrated = await migrateLocalSqlitePath(options); assert.equal(migrated.readiness.journalMode, 'wal'); const runtime = await openLocalSqliteRuntimeDatabase(options); assert.equal(runtime.readiness.journalMode, 'wal'); await runtime.close(); }); test('loads one shared trusted Tool storage bundle only when requested', async (t) => { const { databasePath } = fixture(t); const options = { databasePath, profile: 'edge' }; await migrateLocalSqlitePath(options); const runtime = await openLocalSqliteRuntimeDatabase(options); const first = await runtime.trustedToolStorage(); const second = await runtime.trustedToolStorage(); assert.equal(first, second); assert.equal(Object.isFrozen(first), true); assert.equal(typeof first.invocationArtifacts.findInput, 'function'); assert.equal(typeof first.stepRuns.findById, 'function'); assert.equal(typeof first.startBarriers.findByStartId, 'function'); assert.equal(typeof first.completions.findByStartId, 'function'); assert.equal(typeof first.failureCompletions.findByStartId, 'function'); assert.equal(typeof first.resultKeyCatalog.findCurrent, 'function'); assert.equal(first.resultKeyCatalog.append, undefined); assert.equal(typeof first.resultRekeys.findHeadByArtifactId, 'function'); assert.equal(first.resultRekeys.append, undefined); assert.equal(typeof first.toolDefinitionSnapshots.findCurrent, 'function'); assert.equal( first.toolDefinitionSnapshots, await runtime.projectToolDefinitionSnapshots(), ); await runtime.close(); }); test('backfills legacy credential provenance and recovery-required catalog state', async () => { const client = new DatabaseSync(':memory:'); try { client.exec('PRAGMA foreign_keys = ON'); const legacyStream = { ...localSqliteMigrationDefinition, migrations: localSqliteMigrationDefinition.migrations.slice(0, 16), }; await runMigrationStream({ stream: legacyStream, store: new LocalSqliteMigrationStreamStore(client), }); client.exec(` INSERT INTO "QingLong3IdentitySubjects" ( "subject_type", "subject_id", "status", "version", "created_at_ms", "updated_at_ms" ) VALUES ('user', 'legacy-user', 'active', 1, 1, 1); INSERT INTO "QingLong3ApiCredentials" ( "credential_id", "version", "state", "subject_type", "subject_id", "secret_digest", "created_at_ms", "not_before_at_ms", "expires_at_ms" ) VALUES ( 'legacy-credential', 1, 'active', 'user', 'legacy-user', '${'a'.repeat(64)}', 1, 1, 2 ); `); await localSqliteMigrationDefinition.migrations[16].up({ client }); client.exec('BEGIN IMMEDIATE'); try { await localSqliteMigrationDefinition.migrations[18].up({ client }); client.exec('COMMIT'); } catch (error) { if (client.isTransaction) client.exec('ROLLBACK'); throw error; } assert.deepEqual( { ...client .prepare( `SELECT credential_id, credential_version, pepper_key_id FROM "QingLong3ApiCredentialPepperBindings"`, ) .get(), }, { credential_id: 'legacy-credential', credential_version: 1, pepper_key_id: 'legacy-v1', }, ); assert.deepEqual( { ...client .prepare( `SELECT pepper_key_id, state, version, material_digest FROM "QingLong3LocalOwnerPepperKeys"`, ) .get(), }, { pepper_key_id: 'legacy-v1', state: 'recovery_required', version: 1, material_digest: null, }, ); } finally { client.close(); } }); test('backfills v14 execution revisions with a verified independent digest', async () => { const client = new DatabaseSync(':memory:'); try { client.exec('PRAGMA foreign_keys = ON'); await runMigrationStream({ stream: { ...localSqliteMigrationDefinition, migrations: localSqliteMigrationDefinition.migrations.slice(0, 28), }, store: new LocalSqliteMigrationStreamStore(client), }); const recipe = createLocalExecutionContextRecipe({ environment: [{ name: 'VALUE', kind: 'public', value: 'legacy' }], createdAtMs: 10, }); client .prepare( `INSERT INTO "QingLong3LocalExecutionContextRecipes" ( "context_ref", "environment_json", "content_digest", "created_at_ms" ) VALUES (?, ?, ?, ?)`, ) .run( recipe.contextRef, JSON.stringify(recipe.environment), recipe.contentDigest, recipe.createdAtMs, ); client .prepare( `INSERT INTO "QingLong3LocalTaskExecutionRevisions" ( "project_id", "task_id", "task_revision", "executor_type", "command_json", "working_directory", "timeout_ms", "context_ref", "created_at_ms" ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .run( 'default', 'legacy-task', 'legacy-revision', 'local_process', JSON.stringify({ kind: 'argv', file: '/bin/echo', args: ['legacy'] }), '/tmp', 1000, recipe.contextRef, 11, ); const registry = createBuiltInTaskSpecSemanticRegistry(); const taskCommand = { projectId: 'default', taskId: 'task-definition-backfill', expectedRevision: null, mutationId: '019f7200-0000-7000-8000-000000000029', name: 'Backfilled TaskDefinition', kind: 'command', spec: registry.normalize({ projectId: 'default', taskId: 'task-definition-backfill', kind: 'command', spec: { schema: 'qinglong/command@v1', config: { command: { kind: 'argv', file: '/bin/echo', args: ['definition'], }, }, }, }), labels: {}, enabled: true, occurredAtMs: 12, }; const taskDefinition = createTaskDefinitionRecord(taskCommand, 12); const taskPlan = compileLocalCommandTaskDefinition( taskDefinition, registry, ); client .prepare( `INSERT INTO "QingLong3TaskDefinitions" ( "project_id", "task_id", "current_revision", "created_at_ms", "updated_at_ms" ) VALUES (?, ?, ?, ?, ?)`, ) .run( taskDefinition.projectId, taskDefinition.taskId, taskDefinition.revision, taskDefinition.createdAtMs, taskDefinition.updatedAtMs, ); client .prepare( `INSERT INTO "QingLong3TaskDefinitionRevisions" ( "project_id", "task_id", "revision", "mutation_id", "name", "description", "kind", "spec_json", "labels_json", "enabled", "content_digest", "created_at_ms" ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .run( taskDefinition.projectId, taskDefinition.taskId, taskDefinition.revision, taskDefinition.mutationId, taskDefinition.name, null, taskDefinition.kind, JSON.stringify(taskDefinition.spec), JSON.stringify(taskDefinition.labels), 1, taskDefinition.contentDigest, taskDefinition.updatedAtMs, ); await runMigrationStream({ stream: localSqliteMigrationDefinition, store: new LocalSqliteMigrationStreamStore(client), }); const expected = createLocalTaskExecutionRevision({ projectId: 'default', taskId: 'legacy-task', taskRevision: 'legacy-revision', executorType: 'local_process', command: { kind: 'argv', file: '/bin/echo', args: ['legacy'] }, workingDirectory: '/tmp', timeoutMs: 1000, contextRef: recipe.contextRef, createdAtMs: 11, }); const stored = client .prepare( `SELECT "content_digest" AS "contentDigest", "command_json" AS "commandJson" FROM "QingLong3LocalTaskExecutionRevisions"`, ) .get(); assert.equal(stored.contentDigest, expected.contentDigest); assert.equal(stored.commandJson, JSON.stringify(expected.command)); assert.deepEqual( { ...client .prepare( `SELECT "content_digest" AS "contentDigest", "context_ref" AS "contextRef" FROM "QingLong3LocalTaskExecutionRevisions" WHERE "project_id" = ? AND "task_id" = ? AND "task_revision" = ?`, ) .get( taskDefinition.projectId, taskDefinition.taskId, taskPlan.executionRevision.taskRevision, ), }, { contentDigest: taskPlan.executionRevision.contentDigest, contextRef: taskPlan.contextRecipe.contextRef, }, ); assert.deepEqual( { ...client .prepare( `SELECT contract_version, migration_id FROM "QingLong3SchemaCapabilities" WHERE contract_name = 'local-control-core'`, ) .get(), }, { contract_version: 49, migration_id: '0097-plugin-package-secret-binding-transition-receipts', }, ); } finally { client.close(); } }); test('rolls the digest migration back when a legacy revision is not canonical', async () => { const client = new DatabaseSync(':memory:'); try { client.exec('PRAGMA foreign_keys = ON'); await runMigrationStream({ stream: { ...localSqliteMigrationDefinition, migrations: localSqliteMigrationDefinition.migrations.slice(0, 28), }, store: new LocalSqliteMigrationStreamStore(client), }); const recipe = createLocalExecutionContextRecipe({ environment: [], createdAtMs: 1, }); client .prepare( `INSERT INTO "QingLong3LocalExecutionContextRecipes" ( "context_ref", "environment_json", "content_digest", "created_at_ms" ) VALUES (?, ?, ?, ?)`, ) .run(recipe.contextRef, '[]', recipe.contentDigest, 1); client .prepare( `INSERT INTO "QingLong3LocalTaskExecutionRevisions" ( "project_id", "task_id", "task_revision", "executor_type", "command_json", "working_directory", "timeout_ms", "context_ref", "created_at_ms" ) VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, ?)`, ) .run( 'default', 'corrupt-task', 'corrupt-revision', 'local_process', '{}', recipe.contextRef, 1, ); await assert.rejects( runMigrationStream({ stream: localSqliteMigrationDefinition, store: new LocalSqliteMigrationStreamStore(client), }), /command kind is invalid/, ); assert.equal( client .prepare( `SELECT COUNT(*) AS count FROM pragma_table_info( 'QingLong3LocalTaskExecutionRevisions' ) WHERE name = 'content_digest'`, ) .get().count, 0, ); assert.equal( client .prepare(`SELECT COUNT(*) AS count FROM "QingLong3SchemaMigrations"`) .get().count, 28, ); } finally { client.close(); } }); test('runtime entrypoint keeps migration, compiler and Plugin Package adapter lazy', () => { const script = ` require('@qinglong/local-sqlite/runtime'); const loaded = Object.keys(require.cache) .filter((entry) => /[\\/]migrations[\\/]|[\\/]migration\\.js$|taskDefinitionExecutionCompiler\\.js$|pluginPackageInstallRepository\\.js$|approvedActionExecutionRepository\\.js$|pluginPackageProposalRepository\\.js$/.test(entry), ); process.stdout.write(JSON.stringify(loaded)); `; const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); assert.deepEqual(JSON.parse(result.stdout), []); }); test('readiness inspection subpath excludes DDL and mutable repositories', () => { const script = ` const inspection = require(${JSON.stringify( path.resolve(__dirname, '../dist/readiness/readinessInspection.js'), )}); const loaded = Object.keys(require.cache) .filter((entry) => /[\\/]migrations[\\/]|[\\/]migration\\.js$|runRepository\\.js$|pluginPackageInstallRepository\\.js$/.test(entry), ); process.stdout.write(JSON.stringify({ inspection: typeof inspection.inspectLocalSqliteReadinessPath, loaded, })); `; const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); assert.deepEqual(JSON.parse(result.stdout), { inspection: 'function', loaded: [], }); }); test('Approved Action execution and Package proposal authorities require explicit subpaths', () => { const root = require('@qinglong/local-sqlite'); const execution = require('@qinglong/local-sqlite/approved-action-execution'); const proposal = require('@qinglong/local-sqlite/plugin-package-proposal'); assert.equal(root.LocalSqliteApprovedActionExecutionRepository, undefined); assert.equal( root.LocalSqlitePluginPackageInstallProposalRepository, undefined, ); assert.equal( typeof execution.LocalSqliteApprovedActionExecutionRepository, 'function', ); assert.equal( typeof proposal.LocalSqlitePluginPackageInstallProposalRepository, 'function', ); }); test('runtime bootstrap never auto-migrates an unprepared database', async (t) => { const { databasePath } = fixture(t); new DatabaseSync(databasePath).close(); await assert.rejects( openLocalSqliteRuntimeDatabase({ databasePath, profile: 'standalone' }), LocalSqliteReadinessError, ); const client = new DatabaseSync(databasePath, { readOnly: true }); try { assert.equal( client .prepare( `SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'QingLong3SchemaMigrations'`, ) .get(), undefined, ); } finally { client.close(); } }); test('fails closed for schema drift and migration checksum drift', async (t) => { const { databasePath } = fixture(t); const options = { databasePath, profile: 'standalone' }; await migrateLocalSqlitePath(options); const client = new DatabaseSync(databasePath); client.exec('DROP INDEX ql3_local_events_run_sequence_uidx'); client .prepare( `UPDATE "QingLong3SchemaMigrations" SET checksum = ? WHERE migration_id = '0001-run-core'`, ) .run('0'.repeat(64)); client.close(); await assert.rejects( auditLocalSqlitePath(options), (error) => error instanceof LocalSqliteReadinessError && /audit failed/.test(error.message), ); }); test('excludes reviewed optional feature tables while preserving unknown table drift evidence', async (t) => { const { databasePath } = fixture(t); const options = { databasePath, profile: 'edge' }; await migrateLocalSqlitePath(options); const client = new DatabaseSync(databasePath); 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, 81); const unknownClient = new DatabaseSync(databasePath); unknownClient.exec('CREATE TABLE "UserExtensionData" (id TEXT PRIMARY KEY)'); unknownClient.close(); assert.equal((await auditLocalSqlitePath(options)).tableCount, 82); const triggerClient = new DatabaseSync(databasePath); triggerClient.exec(` CREATE TRIGGER unreviewed_run_trigger AFTER INSERT ON "Runs" BEGIN SELECT 1; END `); triggerClient.close(); await assert.rejects( auditLocalSqlitePath(options), /reviewed trigger contract is incompatible/, ); }); test('rejects wrong Profiles, relative paths and symlink targets before opening', async (t) => { const { directory, databasePath } = fixture(t); await assert.rejects( () => migrateLocalSqlitePath({ databasePath, profile: 'cluster-control', }), LocalSqliteConfigurationError, ); await assert.rejects( () => migrateLocalSqlitePath({ databasePath: 'relative.db', profile: 'edge' }), LocalSqliteConfigurationError, ); const target = path.join(directory, 'target.sqlite'); new DatabaseSync(target).close(); const link = path.join(directory, 'linked.sqlite'); fs.symlinkSync(target, link); await assert.rejects( migrateLocalSqlitePath({ databasePath: link, profile: 'edge' }), LocalSqliteConfigurationError, ); });