import assert from 'node:assert/strict' import { createHash } from 'node:crypto' import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { readFile } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { spawnSync } from 'node:child_process' import test from 'node:test' import { assertBackupReceiptDocument, assertExactActiveUploadIdIndex, assertExactBtreeIndex, assertExpectedDatabaseIdentity, assertTargetMigrationEvidence, databaseFingerprintHash, normalizeColumnDefault, requireExactCommit, sha256, verifiedBackupSummaryHash, } from './safe-production-migrate.mjs' test('accepts only an exact lowercase 40-character commit', () => { const commit = 'a'.repeat(40) assert.equal(requireExactCommit(`${commit}\n`, 'commit'), commit) assert.throws(() => requireExactCommit('a'.repeat(39), 'commit'), /40-character/) assert.throws(() => requireExactCommit('A'.repeat(40), 'commit'), /40-character/) }) test('entrypoint validates only user arguments and still rejects unknown flags', () => { const fixture = mkdtempSync(path.join(os.tmpdir(), 'gx-safe-migrator-entry-')) try { const scripts = path.join(fixture, 'scripts') mkdirSync(scripts) cpSync(new URL('./safe-production-migrate.mjs', import.meta.url), path.join(scripts, 'safe-production-migrate.mjs')) const commit = 'f'.repeat(40) writeFileSync(path.join(fixture, 'DEPLOYMENT_COMMIT'), `${commit}\n`) // Node 24 会把内建 test runner 的子进程标记继续传给 spawn;迁移入口必须按普通脚本执行。 const childEnv = Object.fromEntries( Object.entries(process.env).filter(([name]) => !name.startsWith('NODE_TEST_') && name !== 'NODE_V8_COVERAGE'), ) const run = (args) => spawnSync( process.execPath, [path.join(scripts, 'safe-production-migrate.mjs'), ...args], { cwd: fixture, env: { ...childEnv, EXPECTED_COMMIT_SHA: commit, DATABASE_URL: '' }, encoding: 'utf8', }, ) for (const args of [['--audit'], ['--apply']]) { const result = run(args) assert.notEqual(result.status, 0, `stdout=${result.stdout}\nstderr=${result.stderr}`) assert.match(result.stderr, /DATABASE_URL is required/) assert.doesNotMatch(result.stderr, /Only --audit/) } const unknown = run(['--audit', '--unexpected']) assert.notEqual(unknown.status, 0) assert.match(unknown.stderr, /Only --audit \(default\) or --apply is supported/) } finally { rmSync(fixture, { recursive: true, force: true }) } }) test('uses Prisma-compatible SHA-256 migration checksums', () => { assert.equal( sha256(Buffer.from('SELECT 1;\n')), 'b4e0497804e46e0a0b0b8c31975b062152d551bac49c3c2e80932567b4085dcd', ) }) test('normalizes PostgreSQL text defaults without accepting arbitrary values', () => { assert.equal(normalizeColumnDefault("'/brand/header-logo.png'::text"), '/brand/header-logo.png') assert.equal(normalizeColumnDefault("'/brand/login-logo.png'::character varying"), '/brand/login-logo.png') }) test('accepts only a plain non-unique, non-partial btree with exact ordered columns', () => { assert.doesNotThrow(() => assertExactBtreeIndex( 'CREATE INDEX materials_transcode_lease_idx ON public.materials USING btree (transcode_lease_expires_at, transcode_lease_token)', 'materials', ['transcode_lease_expires_at', 'transcode_lease_token'], )) assert.throws(() => assertExactBtreeIndex( 'CREATE UNIQUE INDEX materials_transcode_lease_idx ON public.materials USING btree (transcode_lease_expires_at, transcode_lease_token)', 'materials', ['transcode_lease_expires_at', 'transcode_lease_token'], ), /non-unique/) assert.throws(() => assertExactBtreeIndex( 'CREATE INDEX materials_transcode_lease_idx ON public.materials USING hash (transcode_lease_expires_at, transcode_lease_token)', 'materials', ['transcode_lease_expires_at', 'transcode_lease_token'], ), /columns or method/) assert.throws(() => assertExactBtreeIndex( 'CREATE INDEX materials_transcode_lease_idx ON public.materials USING btree (transcode_lease_expires_at, transcode_lease_token) WHERE transcode_lease_token IS NOT NULL', 'materials', ['transcode_lease_expires_at', 'transcode_lease_token'], ), /non-partial/) }) test('checks configured and actual database ports independently', () => { const names = [ 'EXPECTED_DATABASE_HOST', 'EXPECTED_DATABASE_PORT', 'EXPECTED_DATABASE_NAME', 'EXPECTED_DATABASE_USER', 'EXPECTED_DATABASE_SERVER_ADDRESS', 'EXPECTED_DATABASE_SERVER_PORT', ] const previous = Object.fromEntries(names.map((name) => [name, process.env[name]])) Object.assign(process.env, { EXPECTED_DATABASE_HOST: 'postgres.service', EXPECTED_DATABASE_PORT: '5433', EXPECTED_DATABASE_NAME: 'materials', EXPECTED_DATABASE_USER: 'app', EXPECTED_DATABASE_SERVER_ADDRESS: '10.0.0.12', EXPECTED_DATABASE_SERVER_PORT: '6432', }) try { const identity = { database: 'materials', user: 'app', server_address: '10.0.0.12', server_port: '6432' } assert.doesNotThrow(() => assertExpectedDatabaseIdentity( new URL('postgresql://app:secret@postgres.service:5433/materials'), identity, )) process.env.EXPECTED_DATABASE_PORT = '5432' assert.throws(() => assertExpectedDatabaseIdentity( new URL('postgresql://app:secret@postgres.service:5433/materials'), identity, ), /fingerprint/) } finally { for (const name of names) { if (previous[name] === undefined) delete process.env[name] else process.env[name] = previous[name] } } }) test('backup receipt binds commit, database, artifact and fresh restore validation', () => { const commit = 'b'.repeat(40) const database = { configuredHost: 'postgres.service', configuredPort: '5432', configuredDatabase: 'materials', configuredUser: 'app', serverAddress: '10.0.0.12', serverPort: '5432', } const isolatedDatabase = { configuredHost: 'restore.service', configuredPort: '5433', configuredDatabase: 'gongxiangp2_restore_fixture01', configuredUser: 'restore_operator', serverAddress: '10.20.0.20', serverPort: '5433', } const boundedSummary = { schemaVersion: 1, sampleLimit: 32, materials: { count: '999', sampleSha256InputMd5: 'a'.repeat(32) }, users: { count: '12', sampleSha256InputMd5: 'b'.repeat(32) }, systemSettings: { count: '1', sampleSha256InputMd5: 'c'.repeat(32) }, prismaMigrations: { count: '24', sampleSha256InputMd5: 'd'.repeat(32) }, productPackageUploadJobs: { count: '7', sampleSha256InputMd5: 'e'.repeat(32) }, } const summaryHash = verifiedBackupSummaryHash(boundedSummary) const receipt = { schemaVersion: 1, generatedBy: 'scripts/create-verified-production-backup.mjs', sourceCommit: commit, database, backup: { format: 'pg_dump-custom', startedAt: '2026-08-12T01:55:00.000Z', completedAt: '2026-08-12T02:00:00.000Z', artifactPath: '/backup/materials.dump', artifactSize: 1234, artifactSha256: 'c'.repeat(64), }, restoreValidation: { status: 'passed', startedAt: '2026-08-12T02:01:00.000Z', completedAt: '2026-08-12T02:10:00.000Z', artifactSha256: 'c'.repeat(64), databaseFingerprint: databaseFingerprintHash(database), isolatedDatabase, isolatedDatabaseFingerprint: databaseFingerprintHash(isolatedDatabase), verificationSummary: { schemaVersion: 1, maintenanceWindowConfirmation: 'NO_WRITES_DURING_SNAPSHOT', statementTimeoutMs: 30_000, sampleLimit: 32, sourceBefore: boundedSummary, sourceAfter: boundedSummary, restored: boundedSummary, sourceSummarySha256: summaryHash, restoredSummarySha256: summaryHash, }, }, } const context = { expectedCommit: commit, database, maxAgeMinutes: 240 } assert.doesNotThrow(() => assertBackupReceiptDocument( receipt, context, new Date('2026-08-12T03:00:00.000Z'), )) assert.throws(() => assertBackupReceiptDocument( { ...receipt, sourceCommit: 'd'.repeat(40) }, context, new Date('2026-08-12T03:00:00.000Z'), ), /sourceCommit/) assert.throws(() => assertBackupReceiptDocument( receipt, context, new Date('2026-08-13T03:00:00.000Z'), ), /stale/) assert.throws(() => assertBackupReceiptDocument( { ...receipt, restoreValidation: { ...receipt.restoreValidation, status: 'not-tested' } }, context, new Date('2026-08-12T03:00:00.000Z'), ), /restore validation/) assert.throws(() => assertBackupReceiptDocument( { ...receipt, generatedBy: undefined }, context, new Date('2026-08-12T03:00:00.000Z'), ), /machine-generated/) assert.throws(() => assertBackupReceiptDocument( { ...receipt, restoreValidation: { ...receipt.restoreValidation, verificationSummary: { ...receipt.restoreValidation.verificationSummary, restored: { ...boundedSummary, materials: { ...boundedSummary.materials, count: '1000' }, }, }, }, }, context, new Date('2026-08-12T03:00:00.000Z'), ), /summaries do not match/) }) test('active uploadId idempotency index requires exact unique btree column and predicate', () => { const index = { tablename: 'materials', indexname: 'materials_uploadId_key', indisunique: true, indisprimary: false, indisvalid: true, indisready: true, access_method: 'btree', columns: ['"uploadId"'], predicate: '(("uploadId" IS NOT NULL) AND ("deletedAt" IS NULL))', } assert.doesNotThrow(() => assertExactActiveUploadIdIndex(index)) assert.throws( () => assertExactActiveUploadIdIndex({ ...index, indisunique: false }), /active-only uploadId unique btree/, ) assert.throws( () => assertExactActiveUploadIdIndex({ ...index, predicate: '("uploadId" IS NOT NULL)' }), /active-only uploadId unique btree/, ) assert.throws( () => assertExactActiveUploadIdIndex({ ...index, columns: ['lower("uploadId")'] }), /active-only uploadId unique btree/, ) }) test('SOP extracts the exact receipt basename and hash from a real helper result', async () => { const fixture = mkdtempSync(path.join(os.tmpdir(), 'gx-backup-sop-extract-')) try { const backupDir = path.join(fixture, 'batch') mkdirSync(backupDir) const dumpName = 'gongxiangp2-fixture.dump' const receiptName = 'gongxiangp2-fixture.receipt.json' const dumpBytes = Buffer.from('PGDMP-streamed-sop-fixture') const dumpHash = createHash('sha256').update(dumpBytes).digest('hex') const receipt = { backup: { artifactPath: `/backup/${dumpName}`, artifactSize: dumpBytes.length, artifactSha256: dumpHash, }, } const receiptBytes = Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`) const receiptHash = createHash('sha256').update(receiptBytes).digest('hex') writeFileSync(path.join(backupDir, dumpName), dumpBytes) writeFileSync(path.join(backupDir, receiptName), receiptBytes) const result = { artifactPath: `/backup/${dumpName}`, artifactSize: dumpBytes.length, artifactSha256: dumpHash, receiptPath: `/backup/${receiptName}`, receiptSize: receiptBytes.length, receiptSha256: receiptHash, } const resultPath = path.join(fixture, 'verified-backup-result.txt') writeFileSync( resultPath, `${JSON.stringify(result, null, 2)}\nProduction backup was created, restored to the isolated server and verified.\n`, ) const sop = (await readFile(new URL('../docs/生产数据库安全增量迁移SOP-2026-08-12.md', import.meta.url), 'utf8')) .replace(/\r\n/g, '\n') const startMarker = 'node --input-type=commonjs - "$HOST_RESULT_FILE" "$HOST_BACKUP_DIR" <<\'NODE\'\n' const start = sop.indexOf(startMarker) assert.ok(start >= 0, 'SOP evidence extraction Node block is missing') const bodyStart = start + startMarker.length const end = sop.indexOf('\nNODE\n', bodyStart) assert.ok(end > bodyStart, 'SOP evidence extraction Node block terminator is missing') const source = sop.slice(bodyStart, end) const extraction = spawnSync( process.execPath, ['--input-type=commonjs', '-', resultPath, backupDir], { input: source, encoding: 'utf8' }, ) assert.equal(extraction.status, 0, extraction.stderr) assert.deepEqual(extraction.stdout.trim().split(/\r?\n/), [receiptName, receiptHash]) } finally { rmSync(fixture, { recursive: true, force: true }) } }) test('target evidence rejects nullable logo columns and structurally wrong indexes', () => { const migrations = new Map([ ['20260811_add_video_transcode_lease', { checksum: 'a' }], ['20260811_update_brand_logo_defaults', { checksum: 'b' }], ['20260812_add_product_package_upload_job_lease', { checksum: 'c' }], ['20260812_align_material_upload_idempotency_index', { checksum: 'd' }], ['20260813_add_material_custody_department', { checksum: 'e' }], ['20260813_add_organization_units_and_employment_state', { checksum: 'f' }], ['20260813_add_user_employee_binding', { checksum: 'g' }], ['20260817_add_material_is_incomplete_shell', { checksum: 'h' }], ['20260820_add_ding_workflow_snapshots', { checksum: 'i' }], ['20260824_expand_ding_workflow_insights', { checksum: 'j' }], ['20260826_add_ding_workflow_demand_type', { checksum: 'k' }], ['20260827_add_ding_workflow_detail_fields', { checksum: 'l' }], ['20260826_add_pack_video_early_access', { checksum: 'm' }], ['20260828_add_pack_item_can_edit_assets', { checksum: 'n' }], ['20260828_add_task_detail_change_approvals', { checksum: 'o' }], ['20260829_add_recoverable_api_key_secret', { checksum: 'p' }], ['20260831_add_private_task_detail', { checksum: 'q' }], ['20260902_add_employee_roster_history', { checksum: 'r' }], ['20260903_add_work_review_brand', { checksum: 's' }], ['20260908_add_brand_knowledge', { checksum: 't' }], ['20260909_expand_brand_knowledge_upload', { checksum: 'u' }], ['20260914_add_brand_knowledge_engagement', { checksum: 'v' }], ['20260914_expand_brand_knowledge_download_count', { checksum: 'w' }], ]) const evidence = { migrationRows: [ { migration_name: '20260811_add_video_transcode_lease', checksum: 'a', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260811_update_brand_logo_defaults', checksum: 'b', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260812_add_product_package_upload_job_lease', checksum: 'c', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260812_align_material_upload_idempotency_index', checksum: 'd', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260813_add_material_custody_department', checksum: 'e', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260813_add_organization_units_and_employment_state', checksum: 'f', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260813_add_user_employee_binding', checksum: 'g', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260817_add_material_is_incomplete_shell', checksum: 'h', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260820_add_ding_workflow_snapshots', checksum: 'i', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260824_expand_ding_workflow_insights', checksum: 'j', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260826_add_ding_workflow_demand_type', checksum: 'k', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260827_add_ding_workflow_detail_fields', checksum: 'l', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260826_add_pack_video_early_access', checksum: 'm', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260828_add_pack_item_can_edit_assets', checksum: 'n', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260828_add_task_detail_change_approvals', checksum: 'o', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260829_add_recoverable_api_key_secret', checksum: 'p', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260831_add_private_task_detail', checksum: 'q', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260902_add_employee_roster_history', checksum: 'r', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260903_add_work_review_brand', checksum: 's', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260908_add_brand_knowledge', checksum: 't', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260909_expand_brand_knowledge_upload', checksum: 'u', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260914_add_brand_knowledge_engagement', checksum: 'v', finished_at: 'now', rolled_back_at: null }, { migration_name: '20260914_expand_brand_knowledge_download_count', checksum: 'w', finished_at: 'now', rolled_back_at: null }, ], columns: [ { table_name: 'materials', column_name: 'transcode_lease_token', data_type: 'text', is_nullable: 'YES' }, { table_name: 'materials', column_name: 'transcode_lease_expires_at', data_type: 'timestamp without time zone', is_nullable: 'YES' }, { table_name: 'materials', column_name: 'uploadId', data_type: 'text', is_nullable: 'YES' }, { table_name: 'materials', column_name: 'task_detail_demand_type', data_type: 'text', is_nullable: 'YES', column_default: null }, { table_name: 'materials', column_name: 'task_detail_private', data_type: 'boolean', is_nullable: 'NO', column_default: 'false' }, { table_name: 'material_permission_pack_items', column_name: 'can_access_private', data_type: 'boolean', is_nullable: 'NO', column_default: 'false' }, { table_name: 'api_keys', column_name: 'secret_ciphertext', data_type: 'text', is_nullable: 'YES' }, { table_name: 'api_keys', column_name: 'secret_nonce', data_type: 'text', is_nullable: 'YES' }, { table_name: 'api_keys', column_name: 'secret_auth_tag', data_type: 'text', is_nullable: 'YES' }, { table_name: 'api_key_reveal_audits', column_name: 'id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'api_key_reveal_audits', column_name: 'api_key_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'api_key_reveal_audits', column_name: 'viewer_user_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'api_key_reveal_audits', column_name: 'created_at', data_type: 'timestamp without time zone', is_nullable: 'NO' }, { table_name: 'materials', column_name: 'custody_department_id', data_type: 'text', is_nullable: 'YES' }, { table_name: 'materials', column_name: 'custody_at', data_type: 'timestamp without time zone', is_nullable: 'YES' }, { table_name: 'materials', column_name: 'custody_reason', data_type: 'text', is_nullable: 'YES' }, { table_name: 'materials', column_name: 'is_incomplete_shell', data_type: 'boolean', is_nullable: 'NO' }, { table_name: 'product_package_upload_jobs', column_name: 'worker_id', data_type: 'text', is_nullable: 'YES' }, { table_name: 'product_package_upload_jobs', column_name: 'lease_token', data_type: 'text', is_nullable: 'YES' }, { table_name: 'product_package_upload_jobs', column_name: 'lease_expires_at', data_type: 'timestamp without time zone', is_nullable: 'YES' }, { table_name: 'organization_units', column_name: 'id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'organization_units', column_name: 'source_dept_id', data_type: 'integer', is_nullable: 'NO' }, { table_name: 'organization_units', column_name: 'parent_id', data_type: 'text', is_nullable: 'YES' }, { table_name: 'organization_units', column_name: 'path', data_type: 'text', is_nullable: 'NO' }, { table_name: 'organization_units', column_name: 'level', data_type: 'integer', is_nullable: 'NO' }, { table_name: 'organization_units', column_name: 'is_active', data_type: 'boolean', is_nullable: 'NO' }, { table_name: 'performance_survey_employee_profiles', column_name: 'employment_state', data_type: 'text', is_nullable: 'YES' }, { table_name: 'performance_survey_employee_profiles', column_name: 'resigned_at', data_type: 'timestamp without time zone', is_nullable: 'YES' }, { table_name: 'performance_survey_employee_profiles', column_name: 'handover_dingtalk_user_id', data_type: 'text', is_nullable: 'YES' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'captured_at', data_type: 'timestamp without time zone', is_nullable: 'NO' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'profile_count', data_type: 'integer', is_nullable: 'NO' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'employee_profile_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'dingtalk_user_id', data_type: 'text', is_nullable: 'YES' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'name', data_type: 'text', is_nullable: 'NO' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'department_name', data_type: 'text', is_nullable: 'YES' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'department_path', data_type: 'text', is_nullable: 'YES' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'employment_state', data_type: 'text', is_nullable: 'YES' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'is_brand_center', data_type: 'boolean', is_nullable: 'NO' }, { table_name: 'performance_survey_employee_roster_snapshots', column_name: 'created_at', data_type: 'timestamp without time zone', is_nullable: 'NO' }, { table_name: 'user_employee_bindings', column_name: 'id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'user_employee_bindings', column_name: 'user_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'user_employee_bindings', column_name: 'employee_profile_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'user_employee_bindings', column_name: 'binding_source', data_type: 'text', is_nullable: 'NO' }, { table_name: 'user_employee_bindings', column_name: 'status', data_type: 'text', is_nullable: 'NO' }, { table_name: 'ding_workflow_snapshots', column_name: 'process_instance_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'ding_workflow_snapshots', column_name: 'status', data_type: 'text', is_nullable: 'NO' }, { table_name: 'ding_workflow_snapshots', column_name: 'is_ongoing', data_type: 'boolean', is_nullable: 'NO' }, { table_name: 'ding_workflow_snapshots', column_name: 'source_updated_at_raw', data_type: 'text', is_nullable: 'YES' }, { table_name: 'ding_workflow_snapshots', column_name: 'scheme_field_count', data_type: 'integer', is_nullable: 'NO' }, { table_name: 'ding_workflow_snapshots', column_name: 'task_count', data_type: 'integer', is_nullable: 'NO' }, { table_name: 'ding_workflow_snapshots', column_name: 'active_task_count', data_type: 'integer', is_nullable: 'NO' }, { table_name: 'ding_workflow_snapshots', column_name: 'synced_at', data_type: 'timestamp without time zone', is_nullable: 'NO' }, { table_name: 'ding_workflow_snapshots', column_name: 'brand', data_type: 'text', is_nullable: 'YES' }, { table_name: 'ding_workflow_snapshots', column_name: 'demand_type', data_type: 'text', is_nullable: 'YES' }, { table_name: 'ding_workflow_snapshots', column_name: 'requester_name', data_type: 'text', is_nullable: 'YES' }, { table_name: 'ding_workflow_snapshots', column_name: 'requester_department', data_type: 'text', is_nullable: 'YES' }, { table_name: 'ding_workflow_snapshots', column_name: 'project_type', data_type: 'text', is_nullable: 'YES' }, { table_name: 'ding_workflow_snapshots', column_name: 'active_tasks', data_type: 'jsonb', is_nullable: 'YES' }, { table_name: 'ding_workflow_snapshots', column_name: 'form_fields', data_type: 'jsonb', is_nullable: 'YES' }, { table_name: 'ding_workflow_sync_states', column_name: 'id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'ding_workflow_sync_states', column_name: 'status', data_type: 'text', is_nullable: 'NO' }, { table_name: 'ding_workflow_sync_states', column_name: 'source_cursor_raw', data_type: 'text', is_nullable: 'YES' }, { table_name: 'ding_workflow_sync_states', column_name: 'last_completed_at', data_type: 'timestamp without time zone', is_nullable: 'YES' }, { table_name: 'ding_workflow_sync_states', column_name: 'lease_owner', data_type: 'text', is_nullable: 'YES' }, { table_name: 'ding_workflow_sync_states', column_name: 'lease_expires_at', data_type: 'timestamp without time zone', is_nullable: 'YES' }, { table_name: 'ding_workflow_sync_states', column_name: 'current_ongoing_count', data_type: 'integer', is_nullable: 'NO' }, { table_name: 'task_detail_change_requests', column_name: 'id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_requests', column_name: 'material_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_requests', column_name: 'requester_user_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_requests', column_name: 'manager_dingtalk_user_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_requests', column_name: 'status', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_requests', column_name: 'operations', data_type: 'jsonb', is_nullable: 'NO' }, { table_name: 'task_detail_change_requests', column_name: 'base_material_updated_at', data_type: 'timestamp without time zone', is_nullable: 'NO' }, { table_name: 'task_detail_change_requests', column_name: 'expires_at', data_type: 'timestamp without time zone', is_nullable: 'NO' }, { table_name: 'task_detail_change_notifications', column_name: 'id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_notifications', column_name: 'request_id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_notifications', column_name: 'event_type', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_notifications', column_name: 'business_key', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_notifications', column_name: 'status', data_type: 'text', is_nullable: 'NO' }, { table_name: 'task_detail_change_notifications', column_name: 'next_retry_at', data_type: 'timestamp without time zone', is_nullable: 'YES' }, { table_name: 'brand_knowledge', column_name: 'id', data_type: 'text', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'title', data_type: 'text', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'knowledge_type', data_type: 'text', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'brand', data_type: 'text', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'product_category_name', data_type: 'text', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'file_name', data_type: 'text', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'file_path', data_type: 'text', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'mime_type', data_type: 'text', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'file_size', data_type: 'bigint', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'download_count', data_type: 'integer', is_nullable: 'NO', column_default: '0' }, { table_name: 'brand_knowledge', column_name: 'preview_image_name', data_type: 'text', is_nullable: 'YES' }, { table_name: 'brand_knowledge', column_name: 'preview_image_path', data_type: 'text', is_nullable: 'YES' }, { table_name: 'brand_knowledge', column_name: 'preview_image_mime_type', data_type: 'text', is_nullable: 'YES' }, { table_name: 'brand_knowledge', column_name: 'preview_image_size', data_type: 'bigint', is_nullable: 'YES' }, { table_name: 'brand_knowledge', column_name: 'uploaded_by', data_type: 'text', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'created_at', data_type: 'timestamp without time zone', is_nullable: 'NO' }, { table_name: 'brand_knowledge', column_name: 'updated_at', data_type: 'timestamp without time zone', is_nullable: 'NO' }, ...['brand_knowledge_likes', 'brand_knowledge_favorites'].flatMap((table_name) => [ { table_name, column_name: 'id', data_type: 'text', is_nullable: 'NO' }, { table_name, column_name: 'knowledge_id', data_type: 'text', is_nullable: 'NO' }, { table_name, column_name: 'user_id', data_type: 'text', is_nullable: 'NO' }, { table_name, column_name: 'created_at', data_type: 'timestamp without time zone', is_nullable: 'NO' }, ]), { table_name: 'system_settings', column_name: 'headerLogoUrl', data_type: 'text', is_nullable: 'NO', column_default: "'/brand/header-logo.png'::text" }, { table_name: 'system_settings', column_name: 'loginLogoUrl', data_type: 'text', is_nullable: 'NO', column_default: "'/brand/login-logo.png'::text" }, { table_name: 'system_settings', column_name: 'logoUrl', data_type: 'text', is_nullable: 'NO', column_default: "'/brand/header-logo.png'::text" }, ], indexes: [ { indexname: 'materials_transcode_lease_idx', indexdef: 'CREATE INDEX materials_transcode_lease_idx ON public.materials USING btree (transcode_lease_expires_at, transcode_lease_token)' }, { tablename: 'materials', indexname: 'materials_uploadId_key', indexdef: 'CREATE UNIQUE INDEX "materials_uploadId_key" ON public.materials USING btree ("uploadId") WHERE (("uploadId" IS NOT NULL) AND ("deletedAt" IS NULL))', indisunique: true, indisprimary: false, indisvalid: true, indisready: true, access_method: 'btree', columns: ['"uploadId"'], predicate: '(("uploadId" IS NOT NULL) AND ("deletedAt" IS NULL))', }, { indexname: 'product_package_upload_jobs_lease_idx', indexdef: 'CREATE INDEX product_package_upload_jobs_lease_idx ON public.product_package_upload_jobs USING btree (status, lease_expires_at)' }, { indexname: 'materials_is_incomplete_shell_idx', indexdef: 'CREATE INDEX materials_is_incomplete_shell_idx ON public.materials USING btree (is_incomplete_shell)' }, { indexname: 'ding_workflow_snapshots_ongoing_updated_idx', indexdef: 'CREATE INDEX ding_workflow_snapshots_ongoing_updated_idx ON public.ding_workflow_snapshots USING btree (is_ongoing, source_updated_at_raw)' }, { indexname: 'ding_workflow_snapshots_brand_idx', indexdef: 'CREATE INDEX ding_workflow_snapshots_brand_idx ON public.ding_workflow_snapshots USING btree (brand)' }, { indexname: 'ding_workflow_snapshots_demand_type_idx', indexdef: 'CREATE INDEX ding_workflow_snapshots_demand_type_idx ON public.ding_workflow_snapshots USING btree (demand_type)' }, { indexname: 'ding_workflow_snapshots_requester_department_idx', indexdef: 'CREATE INDEX ding_workflow_snapshots_requester_department_idx ON public.ding_workflow_snapshots USING btree (requester_department)' }, { indexname: 'ding_workflow_sync_states_lease_idx', indexdef: 'CREATE INDEX ding_workflow_sync_states_lease_idx ON public.ding_workflow_sync_states USING btree (lease_expires_at)' }, { indexname: 'task_detail_change_requests_status_expires_idx', indexdef: 'CREATE INDEX task_detail_change_requests_status_expires_idx ON public.task_detail_change_requests USING btree (status, expires_at)' }, { indexname: 'task_detail_change_requests_status_updated_idx', indexdef: 'CREATE INDEX task_detail_change_requests_status_updated_idx ON public.task_detail_change_requests USING btree (status, updated_at)' }, { indexname: 'task_detail_change_notifications_status_retry_idx', indexdef: 'CREATE INDEX task_detail_change_notifications_status_retry_idx ON public.task_detail_change_notifications USING btree (status, next_retry_at)' }, { indexname: 'api_key_reveal_audits_api_key_id_created_at_idx', indexdef: 'CREATE INDEX api_key_reveal_audits_api_key_id_created_at_idx ON public.api_key_reveal_audits USING btree (api_key_id, created_at)' }, { indexname: 'api_key_reveal_audits_viewer_user_id_created_at_idx', indexdef: 'CREATE INDEX api_key_reveal_audits_viewer_user_id_created_at_idx ON public.api_key_reveal_audits USING btree (viewer_user_id, created_at)' }, { indexname: 'brand_knowledge_product_category_name_idx', indexdef: 'CREATE INDEX brand_knowledge_product_category_name_idx ON public.brand_knowledge USING btree (product_category_name)' }, ...['brand_knowledge_likes', 'brand_knowledge_favorites'].flatMap((table) => [ { tablename: table, indexname: `${table}_knowledge_id_user_id_key`, indexdef: `CREATE UNIQUE INDEX ${table}_knowledge_id_user_id_key ON public.${table} USING btree (knowledge_id, user_id)`, indisunique: true, indisprimary: false, indisvalid: true, indisready: true, access_method: 'btree', columns: ['knowledge_id', 'user_id'], predicate: '', }, { indexname: `${table}_knowledge_id_idx`, indexdef: `CREATE INDEX ${table}_knowledge_id_idx ON public.${table} USING btree (knowledge_id)` }, { indexname: `${table}_user_id_idx`, indexdef: `CREATE INDEX ${table}_user_id_idx ON public.${table} USING btree (user_id)` }, ]), ], constraints: [ { tablename: 'materials', constraint_name: 'materials_private_optimization_check', contype: 'c', convalidated: true, columns: ['task_detail_private', 'materialType', 'task_detail_demand_type'], expression: `((NOT task_detail_private) OR (((materialType = '任务-详情'::text) AND (task_detail_demand_type = '优化'::text)) IS TRUE))`, }, ...['brand_knowledge_likes', 'brand_knowledge_favorites'].flatMap((table) => [ { tablename: table, constraint_name: `${table}_knowledge_id_fkey`, contype: 'f', convalidated: true, columns: ['knowledge_id'], referenced_table: 'brand_knowledge', referenced_columns: ['id'], }, { tablename: table, constraint_name: `${table}_user_id_fkey`, contype: 'f', convalidated: true, columns: ['user_id'], referenced_table: 'users', referenced_columns: ['id'], }, ]), ], brandRows: [ { id: 'local', headerLogoUrl: '/brand/header-logo.png', loginLogoUrl: '/brand/login-logo.png' }, { id: 'custom', headerLogoUrl: 'https://s3.jiansun.vip/gongxiangpan/header.png', loginLogoUrl: 'https://s3.jiansun.vip/gongxiangpan/login.png' }, ], } assert.doesNotThrow(() => assertTargetMigrationEvidence(migrations, evidence)) const nullableLogo = { ...evidence, columns: evidence.columns.map((column) => column.column_name === 'headerLogoUrl' ? { ...column, is_nullable: 'YES' } : column), } assert.throws(() => assertTargetMigrationEvidence(migrations, nullableLogo), /column mismatch/) const uniqueIndex = { ...evidence, indexes: evidence.indexes.map((index) => index.indexname === 'materials_transcode_lease_idx' ? { ...index, indexdef: index.indexdef.replace('CREATE INDEX', 'CREATE UNIQUE INDEX') } : index), } assert.throws(() => assertTargetMigrationEvidence(migrations, uniqueIndex), /non-unique/) const wrongUploadIdPredicate = { ...evidence, indexes: evidence.indexes.map((index) => index.indexname === 'materials_uploadId_key' ? { ...index, predicate: '("uploadId" IS NOT NULL)' } : index), } assert.throws( () => assertTargetMigrationEvidence(migrations, wrongUploadIdPredicate), /active-only uploadId unique btree/, ) const missingOrganizationColumn = { ...evidence, columns: evidence.columns.filter((column) => !( column.table_name === 'organization_units' && column.column_name === 'source_dept_id' )), } assert.throws( () => assertTargetMigrationEvidence(migrations, missingOrganizationColumn), /organization_units\.source_dept_id/, ) const missingIncompleteShellColumn = { ...evidence, columns: evidence.columns.filter((column) => !( column.table_name === 'materials' && column.column_name === 'is_incomplete_shell' )), } assert.throws( () => assertTargetMigrationEvidence(migrations, missingIncompleteShellColumn), /materials\.is_incomplete_shell/, ) const wrongTaskDetailPrivateDefault = { ...evidence, columns: evidence.columns.map((column) => column.table_name === 'materials' && column.column_name === 'task_detail_private' ? { ...column, column_default: 'true' } : column), } assert.throws( () => assertTargetMigrationEvidence(migrations, wrongTaskDetailPrivateDefault), /materials\.task_detail_private/, ) const wrongDownloadCountDefault = { ...evidence, columns: evidence.columns.map((column) => column.table_name === 'brand_knowledge' && column.column_name === 'download_count' ? { ...column, column_default: null } : column), } assert.throws( () => assertTargetMigrationEvidence(migrations, wrongDownloadCountDefault), /brand_knowledge\.download_count/, ) const unexpectedTaskDetailDemandDefault = { ...evidence, columns: evidence.columns.map((column) => column.table_name === 'materials' && column.column_name === 'task_detail_demand_type' ? { ...column, column_default: "'优化'::text" } : column), } assert.throws( () => assertTargetMigrationEvidence(migrations, unexpectedTaskDetailDemandDefault), /materials\.task_detail_demand_type/, ) const missingPrivateAccessColumn = { ...evidence, columns: evidence.columns.filter((column) => !( column.table_name === 'material_permission_pack_items' && column.column_name === 'can_access_private' )), } assert.throws( () => assertTargetMigrationEvidence(migrations, missingPrivateAccessColumn), /material_permission_pack_items\.can_access_private/, ) const missingDemandTypeColumn = { ...evidence, columns: evidence.columns.filter((column) => !( column.table_name === 'ding_workflow_snapshots' && column.column_name === 'demand_type' )), } assert.throws( () => assertTargetMigrationEvidence(migrations, missingDemandTypeColumn), /ding_workflow_snapshots\.demand_type/, ) const missingRecoverableApiKeyColumn = { ...evidence, columns: evidence.columns.filter((column) => !( column.table_name === 'api_keys' && column.column_name === 'secret_ciphertext' )), } assert.throws( () => assertTargetMigrationEvidence(migrations, missingRecoverableApiKeyColumn), /api_keys\.secret_ciphertext/, ) const missingFormFieldsColumn = { ...evidence, columns: evidence.columns.filter((column) => !( column.table_name === 'ding_workflow_snapshots' && column.column_name === 'form_fields' )), } assert.throws( () => assertTargetMigrationEvidence(migrations, missingFormFieldsColumn), /ding_workflow_snapshots\.form_fields/, ) const missingRevealAuditIndex = { ...evidence, indexes: evidence.indexes.filter((index) => index.indexname !== 'api_key_reveal_audits_api_key_id_created_at_idx'), } assert.throws( () => assertTargetMigrationEvidence(migrations, missingRevealAuditIndex), /api_key_reveal_audits/, ) const wrongPrivateConstraint = { ...evidence, constraints: evidence.constraints.map((constraint) => constraint.constraint_name === 'materials_private_optimization_check' ? { ...constraint, expression: `((NOT task_detail_private) OR ((materialType = '任务-详情'::text) AND (task_detail_demand_type = '优化'::text)))` } : constraint), } assert.throws( () => assertTargetMigrationEvidence(migrations, wrongPrivateConstraint), /materials_private_optimization_check/, ) const legacyStoredLogo = { ...evidence, brandRows: [{ id: 'legacy', headerLogoUrl: 'https://s3.1080p.hk/jsxiangche/header-logo.png', loginLogoUrl: '/brand/login-logo.png', }], } assert.throws( () => assertTargetMigrationEvidence(migrations, legacyStoredLogo), /Stored brand logo is untrusted/, ) }) test('container supply chain and migrator image are immutable while SOP keeps complete gates', async () => { const [dockerfile, sop, incidentSop, migratorSource, backupSource, uploadIdMigration, dockerIgnore, deploymentSource] = await Promise.all([ readFile(new URL('../Dockerfile', import.meta.url), 'utf8'), readFile(new URL('../docs/生产数据库安全增量迁移SOP-2026-08-12.md', import.meta.url), 'utf8'), readFile(new URL('../docs/视频预览任务中断事故复盘-2026-08-11.md', import.meta.url), 'utf8'), readFile(new URL('./safe-production-migrate.mjs', import.meta.url), 'utf8'), readFile(new URL('./create-verified-production-backup.mjs', import.meta.url), 'utf8'), readFile(new URL('../prisma/migrations/20260812_align_material_upload_idempotency_index/migration.sql', import.meta.url), 'utf8'), readFile(new URL('../.dockerignore', import.meta.url), 'utf8'), readFile(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8'), ]) const migratorStart = dockerfile.indexOf('FROM base AS migrator') const runnerStart = dockerfile.indexOf('FROM base AS runner', migratorStart) const migrator = dockerfile.slice(migratorStart, runnerStart) const depsStart = dockerfile.indexOf('FROM base AS deps') const builderStart = dockerfile.indexOf('FROM base AS builder', depsStart) const deps = dockerfile.slice(depsStart, builderStart) assert.match( dockerfile, /^ARG BASE_IMAGE=node:18-alpine@sha256:8d6421d663b4c28fd3ebc498332f249011d118945588d0a35cb9bc4b8ca09d9e$/m, ) assert.match(deps, /^COPY package\.json package-lock\.json \.\/$/m) assert.match(deps, /npm ci --prefer-offline --no-audit --no-fund --legacy-peer-deps/) assert.match(deps, /npm_config_build_from_source=canvas npm_config_nodedir=\/usr\/local npm ci/) assert.doesNotMatch(deps, /--omit=optional/) assert.match(deps, /const sharp=require\('sharp'\)/) assert.doesNotMatch(deps, /\bnpm install\b|writeFileSync|delete pkg\.optionalDependencies/) assert.match(deps, /lock_sha="\$\(sha256sum package-lock\.json/) assert.match(deps, /test "\$lock_sha" = "\$\(sha256sum package-lock\.json/) assert.ok(migrator.includes('LABEL org.opencontainers.image.revision=$VCS_REF')) assert.match(migrator, /apk add --no-cache openssl postgresql-client/) assert.match(migrator, /command -v openssl >\/dev\/null/) assert.match(migrator, /command -v pg_dump >\/dev\/null/) assert.match(migrator, /command -v pg_restore >\/dev\/null/) assert.match(migrator, /command -v psql >\/dev\/null/) assert.match(migrator, /COPY --from=builder \/app\/scripts\/create-verified-production-backup\.mjs/) assert.match(migrator, /test -x \/app\/node_modules\/prisma\/node_modules\/@prisma\/engines\/schema-engine-linux-musl/) assert.match(migrator, /chown -R node:node \/app\/node_modules\/prisma\/node_modules\/@prisma\/engines/) assert.match(migrator, /chmod -R a\+rX,a-w \/app\/prisma \/app\/scripts \/app\/DEPLOYMENT_COMMIT/) assert.match(migrator, /USER node/) assert.match(sop, /tar -xOf "\$MIGRATOR_ARCHIVE" manifest\.json/) assert.match(sop, /entry\.RepoTags\.includes\(image\)/) assert.match(sop, /\^blobs\\\/sha256\\\/\(\[0-9a-f\]\{64\}\)\$/) const scriptsExcludeIndex = dockerIgnore.lastIndexOf('scripts/**') assert.ok(scriptsExcludeIndex >= 0) assert.ok(dockerIgnore.lastIndexOf('!scripts/safe-production-migrate.mjs') > scriptsExcludeIndex) assert.ok(dockerIgnore.lastIndexOf('!scripts/create-verified-production-backup.mjs') > scriptsExcludeIndex) assert.match(dockerfile, /20260812_align_material_upload_idempotency_index/) assert.match(migratorSource, /'20260812_align_material_upload_idempotency_index'/) for (const migrationName of [ '20260813_add_material_custody_department', '20260813_add_organization_units_and_employment_state', '20260813_add_user_employee_binding', '20260817_add_material_is_incomplete_shell', '20260820_add_ding_workflow_snapshots', '20260824_expand_ding_workflow_insights', '20260826_add_ding_workflow_demand_type', '20260827_add_ding_workflow_detail_fields', '20260826_add_pack_video_early_access', '20260828_add_pack_item_can_edit_assets', '20260828_add_task_detail_change_approvals', '20260831_add_private_task_detail', '20260902_add_employee_roster_history', '20260903_add_work_review_brand', '20260908_add_brand_knowledge', '20260909_expand_brand_knowledge_upload', '20260914_add_brand_knowledge_engagement', '20260914_expand_brand_knowledge_download_count', ]) { assert.match(dockerfile, new RegExp(migrationName)) assert.match(migratorSource, new RegExp(`'${migrationName}'`)) assert.ok(sop.includes('`' + migrationName + '`')) } assert.match(dockerfile, /20260829_add_recoverable_api_key_secret/) assert.match(migratorSource, /'20260829_add_recoverable_api_key_secret'/) assert.match(deploymentSource, /'20260829_add_recoverable_api_key_secret'/) assert.ok(sop.includes('`20260829_add_recoverable_api_key_secret`')) assert.match(deploymentSource, /'20260831_add_private_task_detail'/) assert.ok(sop.includes('`20260831_add_private_task_detail`')) assert.match(deploymentSource, /'20260902_add_employee_roster_history'/) assert.ok(sop.includes('`20260902_add_employee_roster_history`')) assert.match(deploymentSource, /'20260903_add_work_review_brand'/) assert.ok(sop.includes('`20260903_add_work_review_brand`')) assert.match(deploymentSource, /'20260908_add_brand_knowledge'/) assert.ok(sop.includes('`20260908_add_brand_knowledge`')) assert.match(deploymentSource, /'20260909_expand_brand_knowledge_upload'/) assert.ok(sop.includes('`20260909_expand_brand_knowledge_upload`')) assert.match(deploymentSource, /'20260914_add_brand_knowledge_engagement'/) assert.ok(sop.includes('`20260914_add_brand_knowledge_engagement`')) assert.match(deploymentSource, /'20260914_expand_brand_knowledge_download_count'/) assert.ok(sop.includes('`20260914_expand_brand_knowledge_download_count`')) const indexesQueryStart = migratorSource.indexOf('const indexes = await prisma.$queryRawUnsafe(') const indexesQueryEnd = migratorSource.indexOf('const constraints = await prisma.$queryRawUnsafe(', indexesQueryStart) assert.ok(indexesQueryStart >= 0) assert.ok(indexesQueryEnd > indexesQueryStart) assert.match( migratorSource.slice(indexesQueryStart, indexesQueryEnd), /'brand_knowledge_product_category_name_idx'/, ) assert.match( migratorSource.slice(indexesQueryStart, indexesQueryEnd), /'brand_knowledge_likes_knowledge_id_user_id_key'/, ) assert.match(sop, /materials_private_optimization_check/) assert.match(sop, /task_detail_private/) assert.match(sop, /can_access_private/) assert.match(backupSource, /'--format=custom'/) assert.match(backupSource, /'--serializable-deferrable'/) assert.match(backupSource, /\.dump\.partial-/) assert.match(backupSource, /ALLOW_VERIFIED_BACKUP/) assert.match(backupSource, /ALLOW_ISOLATED_DATABASE_RESET/) assert.match(uploadIdMigration, /^BEGIN;$/m) assert.match(uploadIdMigration, /GROUP BY "uploadId"\s+HAVING COUNT\(\*\) > 1/) assert.match(uploadIdMigration, /DROP INDEX IF EXISTS "materials_uploadId_key"/) assert.match( uploadIdMigration, /CREATE UNIQUE INDEX "materials_uploadId_key"\s+ON "materials" USING btree \("uploadId"\)\s+WHERE "uploadId" IS NOT NULL AND "deletedAt" IS NULL/, ) assert.doesNotMatch(uploadIdMigration, /\b(?:DELETE|UPDATE|TRUNCATE)\b/i) assert.ok(sop.includes('mktemp -d')) assert.ok(sop.includes('git clone --no-checkout')) assert.ok(!sop.includes('git reset --hard')) assert.ok(!sop.includes('git clean -fdx')) for (const name of [ 'EXPECTED_DATABASE_PORT', 'EXPECTED_DATABASE_SERVER_PORT', 'BACKUP_ARTIFACT_ROOT', 'BACKUP_MAX_AGE_MINUTES', ]) { assert.ok(sop.includes(name), `SOP is missing ${name}`) } assert.ok(sop.includes('install -d -m 0750')) assert.match(sop, /legacy bootstrap `Audit`/) assert.match(sop, /legacy bootstrap `Apply`,外部验收 503/) assert.match(sop, /旧容器已停止但仍保留可回退/) assert.match(sop, /顺序不得改为“先 migration,后 bootstrap”/) assert.match(sop, /CONFIRM_MAINTENANCE_WINDOW=NO_WRITES_DURING_SNAPSHOT/) assert.match(sop, /-SkipDeploy` 只构建并推送 runner,\*\*不会\*\*构建 migrator/) assert.match(sop, /docker save --output "\$MIGRATOR_ARCHIVE"/) assert.match(sop, /StrictHostKeyChecking=yes/) assert.match(sop, /docker load --input "\$archive"/) assert.match(sop, /docker image inspect --format '\{\{\.Id\}\}' "\$image"/) assert.match(sop, /MigrationAuditFile/) assert.match(sop, /MigrationAuditSha256/) assert.match(sop, /RequireMigrationAudit/) assert.match(incidentSop, /migrator 必须按《生产数据库安全增量迁移 SOP/) assert.match(incidentSop, /-MigrationAuditFile/) assert.match(incidentSop, /-MigrationAuditSha256/) assert.match(incidentSop, /-RequireMigrationAudit/) assert.match(sop, /receiptNames\.length !== 1 \|\| dumpNames\.length !== 1 \|\| entries\.length !== 2/) assert.match(sop, /entry\.isFile\(\) && entry\.name\.endsWith\('\.receipt\.json'\)/) assert.match(sop, /entry\.isFile\(\) && entry\.name\.endsWith\('\.dump'\)/) assert.match(sop, /stat\.isSymbolicLink\(\)/) assert.match(sop, /result\.receiptSha256 !== receiptSha256/) assert.match(sop, /receipt\.backup\.artifactSha256 !== result\.artifactSha256/) assert.match(sop, /resultText\.slice\(0, markerIndex\)\.trim\(\)/) assert.doesNotMatch(sop, /lastIndexOf\('\\n\{'/) assert.match(sop, /fs\.createReadStream\(dumpPath\)/) assert.match(sop, /result\.artifactSize !== dumpStat\.size/) assert.doesNotMatch(sop, /fs\.readFileSync\(dumpPath\)/) assert.match(sop, /BACKUP_RECEIPT_BASENAME="\$\{BACKUP_EVIDENCE\[0\]\}"/) assert.match(sop, /BACKUP_RECEIPT_PATH="\/backup\/\$BACKUP_RECEIPT_BASENAME"/) assert.match(sop, /-v "\$HOST_BACKUP_DIR:\/backup:ro"/) assert.doesNotMatch(sop, /BACKUP_RECEIPT_PATH=\/backup\/receipt\.json/) assert.doesNotMatch(sop, /-v \/root\/gongxiangp2-db-backup-receipt:\/backup:ro/) assert.match(sop, /PREFLIGHT_AUDIT_FILE="\$HOST_RESULT_DIR\/preflight-/) assert.match(sop, /MIGRATION_AUDIT_OUTPUT="\/audit\/\$\(basename "\$PREFLIGHT_AUDIT_FILE"\)"/) assert.match(sop, /-v "\$HOST_RESULT_DIR:\/audit"/) assert.match(sop, /test -f "\$PREFLIGHT_AUDIT_FILE" && test ! -L "\$PREFLIGHT_AUDIT_FILE"/) assert.doesNotMatch(sop, /-v \/root\/gongxiangp2-migration-audit:\/audit/) assert.match(sop, /二十三份目标 migration/) assert.match(sop, /二十三条目标 migration/) const applyMigration = migratorSource.indexOf("['migrate', 'deploy'") const postEvidence = migratorSource.indexOf('after = await queryEvidence(verifyClient, localMigrations)', applyMigration) const postIdentity = migratorSource.indexOf('assertExpectedDatabaseIdentity(databaseUrl, after.identity)', postEvidence) const compareIdentity = migratorSource.indexOf('Production database identity changed while migrations were being applied', postIdentity) const writeAudit = migratorSource.indexOf('await writeAuditRecord(record)', compareIdentity) assert.ok(applyMigration > 0) assert.ok(postEvidence > applyMigration) assert.ok(postIdentity > postEvidence) assert.ok(compareIdentity > postIdentity) assert.ok(writeAudit > compareIdentity) })