const test = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const deployPath = path.resolve(__dirname, '../../../deploy.sh') const precheckPath = path.resolve( __dirname, '../../prisma/migrations/20260714_asset_permissions_disposal_dingtalk/precheck.sql' ) const deployScript = fs.readFileSync(deployPath, 'utf8') const precheckSql = fs.readFileSync(precheckPath, 'utf8') test('标准部署在 build/up 前必须通过报废迁移预检', () => { const gateCall = deployScript.lastIndexOf('\nrun_disposal_migration_precheck\n') const buildCall = deployScript.indexOf('"${COMPOSE_CMD[@]}" build') const upCall = deployScript.indexOf('"${COMPOSE_CMD[@]}" up -d --remove-orphans') assert.ok(gateCall > 0, '部署脚本必须调用迁移前预检') assert.ok(buildCall > gateCall, '预检必须早于镜像构建') assert.ok(upCall > gateCall, '预检必须早于容器更新/迁移') assert.match( deployScript, /backend\/prisma\/migrations\/20260714_asset_permissions_disposal_dingtalk\/precheck\.sql/ ) }) test('预检只连接固定资产 MySQL,不在命令行暴露密码', () => { assert.match(deployScript, /MYSQL_CONTAINER="equipment-mysql"/) assert.match(deployScript, /--defaults-extra-file="\$defaults_file"/) assert.doesNotMatch(deployScript, /mysql\s+[^\n]*--password=/) assert.doesNotMatch(deployScript, /shebeiguanli|\/opt\/shebeiguanli/) assert.doesNotMatch( deployScript, /LEGACY_CONTAINERS|record_legacy_container_statuses|verify_legacy_container_statuses/ ) }) test('无法确认或存在未核对报废单时默认阻断,仅允许本次显式确认', () => { assert.match(precheckSql, /SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;/) assert.match(precheckSql, /START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;/) assert.match(precheckSql, /'ACTIVE_DISPOSAL_COUNT'/) assert.match(precheckSql, /'REVIEW_ROW'/) assert.match(precheckSql, /COMMIT;/) assert.match( deployScript, /snapshot_output="\$\([\s\S]*run_equipment_mysql --batch --skip-column-names --raw < "\$DISPOSAL_PRECHECK_SQL"[\s\S]*\)"/ ) assert.match(deployScript, /printf '%s\\n' "\$review_rows" \| sha256sum/) assert.match( deployScript, /CONFIRM_ACTIVE_DISPOSALS_REVIEWED" != "\$CURRENT_DISPOSAL_FINGERPRINT"/ ) assert.match(deployScript, /已默认阻止部署/) }) test('未完成报废资产树存在环或深度达到 100 时禁止 override', () => { const anomalyBlock = deployScript.indexOf('if [ "$tree_anomaly_count" -gt 0 ]') const overrideCheck = deployScript.indexOf( 'if [ "$CONFIRM_ACTIVE_DISPOSALS_REVIEWED" != "$CURRENT_DISPOSAL_FINGERPRINT" ]' ) assert.match(precheckSql, /WHERE `cycle_detected` = 1 OR `depth` >= 100/) assert.ok(anomalyBlock > 0, '必须对资产树异常数量做硬阻断') assert.ok(overrideCheck > anomalyBlock, '资产树异常阻断必须早于指纹 override') assert.match(deployScript, /不允许通过确认指纹绕过/) }) test('人工展示与指纹来自同一次数据库一致性快照', () => { const functionBody = deployScript.slice( deployScript.indexOf('calculate_active_disposal_fingerprint()'), deployScript.indexOf('\nrun_disposal_migration_precheck()') ) const mysqlCalls = functionBody.match(/run_equipment_mysql/g) || [] assert.equal(mysqlCalls.length, 1, '一次指纹计算只能打开一个数据库会话') assert.match(functionBody, /review_rows=.*snapshot_output/) assert.match(functionBody, /printf '%s\\n' "\$review_rows"/) assert.doesNotMatch( deployScript.slice( deployScript.indexOf('run_disposal_migration_precheck()'), deployScript.indexOf('\nrecheck_disposal_migration_precheck()') ), /run_equipment_mysql\s+<\s+"\$DISPOSAL_PRECHECK_SQL"/ ) }) test('同名迁移同时存在完成和未完成记录时必须优先判为 incomplete', () => { const stateQuery = deployScript.slice( deployScript.indexOf('SELECT CASE'), deployScript.indexOf('END\nFROM _prisma_migrations') ) assert.ok( stateQuery.indexOf("THEN 'incomplete'") < stateQuery.indexOf("THEN 'applied'"), '未完成记录必须优先阻断,不能被旧的已完成记录掩盖' ) })