forked from wangziqi/gongxue-base
190 lines
7.6 KiB
JavaScript
190 lines
7.6 KiB
JavaScript
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 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 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 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');
|