import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { gateChecks, requiredAttestations } from './production-launch-gate.js'; const repoRoot = process.cwd(); const scriptPath = path.join(repoRoot, 'scripts', 'production-launch-gate.js'); function isoNow() { return new Date().toISOString(); } function sampleSummaryValue(expectedValue) { if (!expectedValue || typeof expectedValue !== 'object' || Array.isArray(expectedValue)) return expectedValue; if (Object.prototype.hasOwnProperty.call(expectedValue, 'eq')) return expectedValue.eq; if (Object.prototype.hasOwnProperty.call(expectedValue, 'oneOf')) return expectedValue.oneOf?.[0]; if (Object.prototype.hasOwnProperty.call(expectedValue, 'lte')) return expectedValue.lte; if (Object.prototype.hasOwnProperty.call(expectedValue, 'lt')) return Number(expectedValue.lt) - 1; if (Object.prototype.hasOwnProperty.call(expectedValue, 'gte')) return expectedValue.gte; if (Object.prototype.hasOwnProperty.call(expectedValue, 'gt')) return Number(expectedValue.gt) + 1; return expectedValue; } function sampleSummary(summarySpec) { const result = {}; for (const [key, value] of Object.entries(summarySpec || {})) { const parts = key.split('.'); let cursor = result; for (const part of parts.slice(0, -1)) { if (!cursor[part] || typeof cursor[part] !== 'object') cursor[part] = {}; cursor = cursor[part]; } cursor[parts.at(-1)] = sampleSummaryValue(value); } return result; } function createEvidence(tempDir, overrides = {}) { const artifactDir = path.join(tempDir, 'launch-artifacts'); fs.mkdirSync(artifactDir, { recursive: true }); const checks = gateChecks.map(spec => { const artifact = `launch-artifacts/${spec.id}.log`; fs.writeFileSync(path.join(tempDir, artifact), `[PASS] ${spec.id}\n`, 'utf8'); return { id: spec.id, status: 'pass', command: `npm run ${spec.commandIncludes} -- recorded-for-launch-gate`, completedAt: isoNow(), artifact, summary: sampleSummary(spec.summary), }; }); const attestations = requiredAttestations.map(spec => ({ id: spec.id, status: 'approved', approver: 'test-owner', approvedAt: isoNow(), notes: spec.label, })); return { schemaVersion: 1, environment: 'production', commit: '52cef9fabcd1234567890abcdef1234567890abc', target: { apiBaseUrl: 'https://api.gongxue100.com', studentH5Url: 'https://www.gongxue100.com', tenantAdminH5Url: 'https://admin.gongxue100.com', platformAdminH5Url: 'https://console.gongxue100.com', }, checks, attestations, ...overrides, }; } function runGate(evidence, options = {}) { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-launch-gate-')); const evidencePath = path.join(tempDir, 'evidence.json'); const finalEvidence = typeof evidence === 'function' ? evidence(tempDir) : evidence; fs.writeFileSync(evidencePath, JSON.stringify(finalEvidence, null, 2), 'utf8'); const result = spawnSync(process.execPath, [scriptPath, '--evidence', evidencePath, '--json', ...(options.args || [])], { cwd: repoRoot, encoding: 'utf8', env: { PATH: process.env.PATH || '', Path: process.env.Path || '', SystemRoot: process.env.SystemRoot || '', ComSpec: process.env.ComSpec || '', TEMP: process.env.TEMP || os.tmpdir(), TMP: process.env.TMP || os.tmpdir(), }, }); const payload = JSON.parse(result.stdout || '{}'); fs.rmSync(tempDir, { recursive: true, force: true }); return { ...result, payload }; } const safe = runGate(tempDir => createEvidence(tempDir)); assert.equal(safe.status, 0, `complete launch evidence should pass: ${safe.stdout} ${safe.stderr}`); assert.equal(safe.payload.summary?.blocker, 0, 'complete launch evidence should have no blockers'); const missingArtifact = runGate(tempDir => { const evidence = createEvidence(tempDir); const item = evidence.checks.find(check => check.id === 'auth.remote-smoke'); fs.rmSync(path.join(tempDir, item.artifact), { force: true }); return evidence; }); assert.notEqual(missingArtifact.status, 0, 'missing artifact should fail launch gate'); assert.ok( missingArtifact.payload.checks?.some(item => item.id === 'check.auth.remote-smoke.artifact' && item.status === 'blocker'), 'missing artifact should be reported as a blocker', ); const wrongMigrationProfile = runGate(tempDir => { const evidence = createEvidence(tempDir); const item = evidence.checks.find(check => check.id === 'migration.pb-production-dry-run'); item.summary.migrationProfile = 'development'; return evidence; }); assert.notEqual(wrongMigrationProfile.status, 0, 'development dry-run evidence should fail launch gate'); assert.ok( wrongMigrationProfile.payload.checks?.some(item => item.id === 'check.migration.pb-production-dry-run.summary' && item.status === 'blocker'), 'migration profile mismatch should be reported as a blocker', ); const missingPostgresTuning = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.checks = evidence.checks.filter(item => item.id !== 'postgres.tuning-evidence'); return evidence; }); assert.notEqual(missingPostgresTuning.status, 0, 'missing PostgreSQL tuning evidence should fail launch gate'); assert.ok( missingPostgresTuning.payload.checks?.some(item => item.id === 'check.postgres.tuning-evidence' && item.status === 'blocker'), 'missing PostgreSQL tuning evidence should be reported as a blocker', ); const slowBenchmark = runGate(tempDir => { const evidence = createEvidence(tempDir); const item = evidence.checks.find(check => check.id === 'performance.api-real-data-read'); item.summary.p95Ms = 301; return evidence; }); assert.notEqual(slowBenchmark.status, 0, 'slow API benchmark should fail launch gate'); assert.ok( slowBenchmark.payload.checks?.some(item => item.id === 'check.performance.api-real-data-read.summary' && item.status === 'blocker'), 'slow API benchmark should be reported as a blocker', ); const missingMixedBenchmark = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.checks = evidence.checks.filter(item => item.id !== 'performance.api-real-data-mixed'); return evidence; }); assert.notEqual(missingMixedBenchmark.status, 0, 'missing mixed read/write benchmark should fail launch gate'); assert.ok( missingMixedBenchmark.payload.checks?.some(item => item.id === 'check.performance.api-real-data-mixed' && item.status === 'blocker'), 'missing mixed benchmark should be reported as a blocker', ); const slowMixedBenchmark = runGate(tempDir => { const evidence = createEvidence(tempDir); const item = evidence.checks.find(check => check.id === 'performance.api-real-data-mixed'); item.summary.p95Ms = 501; return evidence; }); assert.notEqual(slowMixedBenchmark.status, 0, 'slow mixed API benchmark should fail launch gate'); assert.ok( slowMixedBenchmark.payload.checks?.some(item => item.id === 'check.performance.api-real-data-mixed.summary' && item.status === 'blocker'), 'slow mixed benchmark should be reported as a blocker', ); const missingBusinessSampling = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.checks = evidence.checks.filter(item => item.id !== 'migration.pb-import-sample'); return evidence; }); assert.notEqual(missingBusinessSampling.status, 0, 'missing import business sampling should fail launch gate'); assert.ok( missingBusinessSampling.payload.checks?.some(item => item.id === 'check.migration.pb-import-sample' && item.status === 'blocker'), 'missing import business sampling should be reported as a blocker', ); const missingTaroStaticSmoke = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.checks = evidence.checks.filter(item => item.id !== 'taro.h5-static-smoke'); return evidence; }); assert.notEqual(missingTaroStaticSmoke.status, 0, 'missing H5 static smoke should fail launch gate'); assert.ok( missingTaroStaticSmoke.payload.checks?.some(item => item.id === 'check.taro.h5-static-smoke' && item.status === 'blocker'), 'missing H5 static smoke should be reported as a blocker', ); const missingTaroInteractionSmoke = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.checks = evidence.checks.filter(item => item.id !== 'taro.h5-interaction-smoke'); return evidence; }); assert.notEqual(missingTaroInteractionSmoke.status, 0, 'missing H5 interaction smoke should fail launch gate'); assert.ok( missingTaroInteractionSmoke.payload.checks?.some(item => item.id === 'check.taro.h5-interaction-smoke' && item.status === 'blocker'), 'missing H5 interaction smoke should be reported as a blocker', ); const oldTaroInteractionCoverage = runGate(tempDir => { const evidence = createEvidence(tempDir); const item = evidence.checks.find(check => check.id === 'taro.h5-interaction-smoke'); item.summary.pass = 26; return evidence; }); assert.notEqual(oldTaroInteractionCoverage.status, 0, 'old 26-check H5 interaction evidence should fail launch gate'); assert.ok( oldTaroInteractionCoverage.payload.checks?.some(item => item.id === 'check.taro.h5-interaction-smoke.summary' && item.status === 'blocker'), 'old H5 interaction coverage should be reported as a blocker', ); const missingAdminWriteCoverage = runGate(tempDir => { const evidence = createEvidence(tempDir); const item = evidence.checks.find(check => check.id === 'taro.h5-interaction-smoke'); item.summary.mockApi.keyRequests.platformAdminWrites = 0; return evidence; }); assert.notEqual(missingAdminWriteCoverage.status, 0, 'H5 interaction evidence without platform admin writes should fail launch gate'); assert.ok( missingAdminWriteCoverage.payload.checks?.some(item => item.id === 'check.taro.h5-interaction-smoke.summary' && item.status === 'blocker'), 'missing platform admin write coverage should be reported as a blocker', ); const missingTaroReleaseManifest = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.checks = evidence.checks.filter(item => item.id !== 'taro.h5-release-manifest'); return evidence; }); assert.notEqual(missingTaroReleaseManifest.status, 0, 'missing H5 release manifest should fail launch gate'); assert.ok( missingTaroReleaseManifest.payload.checks?.some(item => item.id === 'check.taro.h5-release-manifest' && item.status === 'blocker'), 'missing H5 release manifest should be reported as a blocker', ); const missingRepoSecurityScan = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.checks = evidence.checks.filter(item => item.id !== 'security.repo-scan'); return evidence; }); assert.notEqual(missingRepoSecurityScan.status, 0, 'missing repository security scan should fail launch gate'); assert.ok( missingRepoSecurityScan.payload.checks?.some(item => item.id === 'check.security.repo-scan' && item.status === 'blocker'), 'missing repository security scan should be reported as a blocker', ); const missingAttestation = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.attestations = evidence.attestations.filter(item => item.id !== 'backup.snapshot'); return evidence; }); assert.notEqual(missingAttestation.status, 0, 'missing manual attestation should fail launch gate'); assert.ok( missingAttestation.payload.checks?.some(item => item.id === 'attestation.backup.snapshot' && item.status === 'blocker'), 'missing attestation should be reported as a blocker', ); console.log('[PASS] production launch gate');