forked from wangziqi/gongxue-base
feat: establish production SaaS foundation
This commit is contained in:
@@ -3,7 +3,16 @@ 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';
|
||||
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');
|
||||
@@ -38,23 +47,189 @@ function sampleSummary(summarySpec) {
|
||||
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`;
|
||||
fs.writeFileSync(path.join(tempDir, artifact), `[PASS] ${spec.id}\n`, 'utf8');
|
||||
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,
|
||||
summary: sampleSummary(spec.summary),
|
||||
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',
|
||||
@@ -66,6 +241,7 @@ function createEvidence(tempDir, overrides = {}) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
environment: 'production',
|
||||
releaseTargets: ['h5'],
|
||||
commit: '52cef9fabcd1234567890abcdef1234567890abc',
|
||||
target: {
|
||||
apiBaseUrl: 'https://api.gongxue100.com',
|
||||
@@ -83,6 +259,9 @@ 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 || [])], {
|
||||
@@ -95,6 +274,8 @@ function runGate(evidence, options = {}) {
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -103,10 +284,472 @@ function runGate(evidence, options = {}) {
|
||||
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');
|
||||
@@ -146,6 +789,7 @@ const wrongSmsPnvsDiagnostics = runGate(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');
|
||||
@@ -158,6 +802,7 @@ 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');
|
||||
@@ -170,6 +815,7 @@ 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');
|
||||
@@ -224,6 +870,54 @@ assert.ok(
|
||||
'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');
|
||||
@@ -239,6 +933,7 @@ 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');
|
||||
@@ -251,6 +946,7 @@ 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');
|
||||
@@ -296,6 +992,7 @@ 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');
|
||||
@@ -308,6 +1005,7 @@ 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');
|
||||
|
||||
Reference in New Issue
Block a user