import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import fs from 'node:fs/promises'; import path from 'node:path'; import test from 'node:test'; const ROOT = path.resolve(new URL('..', import.meta.url).pathname); const CANARY_RELEASE = path.join(ROOT, 'scripts', 'release-portal-canary-prod.sh'); test('production release verifies gate report before any 103 connection', async () => { const source = await fs.readFile( path.join(ROOT, 'scripts', 'release-portal-runtime-prod.sh'), 'utf8', ); const gateIndex = source.indexOf('verify-release-gate-report.mjs'); const preflightIndex = source.indexOf('执行 103 只读预检'); const uploadIndex = source.indexOf('上传 Portal runtime 到 103'); assert.ok(gateIndex > 0, 'missing gate verifier'); assert.ok(preflightIndex > gateIndex, '103 preflight must run after gate verification'); assert.ok(uploadIndex > preflightIndex, 'upload must run after preflight'); assert.match(source, /生产发布守门员禁止 --skip-tests/); assert.match(source, /禁止 ALLOW_PORTAL_RELEASE_SCOPE_BYPASS/); }); test('runtime builder removes every persisted path forbidden by artifact policy', async () => { const source = await fs.readFile( path.join(ROOT, 'scripts', 'build-portal-runtime.mjs'), 'utf8', ); assert.match(source, /removeForbiddenPortalRuntimePaths/); assert.match(source, /removeForbiddenPortalRuntimePaths\(runtimeRoot\)/); }); test('runtime builder materializes and declares required Linux ARM64 native packages', async () => { const source = await fs.readFile( path.join(ROOT, 'scripts', 'build-portal-runtime.mjs'), 'utf8', ); assert.match(source, /@resvg\/resvg-js-linux-arm64-gnu/); assert.match(source, /@node-rs\/argon2-linux-arm64-gnu/); assert.match(source, /installLinuxArm64NativePackages\(\)/); assert.match(source, /optionalDependencies: runtimeOptionalDependencies/); assert.match(source, /缺少原生 \.node 文件/); }); test('packaged runtime gate isolates persistent roots and rejects artifact mutation', async () => { const localStackSource = await fs.readFile( path.join(ROOT, 'release-gate', 'local-stack.mjs'), 'utf8', ); assert.match(localStackSource, /MEMIND_PORTAL_H5_ROOT: sandboxRoot/); assert.match(localStackSource, /MEMIND_DEEPSEEK_DISABLE_THINKING: '1'/); assert.match(localStackSource, /deepseek-no-think-proxy\.mjs/); assert.match(localStackSource, /MEMIND_ORCHESTRATOR_MODE: 'shadow'/); assert.match(localStackSource, /MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED: '1'/); assert.match(localStackSource, /H5_USERS_ROOT: usersRoot/); assert.match(localStackSource, /MINDSPACE_STORAGE_ROOT: storageRoot/); assert.match(localStackSource, /MEMIND_SHARED_PUBLISH_ROOT: publishRoot/); for (const scriptName of [ 'run-release-gate-runtime-cold-start.mjs', 'run-release-gate-runtime-upgrade.mjs', ]) { const scriptSource = await fs.readFile(path.join(ROOT, 'scripts', scriptName), 'utf8'); assert.match(scriptSource, /const artifactBefore = await hashArtifact\(runtimeRoot\)/); assert.match(scriptSource, /const artifactAfter = await hashArtifact\(runtimeRoot\)/); assert.match(scriptSource, /artifactAfter\.sha256 !== artifactBefore\.sha256/); } }); test('production release rejects --skip-tests before repository or network preflight', () => { const result = spawnSync( 'bash', [path.join(ROOT, 'scripts', 'release-portal-runtime-prod.sh'), '--skip-tests'], { cwd: ROOT, encoding: 'utf8' }, ); assert.notEqual(result.status, 0); assert.match(result.stderr, /禁止 --skip-tests/); assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /103 只读预检/); }); test('production canary verifies the exact Gate artifact before any 103 preflight or upload', async () => { const source = await fs.readFile(CANARY_RELEASE, 'utf8'); const gateIndex = source.indexOf('verify-release-gate-report.mjs'); const preflightIndex = source.indexOf('Run 103 read-only preflight'); const uploadIndex = source.indexOf('Upload the verified candidate bundle'); assert.ok(gateIndex > 0, 'missing canary Gate verifier'); assert.ok(preflightIndex > gateIndex, '103 preflight must follow Gate verification'); assert.ok(uploadIndex > preflightIndex, 'upload must follow read-only preflight'); assert.match(source, /branch.*!= "main"/); assert.match(source, /rev-parse origin\/main/); assert.match(source, /Production canary release forbids/); }); test('production canary keeps stable 8081 live and switches only after verified backups and fallback', async () => { const source = await fs.readFile(CANARY_RELEASE, 'utf8'); const fullBackup = source.indexOf('Create and verify the full stable backup'); const persistBackup = source.indexOf('Create and verify the persisted-data backup'); const goosedStart = source.indexOf('Start an isolated goosed candidate on 18015'); const candidateStart = source.indexOf('Start the passive candidate Portal on 18081'); const proxyStart = source.indexOf('Start the fail-closed identity router on 18080'); const tunnelSwitch = source.indexOf('Switch only the reverse tunnel'); const fallbackProbe = source.indexOf('route-probe?username=john'); assert.ok(fullBackup > 0); assert.ok(persistBackup > fullBackup); assert.ok(goosedStart > persistBackup); assert.ok(candidateStart > goosedStart); assert.ok(proxyStart > candidateStart); assert.ok(tunnelSwitch > proxyStart); assert.ok(fallbackProbe > tunnelSwitch); assert.match(source, /write_tunnel_plist 8081/); assert.match(source, /CANARY_USERNAMES="john"/); assert.match(source, /CANARY_WECHAT_USER_IDS="wx_ul610et8"/); assert.match(source, /route-probe\?\$\{probe_query\}/); assert.match(source, /candidate_healthy/); assert.match(source, /MEMIND_CANARY_RELEASE_ID/); assert.doesNotMatch(source, /lsof -tiTCP:8081/); assert.doesNotMatch(source, /bootout.*cn\.tkmind\.memind-portal/); }); test('release readiness ignores only the generated canary artifact cover path', async () => { const source = await fs.readFile( path.join(ROOT, 'scripts', 'check-release-ready.sh'), 'utf8', ); assert.match(source, /:\(exclude\)\.runtime\/portal\/public\/plaza-covers\/\*\*/); assert.doesNotMatch(source, /:\(exclude\)\.runtime\/\*\*/); });