feat(P0-A): 整合第一版并冻结最终发布 #1
+3
-1
@@ -112,7 +112,9 @@
|
||||
"review:wp7-02": "node scripts/record-wp7-02-manual-review.mjs",
|
||||
"test:wp7-03": "node scripts/run-wp7-03-validation.mjs --phase green",
|
||||
"test:wp7-03:red": "node scripts/run-wp7-03-validation.mjs --phase red",
|
||||
"test:wp7-04": "node scripts/run-wp7-04-validation.mjs"
|
||||
"test:wp7-04": "node scripts/run-wp7-04-validation.mjs",
|
||||
"test:wp7-05": "node scripts/run-wp7-05-validation.mjs",
|
||||
"test:wp7-05:unit": "node --test tests/package/wp7-05-ui-gate.test.mjs tests/package/wp7-05-coverage.test.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.62.0",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import path from 'node:path';
|
||||
import { REQUIRED_COVERAGE_UNITS } from './wp7-05-ui-gate.mjs';
|
||||
|
||||
const ABSOLUTE_PATH = /^(?:[A-Za-z]:[\\/]|[\\/]{2}|\\\\)/;
|
||||
|
||||
function assertSafeRelative(value, field) {
|
||||
if (typeof value !== 'string' || !value || ABSOLUTE_PATH.test(value) || path.isAbsolute(value)) {
|
||||
throw new Error(`WP7_05_UNSAFE_${field}`);
|
||||
}
|
||||
const normalized = value.replaceAll('\\', '/');
|
||||
if (normalized.split('/').includes('..')) throw new Error(`WP7_05_UNSAFE_${field}`);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function buildCoverageEvidence({ runId, candidateSha256, coverageUnits, viewports }) {
|
||||
if (!runId || !/^[A-Za-z0-9._-]+$/.test(runId)) throw new Error('WP7_05_INVALID_RUN_ID');
|
||||
if (!/^[A-Fa-f0-9]{64}$/.test(candidateSha256 ?? '')) throw new Error('WP7_05_INVALID_CANDIDATE_HASH');
|
||||
if (!Array.isArray(coverageUnits)) throw new Error('WP7_05_COVERAGE_UNITS_REQUIRED');
|
||||
|
||||
const byPage = new Map();
|
||||
for (const unit of coverageUnits) {
|
||||
if (!REQUIRED_COVERAGE_UNITS.includes(unit.page_id)) throw new Error('WP7_05_UNKNOWN_PAGE');
|
||||
if (byPage.has(unit.page_id)) throw new Error('WP7_05_DUPLICATE_PAGE');
|
||||
if (!Array.isArray(unit.states) || unit.states.length === 0) throw new Error('WP7_05_STATES_REQUIRED');
|
||||
const states = unit.states.map((state) => ({
|
||||
state: assertSafeRelative(state.state, 'STATE'),
|
||||
screenshot_100pct: assertSafeRelative(state.screenshot_100pct, 'SCREENSHOT'),
|
||||
screenshot_200pct: assertSafeRelative(state.screenshot_200pct, 'SCREENSHOT'),
|
||||
trace: assertSafeRelative(state.trace, 'TRACE'),
|
||||
}));
|
||||
byPage.set(unit.page_id, { page_id: unit.page_id, states });
|
||||
}
|
||||
const missing = REQUIRED_COVERAGE_UNITS.filter((page) => !byPage.has(page));
|
||||
if (missing.length) throw new Error(`WP7_05_MISSING_PAGES:${missing.join(',')}`);
|
||||
if (!Array.isArray(viewports) || viewports.length !== 2) throw new Error('WP7_05_VIEWPORTS_REQUIRED');
|
||||
|
||||
return {
|
||||
schema_version: '1.0',
|
||||
task: 'TASK-WP7-05',
|
||||
run_id: runId,
|
||||
candidate_sha256: candidateSha256.toUpperCase(),
|
||||
viewports,
|
||||
coverage_units: REQUIRED_COVERAGE_UNITS.map((page) => byPage.get(page)),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
export const REQUIRED_COVERAGE_UNITS = Object.freeze([
|
||||
'support-gate', 'user-auth', 'workspace', 'current-task', 'projects',
|
||||
'project-detail', 'editor', 'export', 'credits', 'settings',
|
||||
'preview-user-variant', 'admin-auth', 'admin-overview', 'admin-users',
|
||||
'admin-invites', 'admin-models', 'admin-assets', 'admin-preview',
|
||||
'admin-generations', 'admin-services-storage', 'admin-audit', 'system-ui',
|
||||
]);
|
||||
|
||||
const REQUIRED_VIEWPORTS = Object.freeze([
|
||||
{ width: 1920, height: 1080, deviceScaleFactor: 1, zoom: 100 },
|
||||
{ width: 1920, height: 1080, deviceScaleFactor: 1, zoom: 200 },
|
||||
]);
|
||||
|
||||
function blocked(code, details = {}) {
|
||||
return { status: 'externally_blocked', code, ...details };
|
||||
}
|
||||
|
||||
export function loadCandidateRecord(path) {
|
||||
if (!path || !fs.existsSync(path)) return blocked('candidate_record_missing');
|
||||
try {
|
||||
const record = JSON.parse(fs.readFileSync(path, 'utf8'));
|
||||
if (!Array.isArray(record.browsers) || record.browsers.length !== 2) {
|
||||
return blocked('candidate_browser_record_incomplete');
|
||||
}
|
||||
const brands = new Set(record.browsers.map((browser) => browser.brand));
|
||||
if (brands.size !== 2 || !brands.has('Google Chrome') || !brands.has('Microsoft Edge')) {
|
||||
return blocked('candidate_browser_pair_invalid');
|
||||
}
|
||||
if (record.windows?.build == null || !record.candidate_package?.sha256 || !record.candidate_package?.fixed_port) {
|
||||
return blocked('candidate_identity_incomplete');
|
||||
}
|
||||
if (record.browsers.some((browser) => !browser.full_version || !browser.major)) {
|
||||
return blocked('candidate_full_version_missing');
|
||||
}
|
||||
return { status: 'ready', record };
|
||||
} catch {
|
||||
return blocked('candidate_record_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
export function validateCoverageEvidence(evidence) {
|
||||
if (!evidence || !Array.isArray(evidence.coverage_units)) {
|
||||
return blocked('coverage_evidence_missing');
|
||||
}
|
||||
const actual = new Set(evidence.coverage_units.map((unit) => unit.page_id));
|
||||
const missing = REQUIRED_COVERAGE_UNITS.filter((unit) => !actual.has(unit));
|
||||
if (missing.length) return blocked('coverage_units_incomplete', { missing });
|
||||
const missingStates = evidence.coverage_units
|
||||
.filter((unit) => REQUIRED_COVERAGE_UNITS.includes(unit.page_id))
|
||||
.filter((unit) => !Array.isArray(unit.states) || unit.states.length === 0)
|
||||
.map((unit) => unit.page_id);
|
||||
if (missingStates.length) return blocked('coverage_states_incomplete', { missingStates });
|
||||
const viewportKeys = new Set((evidence.viewports ?? []).map((viewport) => JSON.stringify(viewport)));
|
||||
const missingViewports = REQUIRED_VIEWPORTS.filter((viewport) => !viewportKeys.has(JSON.stringify(viewport)));
|
||||
if (missingViewports.length) return blocked('candidate_viewports_incomplete', { missingViewports });
|
||||
return { status: 'ready' };
|
||||
}
|
||||
|
||||
export function runWp705Gate({ candidatePath, evidence, dependencies = {} }) {
|
||||
const candidate = loadCandidateRecord(candidatePath);
|
||||
if (candidate.status !== 'ready') return candidate;
|
||||
const coverage = validateCoverageEvidence(evidence);
|
||||
if (coverage.status !== 'ready') return coverage;
|
||||
const externalBlockers = Object.entries(dependencies)
|
||||
.filter(([, status]) => status === 'externally_blocked')
|
||||
.map(([task]) => task);
|
||||
if (externalBlockers.length) return blocked('upstream_external_blocked', { externalBlockers });
|
||||
return { status: 'ready_for_execution' };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { runWp705Gate } from './lib/wp7-05-ui-gate.mjs';
|
||||
|
||||
const result = runWp705Gate({
|
||||
candidatePath: process.env.WP7_01_CANDIDATE_RECORD,
|
||||
evidence: null,
|
||||
dependencies: {
|
||||
'TASK-WP7-03': 'externally_blocked',
|
||||
'TASK-WP7-04': 'externally_blocked',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(JSON.stringify({ task: 'TASK-WP7-05', ...result }));
|
||||
process.exitCode = result.status === 'ready_for_execution' ? 0 : 3;
|
||||
@@ -0,0 +1,41 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildCoverageEvidence } from '../../scripts/lib/wp7-05-coverage.mjs';
|
||||
import { REQUIRED_COVERAGE_UNITS } from '../../scripts/lib/wp7-05-ui-gate.mjs';
|
||||
|
||||
const viewports = [
|
||||
{ width: 1920, height: 1080, deviceScaleFactor: 1, zoom: 100 },
|
||||
{ width: 1920, height: 1080, deviceScaleFactor: 1, zoom: 200 },
|
||||
];
|
||||
|
||||
function completeUnits() {
|
||||
return REQUIRED_COVERAGE_UNITS.map((page_id) => ({
|
||||
page_id,
|
||||
states: [{
|
||||
state: 'normal',
|
||||
screenshot_100pct: `ui/${page_id}/normal/100pct.png`,
|
||||
screenshot_200pct: `ui/${page_id}/normal/200pct.png`,
|
||||
trace: `ui/${page_id}/normal/trace.zip`,
|
||||
}],
|
||||
}));
|
||||
}
|
||||
|
||||
test('builds ordered, sanitized evidence for all 22 coverage units', () => {
|
||||
const evidence = buildCoverageEvidence({
|
||||
runId: 'wp7-05-red-001',
|
||||
candidateSha256: 'a'.repeat(64),
|
||||
coverageUnits: completeUnits(),
|
||||
viewports,
|
||||
});
|
||||
assert.equal(evidence.coverage_units.length, 22);
|
||||
assert.equal(evidence.coverage_units[0].page_id, 'support-gate');
|
||||
assert.equal(evidence.candidate_sha256, 'A'.repeat(64));
|
||||
});
|
||||
|
||||
test('rejects absolute evidence paths and incomplete page state', () => {
|
||||
const units = completeUnits();
|
||||
units[0].states[0].trace = 'C:\\secret\\trace.zip';
|
||||
assert.throws(() => buildCoverageEvidence({
|
||||
runId: 'wp7-05-red-001', candidateSha256: 'a'.repeat(64), coverageUnits: units, viewports,
|
||||
}), /WP7_05_UNSAFE_TRACE/);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { loadCandidateRecord, validateCoverageEvidence, runWp705Gate, REQUIRED_COVERAGE_UNITS } from '../../scripts/lib/wp7-05-ui-gate.mjs';
|
||||
|
||||
test('WP7-05 accepts the WP7-01 candidate record schema', () => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'wp7-05-'));
|
||||
const file = path.join(directory, 'release-candidate.json');
|
||||
fs.writeFileSync(file, JSON.stringify({
|
||||
browsers: [
|
||||
{ brand: 'Google Chrome', full_version: '150.0.0', major: 150 },
|
||||
{ brand: 'Microsoft Edge', full_version: '151.0.0', major: 151 },
|
||||
],
|
||||
windows: { build: '26200.8875' },
|
||||
candidate_package: { fixed_port: 43121, sha256: 'A'.repeat(64) },
|
||||
}));
|
||||
assert.equal(loadCandidateRecord(file).status, 'ready');
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('WP7-05 requires all 22 coverage units and both exact candidate viewports', () => {
|
||||
const result = validateCoverageEvidence({ coverage_units: [], viewports: [] });
|
||||
assert.equal(result.status, 'externally_blocked');
|
||||
assert.equal(result.code, 'coverage_units_incomplete');
|
||||
assert.equal(result.missing.length, REQUIRED_COVERAGE_UNITS.length);
|
||||
});
|
||||
|
||||
test('WP7-05 requires state evidence for every listed coverage unit', () => {
|
||||
const result = validateCoverageEvidence({
|
||||
coverage_units: REQUIRED_COVERAGE_UNITS.map((page_id) => ({ page_id, states: [] })),
|
||||
viewports: [],
|
||||
});
|
||||
assert.equal(result.code, 'coverage_states_incomplete');
|
||||
assert.equal(result.missingStates.length, REQUIRED_COVERAGE_UNITS.length);
|
||||
});
|
||||
|
||||
test('WP7-05 preserves upstream external blockers instead of declaring Green', () => {
|
||||
const result = runWp705Gate({
|
||||
candidatePath: 'missing-release-candidate.json',
|
||||
evidence: null,
|
||||
dependencies: { 'TASK-WP7-03': 'externally_blocked' },
|
||||
});
|
||||
assert.equal(result.status, 'externally_blocked');
|
||||
assert.equal(result.code, 'candidate_record_missing');
|
||||
});
|
||||
Reference in New Issue
Block a user