forked from wangziqi/gongxue-base
1051 lines
49 KiB
JavaScript
1051 lines
49 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 crypto from 'node:crypto';
|
|
import {
|
|
gateChecks,
|
|
parseArgs,
|
|
productionUrlFailure,
|
|
requiredAttestations,
|
|
validateEvidence,
|
|
weappAttestations,
|
|
weappGateChecks,
|
|
} 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, 'truthy')) return expectedValue.truthy ? 'sample-truthy-value' : '';
|
|
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 nestedSummaryPayload(summary) {
|
|
return { summary };
|
|
}
|
|
|
|
function pocketBaseDryRunPayload(summary) {
|
|
return {
|
|
migrationProfile: summary.migrationProfile,
|
|
summary: {
|
|
blockers: summary.blocker,
|
|
warnings: summary.warning,
|
|
},
|
|
migrationReadiness: {
|
|
requiredCollections: Array.from({ length: 3 }, (_, index) => ({ collection: `required-${index}`, present: true })),
|
|
criticalFieldCoverage: Array.from({ length: 3 }, (_, index) => ({ collection: `coverage-${index}`, present: true })),
|
|
},
|
|
};
|
|
}
|
|
|
|
function productionEvidencePayload(spec, summary) {
|
|
if (spec.id === 'db.migration-history') return summary;
|
|
if (spec.id === 'auth.platform-admin-bootstrap') {
|
|
const { identityMatches: _identityMatches, ...artifact } = summary;
|
|
return artifact;
|
|
}
|
|
if (spec.id === 'backup.restore-drill') return summary;
|
|
if (spec.id === 'taro.supply-chain') {
|
|
const reviewedEdges = [
|
|
{ parent: '@tarojs/components', dependency: 'swiper', declared: '11.1.15' },
|
|
{ parent: '@tarojs/components-react', dependency: 'swiper', declared: '11.1.15' },
|
|
{ parent: '@tarojs/plugin-platform-h5', dependency: 'lodash-es', declared: '4.17.21' },
|
|
{ parent: '@tarojs/taro-h5', dependency: 'lodash-es', declared: '4.17.21' },
|
|
];
|
|
return {
|
|
schemaVersion: summary.schemaVersion,
|
|
status: summary.status,
|
|
securedBundleDependencies: summary.securedBundleDependencies,
|
|
npmLs: {
|
|
edges: reviewedEdges,
|
|
},
|
|
audit: { counts: summary.audit.counts },
|
|
riskBoundary: {
|
|
appliesTo: 'Taro 4.2.0 CLI and build toolchain',
|
|
controls: Array.from({ length: summary.riskControlCount }, (_, index) => `control-${index}`),
|
|
},
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function jsonArtifactPayload(spec, summary) {
|
|
const productionPayload = productionEvidencePayload(spec, summary);
|
|
if (productionPayload) return productionPayload;
|
|
if (spec.id === 'readiness.production.env' || spec.id === 'readiness.production.db') return nestedSummaryPayload(summary);
|
|
if (spec.id === 'postgres.tuning-evidence' || spec.id === 'auth.sms-pnvs-diagnostics' || spec.id === 'auth.sms-pnvs-remote-smoke') return summary;
|
|
if (spec.id === 'migration.pb-production-dry-run') return pocketBaseDryRunPayload(summary);
|
|
if (spec.id === 'api.launch-persona-smoke') return summary;
|
|
if (spec.id === 'api.dynamic-tenant-cors-smoke' || spec.id === 'db.tenant-foreign-key-audit') return summary;
|
|
if (spec.id === 'taro.h5-static-smoke' || spec.id === 'taro.h5-release-guardrails' || spec.id === 'security.repo-scan') return nestedSummaryPayload(summary);
|
|
if (spec.id === 'taro.h5-interaction-smoke') return { summary: { fail: summary.fail, pass: summary.pass }, mockApi: summary.mockApi };
|
|
if (spec.id === 'taro.h5-release-manifest') return { ...nestedSummaryPayload(summary), portals: [] };
|
|
if (spec.id === 'taro.weapp-release-guardrails') return nestedSummaryPayload(summary);
|
|
return null;
|
|
}
|
|
|
|
function writeCheckArtifact(spec, summary, artifactPath) {
|
|
const jsonPayload = jsonArtifactPayload(spec, summary);
|
|
if (jsonPayload) {
|
|
fs.writeFileSync(artifactPath, `${JSON.stringify(jsonPayload, null, 2)}\n`, 'utf8');
|
|
return;
|
|
}
|
|
fs.writeFileSync(artifactPath, `TIKU_LAUNCH_GATE_SUCCESS:${spec.id}\n`, 'utf8');
|
|
}
|
|
|
|
function rewriteArtifact(tempDir, item, payload) {
|
|
const artifactPath = path.join(tempDir, item.artifact);
|
|
fs.writeFileSync(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
item.artifactSha256 = crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex');
|
|
}
|
|
|
|
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`;
|
|
const artifactPath = path.join(tempDir, artifact);
|
|
const summary = sampleSummary(spec.summary);
|
|
if (spec.id === 'db.migration-history') {
|
|
const migrationFiles = fs.readdirSync(path.join(repoRoot, 'supabase', 'migrations'))
|
|
.filter(file => file.endsWith('.sql'))
|
|
.sort();
|
|
const latestMigration = /^(\d+)_/.exec(migrationFiles.at(-1) || '')?.[1];
|
|
summary.latestRepositoryMigration = latestMigration;
|
|
summary.latestAppliedMigration = latestMigration;
|
|
}
|
|
if (spec.id === 'auth.platform-admin-bootstrap') {
|
|
const identityHash = crypto.createHash('sha256').update('platform-admin-auth-user').digest('hex');
|
|
summary.adminUserIdSha256 = identityHash;
|
|
summary.authSmokeExpectedUserIdSha256 = identityHash;
|
|
summary.dryRunArtifactSha256 = crypto.createHash('sha256').update('platform-admin-dry-run-artifact').digest('hex');
|
|
summary.applyArtifactSha256 = crypto.createHash('sha256').update('platform-admin-apply-artifact').digest('hex');
|
|
summary.authSmokeArtifactSha256 = crypto.createHash('sha256').update('platform-admin-auth-smoke-artifact').digest('hex');
|
|
summary.auditArtifactSha256 = crypto.createHash('sha256').update('platform-admin-audit-artifact').digest('hex');
|
|
}
|
|
if (spec.id === 'backup.restore-drill') {
|
|
summary.verificationArtifactSha256 = crypto.createHash('sha256').update('backup-restore-verification-artifact').digest('hex');
|
|
}
|
|
if (spec.id === 'taro.h5-release-manifest') {
|
|
const releaseRoot = path.join(tempDir, 'candidate');
|
|
const portalDirs = [
|
|
['student', 'apps/taro/dist/h5-student'],
|
|
['tenant-admin', 'apps/taro/dist/h5-tenant-admin'],
|
|
['platform-admin', 'apps/taro/dist/h5-platform-admin'],
|
|
];
|
|
const portals = portalDirs.map(([portal, relativeDir]) => {
|
|
const dir = path.join(releaseRoot, relativeDir);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(path.join(dir, 'index.html'), `<div id="app">${portal}</div>\n`, 'utf8');
|
|
const content = fs.readFileSync(path.join(dir, 'index.html'));
|
|
const treeHash = crypto.createHash('sha256')
|
|
.update('index.html\0')
|
|
.update(String(content.length))
|
|
.update('\0')
|
|
.update(content)
|
|
.update('\0')
|
|
.digest('hex');
|
|
return { portal, dist: { treeSha256: treeHash } };
|
|
});
|
|
fs.writeFileSync(artifactPath, `${JSON.stringify({ summary, portals }, null, 2)}\n`, 'utf8');
|
|
} else if (spec.id === 'performance.tenant-students-100k') {
|
|
const caseItem = (id, p95, indexes) => ({
|
|
id,
|
|
latencyMs: { p95 },
|
|
explain: { summary: { indexes } },
|
|
});
|
|
fs.writeFileSync(artifactPath, `${JSON.stringify({
|
|
schemaVersion: 1,
|
|
kind: 'tenant-student-capacity',
|
|
safety: { databaseEnvironment: summary.databaseEnvironment },
|
|
fixture: summary.fixture,
|
|
config: { deepCursorApproximateOffset: summary.deepCursorApproximateOffset },
|
|
cases: [
|
|
caseItem('first-page', summary.firstPageP95Ms, ['idx_memberships_student_keyset_page']),
|
|
caseItem('deep-cursor', summary.deepCursorP95Ms, ['idx_memberships_student_keyset_page']),
|
|
caseItem('name-substring', summary.searchP95MaxMs, ['idx_platform_users_identity_search_trgm']),
|
|
caseItem('phone-substring', summary.searchP95MaxMs, ['idx_platform_users_identity_search_trgm']),
|
|
caseItem('email-substring', summary.searchP95MaxMs, ['idx_platform_users_identity_search_trgm']),
|
|
],
|
|
cleanup: {
|
|
cleanupVerified: summary.cleanupVerified,
|
|
remaining: { tenants: 0, platformUsers: 0, memberships: 0, profiles: 0 },
|
|
},
|
|
}, null, 2)}\n`, 'utf8');
|
|
} else {
|
|
writeCheckArtifact(spec, summary, artifactPath);
|
|
}
|
|
return {
|
|
id: spec.id,
|
|
status: 'pass',
|
|
command: `npm run ${spec.commandIncludes} -- recorded-for-launch-gate`,
|
|
completedAt: isoNow(),
|
|
artifact,
|
|
artifactSha256: crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex'),
|
|
summary,
|
|
};
|
|
});
|
|
|
|
const readinessDbCheck = checks.find(item => item.id === 'readiness.production.db');
|
|
const migrationHistoryCheck = checks.find(item => item.id === 'db.migration-history');
|
|
const readinessDbArtifactPath = path.join(tempDir, readinessDbCheck.artifact);
|
|
migrationHistoryCheck.summary.readinessArtifactSha256 = crypto.createHash('sha256')
|
|
.update(fs.readFileSync(readinessDbArtifactPath))
|
|
.digest('hex');
|
|
const migrationHistoryArtifactPath = path.join(tempDir, migrationHistoryCheck.artifact);
|
|
writeCheckArtifact(
|
|
gateChecks.find(spec => spec.id === migrationHistoryCheck.id),
|
|
migrationHistoryCheck.summary,
|
|
migrationHistoryArtifactPath,
|
|
);
|
|
migrationHistoryCheck.artifactSha256 = crypto.createHash('sha256')
|
|
.update(fs.readFileSync(migrationHistoryArtifactPath))
|
|
.digest('hex');
|
|
|
|
const attestations = requiredAttestations.map(spec => ({
|
|
id: spec.id,
|
|
status: 'approved',
|
|
approver: 'test-owner',
|
|
approvedAt: isoNow(),
|
|
notes: spec.label,
|
|
}));
|
|
|
|
return {
|
|
schemaVersion: 1,
|
|
environment: 'production',
|
|
releaseTargets: ['h5'],
|
|
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;
|
|
const deployReleaseRoot = options.deployReleaseRoot === '__TEMP_CANDIDATE__'
|
|
? path.join(tempDir, 'candidate')
|
|
: (options.deployReleaseRoot || '');
|
|
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(),
|
|
DEPLOY_COMMIT_SHA: options.deployCommitSha || '',
|
|
DEPLOY_RELEASE_ROOT: deployReleaseRoot,
|
|
},
|
|
});
|
|
|
|
const payload = JSON.parse(result.stdout || '{}');
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
return { ...result, payload };
|
|
}
|
|
|
|
function sha256(value) {
|
|
return crypto.createHash('sha256').update(value).digest('hex');
|
|
}
|
|
|
|
function liveH5Fixture(tempDir) {
|
|
const portalContents = {
|
|
student: {
|
|
targetKey: 'studentH5Url',
|
|
baseUrl: 'https://student.gongxue100.com',
|
|
appPath: '/js/app.student.js',
|
|
app: 'console.log("student");\n',
|
|
},
|
|
'tenant-admin': {
|
|
targetKey: 'tenantAdminH5Url',
|
|
baseUrl: 'https://admin.gongxue100.com',
|
|
appPath: '/js/app.tenant.js',
|
|
app: 'console.log("tenant-admin");\n',
|
|
},
|
|
'platform-admin': {
|
|
targetKey: 'platformAdminH5Url',
|
|
baseUrl: 'https://console.gongxue100.com',
|
|
appPath: '/js/app.platform.js',
|
|
app: 'console.log("platform-admin");\n',
|
|
},
|
|
};
|
|
const evidence = createEvidence(tempDir);
|
|
evidence.liveH5 = { portals: [] };
|
|
const responses = new Map();
|
|
|
|
for (const [portal, fixture] of Object.entries(portalContents)) {
|
|
const index = `<!doctype html><html><head><script defer src="${fixture.appPath}"></script></head><body><div id="app"></div></body></html>`;
|
|
const runtime = `${JSON.stringify({ portal, apiBaseUrl: evidence.target.apiBaseUrl })}\n`;
|
|
evidence.target[fixture.targetKey] = fixture.baseUrl;
|
|
evidence.liveH5.portals.push({
|
|
portal,
|
|
indexSha256: sha256(index),
|
|
appPath: fixture.appPath,
|
|
appSha256: sha256(fixture.app),
|
|
});
|
|
responses.set(`${fixture.baseUrl}/index.html`, { body: index, contentType: 'text/html; charset=utf-8' });
|
|
responses.set(`${fixture.baseUrl}/runtime-config.json`, { body: runtime, contentType: 'application/json' });
|
|
responses.set(`${fixture.baseUrl}${fixture.appPath}`, { body: fixture.app, contentType: 'application/javascript' });
|
|
}
|
|
|
|
return { evidence, responses };
|
|
}
|
|
|
|
function liveH5ManifestFixture(tempDir) {
|
|
const fixture = liveH5Fixture(tempDir);
|
|
const candidateRoot = path.join(tempDir, 'candidate');
|
|
const manifest = {
|
|
schemaVersion: 1,
|
|
portals: [],
|
|
};
|
|
|
|
for (const portalItem of fixture.evidence.liveH5.portals) {
|
|
const portalDir = path.join(
|
|
candidateRoot,
|
|
'apps',
|
|
'taro',
|
|
'dist',
|
|
portalItem.portal === 'student' ? 'h5-student' : `h5-${portalItem.portal}`,
|
|
);
|
|
const baseUrl = portalItem.portal === 'student'
|
|
? 'https://student.gongxue100.com'
|
|
: portalItem.portal === 'tenant-admin'
|
|
? 'https://admin.gongxue100.com'
|
|
: 'https://console.gongxue100.com';
|
|
const index = fixture.responses.get(`${baseUrl}/index.html`).body;
|
|
const app = fixture.responses.get(`${baseUrl}${portalItem.appPath}`).body;
|
|
const appFile = path.join(portalDir, `.${portalItem.appPath}`);
|
|
fs.mkdirSync(path.dirname(appFile), { recursive: true });
|
|
fs.writeFileSync(path.join(portalDir, 'index.html'), index, 'utf8');
|
|
fs.writeFileSync(appFile, app, 'utf8');
|
|
manifest.portals.push({
|
|
portal: portalItem.portal,
|
|
dist: {
|
|
dir: '../../must-not-be-used',
|
|
indexSha256: sha256(index),
|
|
},
|
|
});
|
|
}
|
|
|
|
const manifestPath = path.join(tempDir, 'launch-artifacts', 'taro-h5-release-manifest.json');
|
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
fixture.evidence.liveH5 = {
|
|
releaseManifestArtifact: path.relative(tempDir, manifestPath),
|
|
releaseManifestSha256: sha256(fs.readFileSync(manifestPath)),
|
|
};
|
|
return fixture;
|
|
}
|
|
|
|
function fixtureFetch(responses) {
|
|
return async url => {
|
|
const key = String(url);
|
|
const item = responses.get(key);
|
|
if (!item) return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
|
|
return new Response(item.body, {
|
|
status: 200,
|
|
headers: { 'content-type': item.contentType },
|
|
});
|
|
};
|
|
}
|
|
|
|
function removeTempDir(tempDir) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
|
|
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 evidenceTemplate = JSON.parse(fs.readFileSync(path.join(repoRoot, 'docs', 'refactor', 'production-launch-evidence.template.json'), 'utf8'));
|
|
const templateCheckIds = evidenceTemplate.checks.map(item => item.id);
|
|
const coreGateCheckIds = gateChecks.map(item => item.id);
|
|
assert.deepEqual(templateCheckIds, coreGateCheckIds, 'template core check IDs must exactly match gateChecks in order');
|
|
assert.equal(new Set(templateCheckIds).size, templateCheckIds.length, 'template core check IDs must be unique');
|
|
assert.ok(coreGateCheckIds.includes('worker.platform-billing'), 'platform billing worker must be a core launch check');
|
|
assert.ok(coreGateCheckIds.includes('worker.platform-dunning'), 'platform dunning worker must be a core launch check');
|
|
for (const id of ['db.migration-history', 'auth.platform-admin-bootstrap', 'backup.restore-drill', 'taro.supply-chain']) {
|
|
assert.ok(coreGateCheckIds.includes(id), `${id} must be a core launch check`);
|
|
}
|
|
for (const item of evidenceTemplate.checks) {
|
|
const spec = gateChecks.find(candidate => candidate.id === item.id);
|
|
if (typeof spec?.artifactSummary === 'function') continue;
|
|
assert.ok(
|
|
String(item.command || '').includes(`TIKU_LAUNCH_GATE_SUCCESS:${item.id}`),
|
|
`log-backed template check ${item.id} must append its explicit success sentinel`,
|
|
);
|
|
}
|
|
assert.equal(
|
|
evidenceTemplate.checks.filter(item => item.artifact && !item.artifactSha256).length,
|
|
0,
|
|
'every template check with an artifact must include artifactSha256',
|
|
);
|
|
assert.ok(evidenceTemplate.liveH5?.releaseManifestSha256, 'template must document the strict live H5 release manifest hash');
|
|
|
|
for (const rejectedUrl of [
|
|
'http://api.gongxue100.com',
|
|
'https://localhost',
|
|
'https://127.0.0.1',
|
|
'https://portal.test',
|
|
'https://portal.example',
|
|
'https://portal.example.com',
|
|
'https://replace-with-domain.invalid',
|
|
'https://replace-with-real-domain.com',
|
|
]) {
|
|
assert.ok(productionUrlFailure(rejectedUrl), `production URL validation must reject ${rejectedUrl}`);
|
|
}
|
|
assert.equal(productionUrlFailure('https://student.gongxue100.com'), '', 'real HTTPS production URL should be accepted');
|
|
assert.equal(parseArgs(['node', 'gate', '--verify-live-h5']).verifyLiveH5, true, 'CLI should explicitly enable strict live H5 validation');
|
|
assert.equal(
|
|
parseArgs(['node', 'gate', '--verify-live-h5', '--no-verify-live-h5']).verifyLiveH5,
|
|
false,
|
|
'CLI should allow deployment wrappers to explicitly disable inherited live validation',
|
|
);
|
|
|
|
const placeholderTarget = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
evidence.target.studentH5Url = 'https://student.example.com';
|
|
return evidence;
|
|
});
|
|
assert.notEqual(placeholderTarget.status, 0, 'placeholder target URL should fail launch gate');
|
|
assert.ok(placeholderTarget.payload.checks?.some(item => item.id === 'target.studentH5Url' && item.status === 'blocker'));
|
|
|
|
{
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-launch-gate-live-'));
|
|
try {
|
|
const fixture = liveH5Fixture(tempDir);
|
|
const checks = await validateEvidence(fixture.evidence, {
|
|
evidencePath: path.join(tempDir, 'evidence.json'),
|
|
allowStale: false,
|
|
maxAgeDays: 14,
|
|
verifyLiveH5: true,
|
|
liveTimeoutMs: 5_000,
|
|
fetchImpl: fixtureFetch(fixture.responses),
|
|
deployReleaseRoot: path.join(tempDir, 'candidate'),
|
|
});
|
|
assert.equal(checks.some(item => item.status === 'blocker'), false, JSON.stringify(checks.filter(item => item.status === 'blocker'), null, 2));
|
|
assert.equal(checks.filter(item => /^live_h5\..+\.app_hash$/.test(item.id) && item.status === 'pass').length, 3);
|
|
} finally {
|
|
removeTempDir(tempDir);
|
|
}
|
|
}
|
|
|
|
{
|
|
const tempDir = fs.mkdtempSync(path.join(repoRoot, '.tmp-launch-gate-manifest-'));
|
|
try {
|
|
const fixture = liveH5ManifestFixture(tempDir);
|
|
const checks = await validateEvidence(fixture.evidence, {
|
|
evidencePath: path.join(tempDir, 'evidence.json'),
|
|
allowStale: false,
|
|
maxAgeDays: 14,
|
|
verifyLiveH5: true,
|
|
liveTimeoutMs: 5_000,
|
|
fetchImpl: fixtureFetch(fixture.responses),
|
|
deployReleaseRoot: path.join(tempDir, 'candidate'),
|
|
});
|
|
assert.equal(checks.some(item => item.status === 'blocker'), false, JSON.stringify(checks.filter(item => item.status === 'blocker'), null, 2));
|
|
assert.equal(checks.filter(item => /^live_h5\..+\.candidate_files$/.test(item.id) && item.status === 'pass').length, 3);
|
|
} finally {
|
|
removeTempDir(tempDir);
|
|
}
|
|
}
|
|
|
|
{
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-launch-gate-live-'));
|
|
try {
|
|
const fixture = liveH5Fixture(tempDir);
|
|
const studentApp = fixture.evidence.liveH5.portals.find(item => item.portal === 'student');
|
|
fixture.responses.set('https://student.gongxue100.com/js/app.student.js', {
|
|
body: 'console.log("stale-production-bundle");\n',
|
|
contentType: 'application/javascript',
|
|
});
|
|
const checks = await validateEvidence(fixture.evidence, {
|
|
evidencePath: path.join(tempDir, 'evidence.json'),
|
|
allowStale: false,
|
|
maxAgeDays: 14,
|
|
verifyLiveH5: true,
|
|
liveTimeoutMs: 5_000,
|
|
fetchImpl: fixtureFetch(fixture.responses),
|
|
});
|
|
assert.ok(studentApp.appSha256);
|
|
assert.ok(checks.some(item => item.id === 'live_h5.student.app_hash' && item.status === 'blocker'));
|
|
} finally {
|
|
removeTempDir(tempDir);
|
|
}
|
|
}
|
|
|
|
{
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-launch-gate-live-'));
|
|
try {
|
|
const fixture = liveH5Fixture(tempDir);
|
|
fixture.responses.set('https://admin.gongxue100.com/runtime-config.json', {
|
|
body: JSON.stringify({ portal: 'student', apiBaseUrl: 'https://api.other-domain.com' }),
|
|
contentType: 'application/json',
|
|
});
|
|
const checks = await validateEvidence(fixture.evidence, {
|
|
evidencePath: path.join(tempDir, 'evidence.json'),
|
|
allowStale: false,
|
|
maxAgeDays: 14,
|
|
verifyLiveH5: true,
|
|
liveTimeoutMs: 5_000,
|
|
fetchImpl: fixtureFetch(fixture.responses),
|
|
});
|
|
assert.ok(checks.some(item => item.id === 'live_h5.tenant-admin.runtime_portal' && item.status === 'blocker'));
|
|
assert.ok(checks.some(item => item.id === 'live_h5.tenant-admin.runtime_api' && item.status === 'blocker'));
|
|
} finally {
|
|
removeTempDir(tempDir);
|
|
}
|
|
}
|
|
|
|
const wrongCommit = runGate(tempDir => createEvidence(tempDir), {
|
|
deployCommitSha: 'aaaaaaaaaaaa',
|
|
deployReleaseRoot: path.join(os.tmpdir(), 'missing-release'),
|
|
});
|
|
assert.notEqual(wrongCommit.status, 0, 'evidence for another commit should fail launch gate');
|
|
assert.ok(wrongCommit.payload.checks?.some(item => item.id === 'evidence.commit_match' && item.status === 'blocker'));
|
|
|
|
const matchingCandidate = runGate(tempDir => createEvidence(tempDir), {
|
|
deployCommitSha: '52cef9fabcd1',
|
|
deployReleaseRoot: '',
|
|
});
|
|
assert.notEqual(matchingCandidate.status, 0, 'deployment gate without a candidate release root should fail');
|
|
assert.ok(matchingCandidate.payload.checks?.some(item => item.id === 'evidence.release_root' && item.status === 'blocker'));
|
|
|
|
const candidateTreeMatch = runGate(tempDir => createEvidence(tempDir), {
|
|
deployCommitSha: '52cef9fabcd1',
|
|
deployReleaseRoot: '__TEMP_CANDIDATE__',
|
|
});
|
|
assert.equal(candidateTreeMatch.status, 0, `matching candidate release should pass: ${candidateTreeMatch.stdout}`);
|
|
|
|
const candidateTreeMismatch = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
fs.appendFileSync(path.join(tempDir, 'candidate/apps/taro/dist/h5-student/index.html'), 'tampered\n', 'utf8');
|
|
return evidence;
|
|
}, {
|
|
deployCommitSha: '52cef9fabcd1',
|
|
deployReleaseRoot: '__TEMP_CANDIDATE__',
|
|
});
|
|
assert.notEqual(candidateTreeMismatch.status, 0, 'candidate release that differs from the manifest should fail');
|
|
assert.ok(candidateTreeMismatch.payload.checks?.some(item => item.id === 'check.taro.h5-release-manifest.release_tree' && item.status === 'blocker'));
|
|
|
|
const tamperedArtifact = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'auth.remote-smoke');
|
|
fs.appendFileSync(path.join(tempDir, item.artifact), 'tampered\n', 'utf8');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(tamperedArtifact.status, 0, 'tampered artifact should fail launch gate');
|
|
assert.ok(tamperedArtifact.payload.checks?.some(item => item.id === 'check.auth.remote-smoke.artifact_hash' && item.status === 'blocker'));
|
|
|
|
const missingArtifactHash = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
delete evidence.checks.find(check => check.id === 'auth.remote-smoke').artifactSha256;
|
|
return evidence;
|
|
});
|
|
assert.notEqual(missingArtifactHash.status, 0, 'missing artifact hash should fail launch gate');
|
|
assert.ok(missingArtifactHash.payload.checks?.some(item => item.id === 'check.auth.remote-smoke.artifact_hash' && item.status === 'blocker'));
|
|
|
|
const forgedPassLog = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'worker.platform-billing');
|
|
const artifactPath = path.join(tempDir, item.artifact);
|
|
fs.writeFileSync(artifactPath, '[PASS] worker.platform-billing\n', 'utf8');
|
|
item.artifactSha256 = crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(forgedPassLog.status, 0, 'a forged generic PASS log must not satisfy a log-backed check');
|
|
assert.ok(
|
|
forgedPassLog.payload.checks?.some(item => item.id === 'check.worker.platform-billing.artifact_success' && item.status === 'blocker'),
|
|
'missing check-specific log success sentinel must be reported as a blocker',
|
|
);
|
|
|
|
const forgedJsonSummary = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'readiness.production.env');
|
|
const artifactPath = path.join(tempDir, item.artifact);
|
|
const payload = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
|
|
payload.summary.blocker = 1;
|
|
fs.writeFileSync(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
item.artifactSha256 = crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(forgedJsonSummary.status, 0, 'evidence summary must not override a failing structured artifact');
|
|
assert.ok(
|
|
forgedJsonSummary.payload.checks?.some(item => item.id === 'check.readiness.production.env.artifact_summary' && item.status === 'blocker'),
|
|
'structured artifact mismatch must be reported as a blocker',
|
|
);
|
|
|
|
const invalidJsonArtifact = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'readiness.production.db');
|
|
const artifactPath = path.join(tempDir, item.artifact);
|
|
fs.writeFileSync(artifactPath, '{not-json}\n', 'utf8');
|
|
item.artifactSha256 = crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(invalidJsonArtifact.status, 0, 'invalid JSON artifact must not satisfy a structured check');
|
|
assert.ok(
|
|
invalidJsonArtifact.payload.checks?.some(item => item.id === 'check.readiness.production.db.artifact_json' && item.status === 'blocker'),
|
|
'invalid structured artifact must be reported as a JSON blocker',
|
|
);
|
|
|
|
const unverifiedJsonSummaryField = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'readiness.production.env');
|
|
item.summary.operatorAssertion = 'pass';
|
|
return evidence;
|
|
});
|
|
assert.notEqual(unverifiedJsonSummaryField.status, 0, 'structured checks must reject summary fields that are not derived from the artifact gate contract');
|
|
assert.ok(
|
|
unverifiedJsonSummaryField.payload.checks?.some(item => item.id === 'check.readiness.production.env.artifact_summary' && item.status === 'blocker'),
|
|
'unverified structured summary fields must be reported as artifact summary blockers',
|
|
);
|
|
|
|
const staleMigrationArtifact = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'db.migration-history');
|
|
const payload = jsonArtifactPayload(gateChecks.find(spec => spec.id === item.id), item.summary);
|
|
payload.latestAppliedMigration = '202607120018';
|
|
rewriteArtifact(tempDir, item, payload);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(staleMigrationArtifact.status, 0, 'migration evidence older than the repository latest migration must fail');
|
|
assert.ok(staleMigrationArtifact.payload.checks?.some(item => item.id === 'check.db.migration-history.artifact_summary' && item.status === 'blocker'));
|
|
|
|
const forgedMigrationReadinessHash = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'db.migration-history');
|
|
const payload = jsonArtifactPayload(gateChecks.find(spec => spec.id === item.id), item.summary);
|
|
payload.readinessArtifactSha256 = 'not-a-sha256';
|
|
rewriteArtifact(tempDir, item, payload);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(forgedMigrationReadinessHash.status, 0, 'migration evidence must bind the readiness artifact by SHA-256');
|
|
assert.ok(forgedMigrationReadinessHash.payload.checks?.some(item => item.id === 'check.db.migration-history.artifact_summary' && item.status === 'blocker'));
|
|
|
|
const mismatchedMigrationReadinessArtifact = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'readiness.production.db');
|
|
const artifactPath = path.join(tempDir, item.artifact);
|
|
fs.appendFileSync(artifactPath, '\nchanged-after-migration-summary\n', 'utf8');
|
|
item.artifactSha256 = crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(mismatchedMigrationReadinessArtifact.status, 0, 'migration summary must fail when the bound readiness artifact changes');
|
|
assert.ok(mismatchedMigrationReadinessArtifact.payload.checks?.some(item => item.id === 'check.db.migration-history.artifact_summary' && item.status === 'blocker'));
|
|
|
|
const mismatchedPlatformAdminIdentity = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'auth.platform-admin-bootstrap');
|
|
const payload = jsonArtifactPayload(gateChecks.find(spec => spec.id === item.id), item.summary);
|
|
payload.authSmokeExpectedUserIdSha256 = crypto.createHash('sha256').update('different-auth-user').digest('hex');
|
|
rewriteArtifact(tempDir, item, payload);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(mismatchedPlatformAdminIdentity.status, 0, 'platform admin bootstrap and Auth smoke identities must match');
|
|
assert.ok(mismatchedPlatformAdminIdentity.payload.checks?.some(item => item.id === 'check.auth.platform-admin-bootstrap.artifact_summary' && item.status === 'blocker'));
|
|
|
|
const unverifiedRestoreDrill = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'backup.restore-drill');
|
|
const payload = jsonArtifactPayload(gateChecks.find(spec => spec.id === item.id), item.summary);
|
|
payload.integrityVerified = false;
|
|
rewriteArtifact(tempDir, item, payload);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(unverifiedRestoreDrill.status, 0, 'restore drill without verified integrity must fail');
|
|
assert.ok(unverifiedRestoreDrill.payload.checks?.some(item => item.id === 'check.backup.restore-drill.artifact_summary' && item.status === 'blocker'));
|
|
|
|
const forgedRestoreVerificationHash = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'backup.restore-drill');
|
|
const payload = jsonArtifactPayload(gateChecks.find(spec => spec.id === item.id), item.summary);
|
|
payload.verificationArtifactSha256 = 'not-a-sha256';
|
|
rewriteArtifact(tempDir, item, payload);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(forgedRestoreVerificationHash.status, 0, 'restore drill must bind its verification output by SHA-256');
|
|
assert.ok(forgedRestoreVerificationHash.payload.checks?.some(item => item.id === 'check.backup.restore-drill.artifact_summary' && item.status === 'blocker'));
|
|
|
|
const regressedTaroSupplyChain = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'taro.supply-chain');
|
|
const payload = jsonArtifactPayload(gateChecks.find(spec => spec.id === item.id), item.summary);
|
|
payload.securedBundleDependencies.swiper = '11.1.15';
|
|
payload.audit.counts.critical = 4;
|
|
rewriteArtifact(tempDir, item, payload);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(regressedTaroSupplyChain.status, 0, 'Taro supply-chain bundle or vulnerability regression must fail');
|
|
assert.ok(regressedTaroSupplyChain.payload.checks?.some(item => item.id === 'check.taro.supply-chain.artifact_summary' && item.status === 'blocker'));
|
|
|
|
const missingWeappEvidence = runGate(tempDir => createEvidence(tempDir, { releaseTargets: ['h5', 'weapp'] }));
|
|
assert.notEqual(missingWeappEvidence.status, 0, 'WeApp release target without production evidence should fail launch gate');
|
|
assert.ok(missingWeappEvidence.payload.checks?.some(item => item.id === 'check.taro.build.weapp-student' && item.status === 'blocker'));
|
|
|
|
const completeWeappEvidence = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir, { releaseTargets: ['h5', 'weapp'] });
|
|
for (const spec of weappGateChecks) {
|
|
const artifact = `launch-artifacts/${spec.id}.log`;
|
|
const artifactPath = path.join(tempDir, artifact);
|
|
const summary = sampleSummary(spec.summary);
|
|
writeCheckArtifact(spec, summary, artifactPath);
|
|
evidence.checks.push({
|
|
id: spec.id,
|
|
status: 'pass',
|
|
command: `npm run ${spec.commandIncludes}`,
|
|
completedAt: isoNow(),
|
|
artifact,
|
|
artifactSha256: crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex'),
|
|
summary,
|
|
});
|
|
}
|
|
evidence.attestations.push(...weappAttestations.map(spec => ({
|
|
id: spec.id,
|
|
status: 'approved',
|
|
approver: 'test-owner',
|
|
approvedAt: isoNow(),
|
|
notes: spec.label,
|
|
})));
|
|
return evidence;
|
|
});
|
|
assert.equal(completeWeappEvidence.status, 0, `complete WeApp evidence should pass: ${completeWeappEvidence.stdout}`);
|
|
|
|
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 missingSmsPnvsSmoke = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
evidence.checks = evidence.checks.filter(item => item.id !== 'auth.sms-pnvs-remote-smoke');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(missingSmsPnvsSmoke.status, 0, 'missing PNVS SMS smoke should fail launch gate');
|
|
assert.ok(
|
|
missingSmsPnvsSmoke.payload.checks?.some(item => item.id === 'check.auth.sms-pnvs-remote-smoke' && item.status === 'blocker'),
|
|
'missing PNVS SMS smoke should be reported as a blocker',
|
|
);
|
|
|
|
const missingSmsPnvsDiagnostics = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
evidence.checks = evidence.checks.filter(item => item.id !== 'auth.sms-pnvs-diagnostics');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(missingSmsPnvsDiagnostics.status, 0, 'missing PNVS diagnostics should fail launch gate');
|
|
assert.ok(
|
|
missingSmsPnvsDiagnostics.payload.checks?.some(item => item.id === 'check.auth.sms-pnvs-diagnostics' && item.status === 'blocker'),
|
|
'missing PNVS diagnostics should be reported as a blocker',
|
|
);
|
|
|
|
const wrongSmsPnvsDiagnostics = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'auth.sms-pnvs-diagnostics');
|
|
item.summary.env.providerMatchesPnvs = false;
|
|
item.summary.provider.provider = 'aliyun';
|
|
rewriteArtifact(tempDir, item, item.summary);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(wrongSmsPnvsDiagnostics.status, 0, 'non-PNVS diagnostics should fail launch gate');
|
|
assert.ok(
|
|
wrongSmsPnvsDiagnostics.payload.checks?.some(item => item.id === 'check.auth.sms-pnvs-diagnostics.summary' && item.status === 'blocker'),
|
|
'non-PNVS diagnostics should be reported as a blocker',
|
|
);
|
|
|
|
const wrongSmsProviderSmoke = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'auth.sms-pnvs-remote-smoke');
|
|
item.summary.provider = 'aliyun';
|
|
rewriteArtifact(tempDir, item, item.summary);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(wrongSmsProviderSmoke.status, 0, 'non-PNVS SMS smoke should fail launch gate');
|
|
assert.ok(
|
|
wrongSmsProviderSmoke.payload.checks?.some(item => item.id === 'check.auth.sms-pnvs-remote-smoke.summary' && item.status === 'blocker'),
|
|
'non-PNVS SMS smoke 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';
|
|
rewriteArtifact(tempDir, item, pocketBaseDryRunPayload(item.summary));
|
|
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 capacityCleanupMismatch = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'performance.tenant-students-100k');
|
|
const artifactPath = path.join(tempDir, item.artifact);
|
|
const payload = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
|
|
payload.cleanup.remaining.platformUsers = 1;
|
|
fs.writeFileSync(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
item.artifactSha256 = crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(capacityCleanupMismatch.status, 0, 'capacity artifact with managed rows after cleanup should fail');
|
|
assert.ok(
|
|
capacityCleanupMismatch.payload.checks?.some(item => item.id === 'check.performance.tenant-students-100k.artifact_summary' && item.status === 'blocker'),
|
|
'capacity cleanup mismatch should be reported from the artifact',
|
|
);
|
|
|
|
const tenantForeignKeyArtifactMismatch = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'db.tenant-foreign-key-audit');
|
|
const artifactPath = path.join(tempDir, item.artifact);
|
|
const payload = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
|
|
payload.data.invalidRelations = 1;
|
|
fs.writeFileSync(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
item.artifactSha256 = crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(tenantForeignKeyArtifactMismatch.status, 0, 'tenant foreign key artifact with invalid data relations should fail');
|
|
assert.ok(
|
|
tenantForeignKeyArtifactMismatch.payload.checks?.some(item => item.id === 'check.db.tenant-foreign-key-audit.artifact_summary' && item.status === 'blocker'),
|
|
'tenant foreign key mismatch must be reported from the raw artifact',
|
|
);
|
|
|
|
const corsArtifactMismatch = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'api.dynamic-tenant-cors-smoke');
|
|
const artifactPath = path.join(tempDir, item.artifact);
|
|
const payload = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
|
|
payload.unknownOriginDenied = false;
|
|
fs.writeFileSync(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
item.artifactSha256 = crypto.createHash('sha256').update(fs.readFileSync(artifactPath)).digest('hex');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(corsArtifactMismatch.status, 0, 'dynamic CORS artifact mismatch should fail');
|
|
assert.ok(
|
|
corsArtifactMismatch.payload.checks?.some(item => item.id === 'check.api.dynamic-tenant-cors-smoke.artifact_summary' && item.status === 'blocker'),
|
|
'dynamic CORS mismatch should be reported from the artifact',
|
|
);
|
|
|
|
const missingLaunchPersonaSmoke = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
evidence.checks = evidence.checks.filter(item => item.id !== 'api.launch-persona-smoke');
|
|
return evidence;
|
|
});
|
|
assert.notEqual(missingLaunchPersonaSmoke.status, 0, 'missing launch persona smoke should fail launch gate');
|
|
assert.ok(
|
|
missingLaunchPersonaSmoke.payload.checks?.some(item => item.id === 'check.api.launch-persona-smoke' && item.status === 'blocker'),
|
|
'missing launch persona smoke should be reported as a blocker',
|
|
);
|
|
|
|
const launchPersonaWithoutSvip = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'api.launch-persona-smoke');
|
|
item.summary.student.result.entitlement.isSvip = false;
|
|
rewriteArtifact(tempDir, item, item.summary);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(launchPersonaWithoutSvip.status, 0, 'launch persona smoke without SVIP should fail launch gate');
|
|
assert.ok(
|
|
launchPersonaWithoutSvip.payload.checks?.some(item => item.id === 'check.api.launch-persona-smoke.summary' && item.status === 'blocker'),
|
|
'launch persona SVIP failure should be reported as a blocker',
|
|
);
|
|
|
|
const launchPersonaLegacyAuth = runGate(tempDir => {
|
|
const evidence = createEvidence(tempDir);
|
|
const item = evidence.checks.find(check => check.id === 'api.launch-persona-smoke');
|
|
item.summary.authMode = 'legacy';
|
|
rewriteArtifact(tempDir, item, item.summary);
|
|
return evidence;
|
|
});
|
|
assert.notEqual(launchPersonaLegacyAuth.status, 0, 'launch persona smoke with legacy auth should fail launch gate');
|
|
assert.ok(
|
|
launchPersonaLegacyAuth.payload.checks?.some(item => item.id === 'check.api.launch-persona-smoke.summary' && item.status === 'blocker'),
|
|
'launch persona legacy auth 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;
|
|
rewriteArtifact(tempDir, item, jsonArtifactPayload(gateChecks.find(spec => spec.id === item.id), item.summary));
|
|
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;
|
|
rewriteArtifact(tempDir, item, jsonArtifactPayload(gateChecks.find(spec => spec.id === item.id), item.summary));
|
|
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');
|