Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
693fa117b7 | ||
|
|
08f3cccae4 | ||
|
|
a22b1f19e9 | ||
|
|
194b59d4a5 | ||
|
|
0f03b12f64 | ||
|
|
ad86b4ddcc | ||
|
|
08e9c39e49 | ||
|
|
ffd1643848 | ||
|
|
55646ba1b4 | ||
|
|
8abf1397a6 | ||
|
|
e0e101ef28 | ||
|
|
33f87f8db3 |
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"appVersion": "0.0.0",
|
||||||
|
"browsers": [
|
||||||
|
{
|
||||||
|
"brand": "Google Chrome",
|
||||||
|
"fullVersion": "150.0.7871.187"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Microsoft Edge",
|
||||||
|
"fullVersion": "151.0.4129.59"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildCommit": "08f3cccae4a1e75e2f2292eef14611313523916d",
|
||||||
|
"deferredExternalTasks": [
|
||||||
|
"TASK-WP7-03",
|
||||||
|
"TASK-WP7-04"
|
||||||
|
],
|
||||||
|
"finalRelease": true,
|
||||||
|
"fixedPort": 43121,
|
||||||
|
"frozenFromCommit": "08e9c39e49d68f8642d5acfe22b0fdb40a3a08fa",
|
||||||
|
"recordedAt": "2026-08-04T15:20:54.271Z",
|
||||||
|
"releaseStatus": "first_version_internal",
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"windows": {
|
||||||
|
"arch": "x64",
|
||||||
|
"build": "26200.8875",
|
||||||
|
"displayVersion": "25H2"
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-1
@@ -112,7 +112,13 @@
|
|||||||
"review:wp7-02": "node scripts/record-wp7-02-manual-review.mjs",
|
"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": "node scripts/run-wp7-03-validation.mjs --phase green",
|
||||||
"test:wp7-03:red": "node scripts/run-wp7-03-validation.mjs --phase red",
|
"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",
|
||||||
|
"test:wp7-06": "node scripts/run-wp7-06-validation.mjs",
|
||||||
|
"test:wp7-06:unit": "node --test tests/package/wp7-06-prefreeze.test.mjs",
|
||||||
|
"test:wp7-07": "node scripts/run-wp7-07-validation.mjs",
|
||||||
|
"test:wp7-07:unit": "node --test tests/package/wp7-07-final-release.test.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.62.0",
|
"@playwright/test": "1.62.0",
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ function copyApplication(source, destination, runtimeDependencies) {
|
|||||||
|
|
||||||
function buildArtifacts(stagingRoot) {
|
function buildArtifacts(stagingRoot) {
|
||||||
debug("build workspace artifacts");
|
debug("build workspace artifacts");
|
||||||
run("pnpm", ["--filter", "@dada/shared-contracts", "build"]);
|
run("pnpm", ["build:workspace-packages"]);
|
||||||
run("pnpm", ["--filter", "@dada/web", "build"]);
|
run("pnpm", ["--filter", "@dada/web", "build"]);
|
||||||
run("pnpm", ["--filter", "@dada/api", "build"]);
|
run("pnpm", ["--filter", "@dada/api", "build"]);
|
||||||
run("pnpm", ["--filter", "@dada/worker", "build"]);
|
run("pnpm", ["--filter", "@dada/worker", "build"]);
|
||||||
@@ -208,7 +208,7 @@ async function waitForHealth(child) {
|
|||||||
throw new Error("Packaged API did not become healthy on fixed port 43121.", { cause: lastError });
|
throw new Error("Packaged API did not become healthy on fixed port 43121.", { cause: lastError });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyExtractedPackage(zipPath, packageName) {
|
export async function verifyExtractedPackage(zipPath, packageName, expectedSupport) {
|
||||||
const extractRoot = mkdtempSync(join(tmpdir(), "dada-wp0-09-"));
|
const extractRoot = mkdtempSync(join(tmpdir(), "dada-wp0-09-"));
|
||||||
try {
|
try {
|
||||||
const escapedZip = zipPath.replaceAll("'", "''");
|
const escapedZip = zipPath.replaceAll("'", "''");
|
||||||
@@ -235,21 +235,37 @@ async function verifyExtractedPackage(zipPath, packageName) {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const health = await waitForHealth(api);
|
const health = await waitForHealth(api);
|
||||||
|
const brands = [
|
||||||
|
{ brand: "Not_A Brand", version: "99" },
|
||||||
|
{ brand: "Chromium", version: String(expectedSupport.major) },
|
||||||
|
{ brand: expectedSupport.brand, version: String(expectedSupport.major) },
|
||||||
|
];
|
||||||
|
const fullVersionList = [
|
||||||
|
{ brand: "Not_A Brand", version: "99.0.0.0" },
|
||||||
|
{ brand: "Chromium", version: expectedSupport.fullVersion },
|
||||||
|
{ brand: expectedSupport.brand, version: expectedSupport.fullVersion },
|
||||||
|
];
|
||||||
|
const serializeBrands = (values) => values.map(({ brand, version }) => `"${brand}";v="${version}"`).join(", ");
|
||||||
const releaseGate = await fetch(`http://127.0.0.1:${fixedPort}/api/v1/support/check`, {
|
const releaseGate = await fetch(`http://127.0.0.1:${fixedPort}/api/v1/support/check`, {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
brands: [{ brand: "Google Chrome", version: "150" }],
|
brands,
|
||||||
full_version_list: [{ brand: "Google Chrome", version: "150.0.0.0" }],
|
full_version_list: fullVersionList,
|
||||||
platform: "Windows",
|
platform: "Windows",
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"sec-ch-ua": '"Google Chrome";v="150"',
|
host: `127.0.0.1:${fixedPort}`,
|
||||||
"sec-ch-ua-full-version-list": '"Google Chrome";v="150.0.0.0"',
|
origin: `http://127.0.0.1:${fixedPort}`,
|
||||||
|
"sec-ch-ua": serializeBrands(brands),
|
||||||
|
"sec-ch-ua-full-version-list": serializeBrands(fullVersionList),
|
||||||
"sec-ch-ua-platform": '"Windows"',
|
"sec-ch-ua-platform": '"Windows"',
|
||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
if (releaseGate.status !== 426) throw new Error(`Candidate RELEASE.json unexpectedly passed with ${releaseGate.status}.`);
|
if (releaseGate.status !== expectedSupport.statusCode) {
|
||||||
|
const responseBody = await releaseGate.text();
|
||||||
|
throw new Error(`Packaged RELEASE.json support gate returned ${releaseGate.status}; expected ${expectedSupport.statusCode}: ${responseBody}`);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
api: { executable: "runtime/node.exe", health, pid: api.pid, release_gate: { status_code: releaseGate.status }, status: "passed" },
|
api: { executable: "runtime/node.exe", health, pid: api.pid, release_gate: { status_code: releaseGate.status }, status: "passed" },
|
||||||
native,
|
native,
|
||||||
@@ -291,7 +307,7 @@ function scanPackage(packageDirectory) {
|
|||||||
return { disallowed_matches: disallowedMatches, reparse_points: reparsePoints, scanned_files: files.length, status: disallowedMatches.length === 0 && reparsePoints.length === 0 ? "passed" : "failed" };
|
return { disallowed_matches: disallowedMatches, reparse_points: reparsePoints, scanned_files: files.length, status: disallowedMatches.length === 0 && reparsePoints.length === 0 ? "passed" : "failed" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildAndValidatePortablePackage({ evidenceDirectory, outputRoot }) {
|
export async function buildAndValidatePortablePackage({ evidenceDirectory, outputRoot, releaseRecord }) {
|
||||||
if (process.platform !== frozenRuntime.os || process.arch !== frozenRuntime.arch || process.version.slice(1) !== frozenRuntime.node) {
|
if (process.platform !== frozenRuntime.os || process.arch !== frozenRuntime.arch || process.version.slice(1) !== frozenRuntime.node) {
|
||||||
throw new Error("Portable package build requires frozen Node 24.13.0 on win-x64.");
|
throw new Error("Portable package build requires frozen Node 24.13.0 on win-x64.");
|
||||||
}
|
}
|
||||||
@@ -351,7 +367,8 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
|
|||||||
writeJson(join(packageDirectory, "LICENSES", "third-party.json"), { api: apiDependencies, runtime: { node: frozenRuntime.node }, schema_version: "1.0", worker: workerDependencies });
|
writeJson(join(packageDirectory, "LICENSES", "third-party.json"), { api: apiDependencies, runtime: { node: frozenRuntime.node }, schema_version: "1.0", worker: workerDependencies });
|
||||||
|
|
||||||
const commit = run("git", ["rev-parse", "HEAD"]);
|
const commit = run("git", ["rev-parse", "HEAD"]);
|
||||||
writeJson(join(packageDirectory, "RELEASE.json"), {
|
const finalRelease = releaseRecord !== undefined;
|
||||||
|
writeJson(join(packageDirectory, "RELEASE.json"), releaseRecord ?? {
|
||||||
app_version: appVersion,
|
app_version: appVersion,
|
||||||
browsers: [],
|
browsers: [],
|
||||||
build_commit: commit,
|
build_commit: commit,
|
||||||
@@ -360,16 +377,20 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
|
|||||||
windows_build: null,
|
windows_build: null,
|
||||||
});
|
});
|
||||||
writeFileSync(join(packageDirectory, "START-HERE.txt"), [
|
writeFileSync(join(packageDirectory, "START-HERE.txt"), [
|
||||||
"Dada P0-A candidate package",
|
finalRelease ? "Dada P0-A first-version portable package" : "Dada P0-A candidate package",
|
||||||
"",
|
"",
|
||||||
"This candidate is unsigned and is not a final P0-A release.",
|
finalRelease
|
||||||
|
? "This unsigned first-version package passed the local P0-A release gates recorded in RELEASE.json."
|
||||||
|
: "This candidate is unsigned and is not a final P0-A release.",
|
||||||
"Verify the adjacent SHA-256 file before first launch.",
|
"Verify the adjacent SHA-256 file before first launch.",
|
||||||
"Windows SmartScreen may warn on first launch because the executable is unsigned.",
|
"Windows SmartScreen may warn on first launch because the executable is unsigned.",
|
||||||
"For an antivirus alert, compare the package hash with the Gitea build record.",
|
"For an antivirus alert, compare the package hash with the Gitea build record.",
|
||||||
"Do not disable antivirus protection, add broad exclusions, or skip hash verification.",
|
"Do not disable antivirus protection, add broad exclusions, or skip hash verification.",
|
||||||
"To update, exit Dada from the tray and replace the complete program directory.",
|
"To update, exit Dada from the tray and replace the complete program directory.",
|
||||||
"Dada uses 127.0.0.1:43121 and does not support LAN or remote access.",
|
"Dada uses 127.0.0.1:43121 and does not support LAN or remote access.",
|
||||||
"A final RELEASE.json is created only after WP-7 acceptance.",
|
finalRelease
|
||||||
|
? "Resend and Amap real-provider validation remain explicitly deferred and are not recorded as passed."
|
||||||
|
: "A final RELEASE.json is created only after WP-7 acceptance.",
|
||||||
"",
|
"",
|
||||||
].join("\r\n"));
|
].join("\r\n"));
|
||||||
|
|
||||||
@@ -381,7 +402,18 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
|
|||||||
const zipSha256 = fileSha256(zipPath);
|
const zipSha256 = fileSha256(zipPath);
|
||||||
const shaPath = `${zipPath}.sha256`;
|
const shaPath = `${zipPath}.sha256`;
|
||||||
writeFileSync(shaPath, `${zipSha256} ${basename(zipPath)}\n`);
|
writeFileSync(shaPath, `${zipSha256} ${basename(zipPath)}\n`);
|
||||||
const processTree = await verifyExtractedPackage(zipPath, packageName);
|
const supportBrowser = finalRelease ? releaseRecord.browsers[0] : undefined;
|
||||||
|
const processTree = await verifyExtractedPackage(zipPath, packageName, finalRelease ? {
|
||||||
|
brand: supportBrowser.brand,
|
||||||
|
fullVersion: supportBrowser.fullVersion,
|
||||||
|
major: Number.parseInt(supportBrowser.fullVersion.split(".")[0], 10),
|
||||||
|
statusCode: 200,
|
||||||
|
} : {
|
||||||
|
brand: "Google Chrome",
|
||||||
|
fullVersion: "150.0.0.0",
|
||||||
|
major: 150,
|
||||||
|
statusCode: 426,
|
||||||
|
});
|
||||||
const fileEntries = listFiles(packageDirectory).files.map((path) => ({
|
const fileEntries = listFiles(packageDirectory).files.map((path) => ({
|
||||||
path: relative(packageDirectory, path).replaceAll("\\", "/"),
|
path: relative(packageDirectory, path).replaceAll("\\", "/"),
|
||||||
sha256: fileSha256(path),
|
sha256: fileSha256(path),
|
||||||
@@ -392,7 +424,7 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
|
|||||||
files: fileEntries,
|
files: fileEntries,
|
||||||
fixed_port: fixedPort,
|
fixed_port: fixedPort,
|
||||||
package_name: packageName,
|
package_name: packageName,
|
||||||
release_status: "candidate_unvalidated",
|
release_status: finalRelease ? releaseRecord.releaseStatus : "candidate_unvalidated",
|
||||||
schema_version: "1.0",
|
schema_version: "1.0",
|
||||||
zip_sha256: zipSha256,
|
zip_sha256: zipSha256,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,62 @@
|
|||||||
|
const EXPECTED_TRACE_SUMMARY = Object.freeze({
|
||||||
|
acceptanceCriteria: 52,
|
||||||
|
errorCategories: 9,
|
||||||
|
featureModules: 13,
|
||||||
|
parentFamilies: 89,
|
||||||
|
penProductFrames: 18,
|
||||||
|
productContracts: 19,
|
||||||
|
requirements: 109,
|
||||||
|
tasks: 52,
|
||||||
|
testCases: 117,
|
||||||
|
uiPages: 22,
|
||||||
|
});
|
||||||
|
|
||||||
|
const REQUIRED_UPSTREAM = Object.freeze({
|
||||||
|
"TASK-WP7-01": "passed",
|
||||||
|
"TASK-WP7-02": "passed",
|
||||||
|
"TASK-WP7-03": "deferred_nonblocking_first_version",
|
||||||
|
"TASK-WP7-04": "deferred_nonblocking_first_version",
|
||||||
|
"TASK-WP7-05": "passed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const shaPattern = /^[0-9a-f]{40}$/i;
|
||||||
|
|
||||||
|
export function buildWp706PrefreezeReport({ currentCommit, releaseExists, trace, upstream }) {
|
||||||
|
if (releaseExists) throw new Error("WP7_06_RELEASE_WRITTEN_PREMATURELY");
|
||||||
|
if (!shaPattern.test(currentCommit ?? "")) throw new Error("WP7_06_CURRENT_COMMIT_INVALID");
|
||||||
|
if (trace?.status !== "passed" || !Array.isArray(trace?.errors) || trace.errors.length > 0) {
|
||||||
|
throw new Error("WP7_06_TRACE_VALIDATION_FAILED");
|
||||||
|
}
|
||||||
|
for (const [key, expected] of Object.entries(EXPECTED_TRACE_SUMMARY)) {
|
||||||
|
if (trace.summary?.[key] !== expected) throw new Error(`WP7_06_TRACE_COUNT_MISMATCH:${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [taskId, expectedStatus] of Object.entries(REQUIRED_UPSTREAM)) {
|
||||||
|
const item = upstream?.[taskId];
|
||||||
|
if (!item || !shaPattern.test(item.head ?? "")) throw new Error(`WP7_06_UPSTREAM_HEAD_INVALID:${taskId}`);
|
||||||
|
if (item.merged !== true) throw new Error(`WP7_06_UPSTREAM_NOT_MERGED:${taskId}`);
|
||||||
|
if (item.status !== expectedStatus) throw new Error(`WP7_06_UNSUPPORTED_STATUS:${taskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: "1.0",
|
||||||
|
task_id: "TASK-WP7-06",
|
||||||
|
status: "passed",
|
||||||
|
current_commit: currentCommit.toLowerCase(),
|
||||||
|
release_json_written: false,
|
||||||
|
trace_summary: { ...EXPECTED_TRACE_SUMMARY },
|
||||||
|
upstream: Object.fromEntries(Object.entries(REQUIRED_UPSTREAM).map(([taskId]) => [taskId, {
|
||||||
|
branch: upstream[taskId].branch,
|
||||||
|
head: upstream[taskId].head.toLowerCase(),
|
||||||
|
merged: true,
|
||||||
|
status: upstream[taskId].status,
|
||||||
|
}])),
|
||||||
|
deferred_external_tasks: Object.entries(REQUIRED_UPSTREAM)
|
||||||
|
.filter(([, status]) => status === "deferred_nonblocking_first_version")
|
||||||
|
.map(([taskId]) => taskId),
|
||||||
|
final_release_allowed: false,
|
||||||
|
next_task: "TASK-WP7-07",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { EXPECTED_TRACE_SUMMARY, REQUIRED_UPSTREAM };
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||||
|
import { extname, join, relative } from "node:path";
|
||||||
|
|
||||||
|
const SHA40 = /^[a-f0-9]{40}$/i;
|
||||||
|
const SHA64 = /^[a-f0-9]{64}$/i;
|
||||||
|
const VERSION = /^[1-9][0-9]*\.[0-9]+\.[0-9]+\.[0-9]+$/;
|
||||||
|
const ABSOLUTE_PATH = /(?:[A-Za-z]:[\\/](?:Users|Documents)[\\/][^\\/"'\s]+[\\/]|\/Users\/[^/"'\s]+\/|\/home\/[^/"'\s]+\/)/;
|
||||||
|
const CREDENTIAL = /\b(?:sk|key)-[A-Za-z0-9_-]{16,}\b|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i;
|
||||||
|
const TEXT_EXTENSIONS = new Set([".cjs", ".cs", ".css", ".html", ".js", ".json", ".mjs", ".ts", ".tsx", ".txt", ".xml", ".yaml", ".yml"]);
|
||||||
|
|
||||||
|
export const DEFERRED_EXTERNAL_TASKS = Object.freeze(["TASK-WP7-03", "TASK-WP7-04"]);
|
||||||
|
|
||||||
|
export function buildFinalReleaseRecord({ appVersion, browsers, buildCommit, frozenFromCommit, recordedAt, windows }) {
|
||||||
|
const record = {
|
||||||
|
appVersion,
|
||||||
|
browsers: browsers.map(({ brand, fullVersion }) => ({ brand, fullVersion })),
|
||||||
|
buildCommit: buildCommit.toLowerCase(),
|
||||||
|
deferredExternalTasks: [...DEFERRED_EXTERNAL_TASKS],
|
||||||
|
finalRelease: true,
|
||||||
|
fixedPort: 43121,
|
||||||
|
frozenFromCommit: frozenFromCommit.toLowerCase(),
|
||||||
|
recordedAt,
|
||||||
|
releaseStatus: "first_version_internal",
|
||||||
|
schemaVersion: "1.0",
|
||||||
|
windows: { arch: windows.arch, build: windows.build, displayVersion: windows.displayVersion },
|
||||||
|
};
|
||||||
|
return validateFinalReleaseRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateFinalReleaseRecord(record) {
|
||||||
|
const errors = [];
|
||||||
|
if (record?.schemaVersion !== "1.0") errors.push("schemaVersion");
|
||||||
|
if (record?.releaseStatus !== "first_version_internal") errors.push("releaseStatus");
|
||||||
|
if (record?.finalRelease !== true) errors.push("finalRelease");
|
||||||
|
if (record?.fixedPort !== 43121) errors.push("fixedPort");
|
||||||
|
if (!SHA40.test(record?.buildCommit ?? "")) errors.push("buildCommit");
|
||||||
|
if (!SHA40.test(record?.frozenFromCommit ?? "")) errors.push("frozenFromCommit");
|
||||||
|
if (!Number.isFinite(Date.parse(record?.recordedAt ?? ""))) errors.push("recordedAt");
|
||||||
|
if (!Array.isArray(record?.deferredExternalTasks) || record.deferredExternalTasks.join("|") !== DEFERRED_EXTERNAL_TASKS.join("|")) errors.push("deferredExternalTasks");
|
||||||
|
if (record?.windows?.arch !== "x64" || !/^\d+\.\d+$/.test(record?.windows?.build ?? "")) errors.push("windows");
|
||||||
|
if (!Array.isArray(record?.browsers) || record.browsers.length !== 2) {
|
||||||
|
errors.push("browsers");
|
||||||
|
} else {
|
||||||
|
const brands = record.browsers.map(({ brand }) => brand).sort();
|
||||||
|
if (brands.join("|") !== "Google Chrome|Microsoft Edge") errors.push("browserBrands");
|
||||||
|
for (const browser of record.browsers) {
|
||||||
|
if (!VERSION.test(browser.fullVersion ?? "")) errors.push(`${browser.brand}.fullVersion`);
|
||||||
|
if ("path" in browser || "executablePath" in browser || "executableSha256" in browser) errors.push(`${browser.brand}.privateMetadata`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const serialized = JSON.stringify(record);
|
||||||
|
if (ABSOLUTE_PATH.test(serialized) || CREDENTIAL.test(serialized)) errors.push("sensitiveValue");
|
||||||
|
if (errors.length > 0) throw new Error(`WP7_07_RELEASE_INVALID:${[...new Set(errors)].join(",")}`);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha256File(path) {
|
||||||
|
return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scanReleaseFiles({ roots, allowedFixturePaths = [] }) {
|
||||||
|
const allowed = new Set(allowedFixturePaths.map((value) => value.replaceAll("\\", "/")));
|
||||||
|
const findings = [];
|
||||||
|
let scannedFiles = 0;
|
||||||
|
function visit(root, current = root) {
|
||||||
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
||||||
|
if ([".git", ".pnpm-store", "node_modules", "bin", "obj"].includes(entry.name)) continue;
|
||||||
|
const path = join(current, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
visit(root, path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile()) continue;
|
||||||
|
scannedFiles += 1;
|
||||||
|
if (!TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue;
|
||||||
|
const logicalPath = relative(root, path).replaceAll("\\", "/");
|
||||||
|
const content = readFileSync(path, "utf8");
|
||||||
|
if (!allowed.has(logicalPath) && ABSOLUTE_PATH.test(content)) findings.push({ path: logicalPath, rule: "absolute_user_path" });
|
||||||
|
if (!allowed.has(logicalPath) && CREDENTIAL.test(content)) findings.push({ path: logicalPath, rule: "credential_shape" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const root of roots) {
|
||||||
|
if (!statSync(root).isDirectory()) throw new Error(`WP7_07_SCAN_ROOT_INVALID:${root}`);
|
||||||
|
visit(root);
|
||||||
|
}
|
||||||
|
return { findings, scanned_files: scannedFiles, status: findings.length === 0 ? "passed" : "failed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateFinalEvidence({ packageManifest, release, releaseSha256, scan }) {
|
||||||
|
validateFinalReleaseRecord(release);
|
||||||
|
if (!SHA64.test(releaseSha256 ?? "")) throw new Error("WP7_07_RELEASE_HASH_INVALID");
|
||||||
|
if (packageManifest?.release_status !== release.releaseStatus || !SHA64.test(packageManifest?.zip_sha256 ?? "")) throw new Error("WP7_07_PACKAGE_MANIFEST_INVALID");
|
||||||
|
if (scan?.status !== "passed" || scan.findings?.length !== 0) throw new Error("WP7_07_LEAK_SCAN_FAILED");
|
||||||
|
return { release_sha256: releaseSha256.toUpperCase(), status: "passed", zip_sha256: packageManifest.zip_sha256.toUpperCase() };
|
||||||
|
}
|
||||||
@@ -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,108 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
import { validateTddTrace } from "./lib/tdd-trace.mjs";
|
||||||
|
import { buildWp706PrefreezeReport, REQUIRED_UPSTREAM } from "./lib/wp7-06-prefreeze.mjs";
|
||||||
|
|
||||||
|
const runId = process.env.DADA_TDD_RUN_ID ?? `wp7-06-prefreeze-${new Date().toISOString().replace(/[^0-9]/g, "")}`;
|
||||||
|
const runDirectory = resolve("artifacts", "tdd", runId);
|
||||||
|
const caseDirectory = resolve(runDirectory, "cases", "TDD-WP7-AC-001-trace-structure");
|
||||||
|
if (existsSync(runDirectory)) throw new Error(`Evidence run already exists: ${runId}`);
|
||||||
|
mkdirSync(caseDirectory, { recursive: true });
|
||||||
|
|
||||||
|
function writeJson(path, value) {
|
||||||
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function git(args) {
|
||||||
|
const result = spawnSync("git", args, { encoding: "utf8", timeout: 60_000 });
|
||||||
|
if ((result.status ?? 1) !== 0) throw new Error(`WP7_06_GIT_COMMAND_FAILED:${args[0]}`);
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand(name, command) {
|
||||||
|
const startedAt = new Date().toISOString();
|
||||||
|
const result = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", command], {
|
||||||
|
encoding: "utf8",
|
||||||
|
env: process.env,
|
||||||
|
maxBuffer: 64 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
if (result.stdout) process.stdout.write(result.stdout);
|
||||||
|
if (result.stderr) process.stderr.write(result.stderr);
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
exit_code: result.status ?? 1,
|
||||||
|
finished_at: new Date().toISOString(),
|
||||||
|
name,
|
||||||
|
started_at: startedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
runCommand("prefreeze-unit", "node --test tests/package/wp7-06-prefreeze.test.mjs"),
|
||||||
|
runCommand("tdd-trace", "pnpm validate:tdd-trace"),
|
||||||
|
];
|
||||||
|
if (commands.some((command) => command.exit_code !== 0)) {
|
||||||
|
writeJson(resolve(caseDirectory, "commands.json"), { commands, run_id: runId, schema_version: "1.0" });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trace = validateTddTrace();
|
||||||
|
const currentCommit = git(["rev-parse", "HEAD"]);
|
||||||
|
const remoteLines = git(["ls-remote", "--heads", "origin", "codex/wp7-01", "codex/wp7-02", "codex/wp7-03", "codex/wp7-04", "codex/wp7-05"])
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter(Boolean);
|
||||||
|
const remoteHeads = Object.fromEntries(remoteLines.map((line) => {
|
||||||
|
const [head, reference] = line.split(/\s+/);
|
||||||
|
return [reference.replace("refs/heads/", ""), head];
|
||||||
|
}));
|
||||||
|
const upstream = Object.fromEntries(Object.entries(REQUIRED_UPSTREAM).map(([taskId, status]) => {
|
||||||
|
const branch = taskId.replace("TASK-", "codex/").toLowerCase();
|
||||||
|
const head = remoteHeads[branch];
|
||||||
|
const ancestry = spawnSync("git", ["merge-base", "--is-ancestor", head ?? "missing", "HEAD"], { encoding: "utf8", timeout: 30_000 });
|
||||||
|
return [taskId, { branch, head, merged: ancestry.status === 0, status }];
|
||||||
|
}));
|
||||||
|
const report = buildWp706PrefreezeReport({
|
||||||
|
currentCommit,
|
||||||
|
releaseExists: existsSync(resolve("RELEASE.json")),
|
||||||
|
trace,
|
||||||
|
upstream,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
acceptance_criteria: Array.from({ length: 56 }, (_, index) => index + 1)
|
||||||
|
.filter((number) => ![8, 26, 37, 54].includes(number))
|
||||||
|
.map((number) => `AC-${String(number).padStart(2, "0")}`),
|
||||||
|
automation: ["automated", "manual_review"],
|
||||||
|
commit: currentCommit,
|
||||||
|
evidence_refs: ["commands.json", "trace-summary.json", "upstream-lineage.json", "deferred-external.json"],
|
||||||
|
finished_at: new Date().toISOString(),
|
||||||
|
manifest: {
|
||||||
|
path: "tasks.manifest.json",
|
||||||
|
sha256: createHash("sha256").update(readFileSync("tasks.manifest.json")).digest("hex").toUpperCase(),
|
||||||
|
},
|
||||||
|
missing_evidence: [],
|
||||||
|
release_gate: ["release:P0-A"],
|
||||||
|
requirements: ["NFR-05"],
|
||||||
|
run_id: runId,
|
||||||
|
schema_version: "1.0",
|
||||||
|
status: report.status,
|
||||||
|
task_id: "TASK-WP7-06",
|
||||||
|
test_id: "TDD-WP7-AC-001-trace-structure",
|
||||||
|
work_package: "WP-7",
|
||||||
|
};
|
||||||
|
|
||||||
|
writeJson(resolve(caseDirectory, "commands.json"), { commands, run_id: runId, schema_version: "1.0" });
|
||||||
|
writeJson(resolve(caseDirectory, "trace-summary.json"), { status: trace.status, summary: trace.summary, schema_version: "1.0" });
|
||||||
|
writeJson(resolve(caseDirectory, "upstream-lineage.json"), { current_commit: currentCommit, tasks: report.upstream, schema_version: "1.0" });
|
||||||
|
writeJson(resolve(caseDirectory, "deferred-external.json"), {
|
||||||
|
policy: "product_owner_first_version_nonblocking",
|
||||||
|
tasks: report.deferred_external_tasks,
|
||||||
|
treated_as_real_provider_pass: false,
|
||||||
|
schema_version: "1.0",
|
||||||
|
});
|
||||||
|
writeJson(resolve(caseDirectory, "result.json"), result);
|
||||||
|
writeJson(resolve(runDirectory, "evidence.json"), { cases: [{ missing_evidence: [], status: result.status, test_id: result.test_id }], run_id: runId, status: result.status, schema_version: "1.0" });
|
||||||
|
console.log(JSON.stringify({ deferred_external_tasks: report.deferred_external_tasks, next_task: report.next_task, run_id: runId, status: report.status }, null, 2));
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { basename, join, resolve } from "node:path";
|
||||||
|
|
||||||
|
import { buildAndValidatePortablePackage } from "./lib/portable-package.mjs";
|
||||||
|
import { readCandidateEnvironment } from "./lib/release-candidate.mjs";
|
||||||
|
import {
|
||||||
|
buildFinalReleaseRecord,
|
||||||
|
scanReleaseFiles,
|
||||||
|
sha256File,
|
||||||
|
validateFinalEvidence,
|
||||||
|
} from "./lib/wp7-07-final-release.mjs";
|
||||||
|
|
||||||
|
const runId = process.env.DADA_TDD_RUN_ID ?? `wp7-07-final-${new Date().toISOString().replace(/[^0-9]/g, "")}`;
|
||||||
|
const runDirectory = resolve("artifacts", "tdd", runId);
|
||||||
|
const releaseCase = resolve(runDirectory, "cases", "TDD-WP7-REL-001-final-release-record");
|
||||||
|
const securityCase = resolve(runDirectory, "cases", "TDD-WP7-SEC-001-artifact-leak-scan");
|
||||||
|
const outputRoot = resolve(".build", "wp7-07-final-release");
|
||||||
|
if (existsSync(runDirectory)) throw new Error(`Evidence run already exists: ${runId}`);
|
||||||
|
mkdirSync(releaseCase, { recursive: true });
|
||||||
|
mkdirSync(securityCase, { recursive: true });
|
||||||
|
|
||||||
|
function writeJson(path, value) {
|
||||||
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function git(args) {
|
||||||
|
const result = spawnSync("git", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: 120_000 });
|
||||||
|
if ((result.status ?? 1) !== 0) throw new Error(`WP7_07_GIT_FAILED:${args.join(" ")}`);
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitGrep(args) {
|
||||||
|
const result = spawnSync("git", ["grep", ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: 120_000 });
|
||||||
|
if (![0, 1].includes(result.status ?? 2)) throw new Error("WP7_07_GIT_GREP_FAILED");
|
||||||
|
return result.status === 0 ? result.stdout.trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(name, command) {
|
||||||
|
const startedAt = new Date().toISOString();
|
||||||
|
const result = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", command], {
|
||||||
|
encoding: "utf8",
|
||||||
|
env: process.env,
|
||||||
|
maxBuffer: 64 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
if (result.stdout) process.stdout.write(result.stdout);
|
||||||
|
if (result.stderr) process.stderr.write(result.stderr);
|
||||||
|
return { command, exit_code: result.status ?? 1, finished_at: new Date().toISOString(), name, started_at: startedAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentCommit = git(["rev-parse", "HEAD"]);
|
||||||
|
const prefreezeCommit = git(["ls-remote", "origin", "refs/heads/codex/wp7-06"]).split(/\s+/)[0];
|
||||||
|
if (!prefreezeCommit || spawnSync("git", ["merge-base", "--is-ancestor", prefreezeCommit, "HEAD"]).status !== 0) {
|
||||||
|
throw new Error("WP7_07_PREFREEZE_LINEAGE_INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
run("unit", "node --test tests/package/wp7-07-final-release.test.mjs"),
|
||||||
|
run("security", "pnpm test:security"),
|
||||||
|
run("trace", "pnpm validate:tdd-trace"),
|
||||||
|
];
|
||||||
|
if (commands.some(({ exit_code }) => exit_code !== 0)) {
|
||||||
|
writeJson(join(releaseCase, "commands.json"), { commands, run_id: runId, schema_version: "1.0" });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const environment = readCandidateEnvironment();
|
||||||
|
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
|
||||||
|
const release = buildFinalReleaseRecord({
|
||||||
|
appVersion: packageJson.version,
|
||||||
|
browsers: environment.browsers.map(({ brand, full_version }) => ({ brand, fullVersion: full_version })),
|
||||||
|
buildCommit: currentCommit,
|
||||||
|
frozenFromCommit: prefreezeCommit,
|
||||||
|
recordedAt: new Date().toISOString(),
|
||||||
|
windows: {
|
||||||
|
arch: environment.windows.arch,
|
||||||
|
build: environment.windows.build,
|
||||||
|
displayVersion: environment.windows.display_version,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
writeJson(resolve("RELEASE.json"), release);
|
||||||
|
|
||||||
|
const packageResult = await buildAndValidatePortablePackage({ evidenceDirectory: releaseCase, outputRoot, releaseRecord: release });
|
||||||
|
const packageDirectory = join(outputRoot, packageResult.packageManifest.package_name);
|
||||||
|
const zipPath = join(outputRoot, `${packageResult.packageManifest.package_name}.zip`);
|
||||||
|
const releaseSha256 = sha256File(resolve("RELEASE.json"));
|
||||||
|
const packageReleaseSha256 = sha256File(join(packageDirectory, "RELEASE.json"));
|
||||||
|
if (releaseSha256 !== packageReleaseSha256) throw new Error("WP7_07_PACKAGE_RELEASE_DRIFT");
|
||||||
|
|
||||||
|
const finalScan = scanReleaseFiles({ roots: [packageDirectory, releaseCase] });
|
||||||
|
const trackedSensitive = gitGrep(["-I", "-n", "-E", "C:\\\\Users\\\\[^\\\\]+|sk-[A-Za-z0-9_-]{24,}", "HEAD", "--", ":!tests", ":!scripts/lib/wp7-07-final-release.mjs"]);
|
||||||
|
const scan = {
|
||||||
|
...finalScan,
|
||||||
|
git_current_findings: trackedSensitive ? trackedSensitive.split(/\r?\n/).filter(Boolean) : [],
|
||||||
|
status: finalScan.status === "passed" && !trackedSensitive ? "passed" : "failed",
|
||||||
|
};
|
||||||
|
writeJson(join(securityCase, "scan-report.json"), scan);
|
||||||
|
writeJson(join(securityCase, "allowlist.json"), {
|
||||||
|
entries: ["tests/**:synthetic security traps", "scripts/lib/wp7-07-final-release.mjs:scanner patterns"],
|
||||||
|
real_values_allowed: false,
|
||||||
|
schema_version: "1.0",
|
||||||
|
});
|
||||||
|
if (scan.status !== "passed") throw new Error("WP7_07_LEAK_SCAN_FAILED");
|
||||||
|
|
||||||
|
const finalEvidence = validateFinalEvidence({ packageManifest: packageResult.packageManifest, release, releaseSha256, scan });
|
||||||
|
copyFileSync(resolve("RELEASE.json"), join(releaseCase, "RELEASE.json"));
|
||||||
|
copyFileSync(join(packageDirectory, "START-HERE.txt"), join(releaseCase, "START-HERE.txt"));
|
||||||
|
writeJson(join(releaseCase, "environment.json"), {
|
||||||
|
browsers: release.browsers,
|
||||||
|
fixed_port: release.fixedPort,
|
||||||
|
windows: release.windows,
|
||||||
|
schema_version: "1.0",
|
||||||
|
});
|
||||||
|
writeJson(join(releaseCase, "final-package.json"), {
|
||||||
|
file_name: basename(zipPath),
|
||||||
|
release_sha256: finalEvidence.release_sha256,
|
||||||
|
release_status: release.releaseStatus,
|
||||||
|
zip_sha256: finalEvidence.zip_sha256,
|
||||||
|
schema_version: "1.0",
|
||||||
|
});
|
||||||
|
writeJson(join(releaseCase, "commands.json"), { commands, run_id: runId, schema_version: "1.0" });
|
||||||
|
writeJson(join(releaseCase, "result.json"), {
|
||||||
|
acceptance_criteria: ["AC-24", "AC-41", "AC-48", "AC-56"],
|
||||||
|
deferred_external_tasks: release.deferredExternalTasks,
|
||||||
|
evidence_refs: ["RELEASE.json", "START-HERE.txt", "environment.json", "package-manifest.json", "final-package.json"],
|
||||||
|
release_gate: ["release:P0-A"],
|
||||||
|
requirements: ["NFR-01", "NFR-09", "PRIV-01", "PRIV-02"],
|
||||||
|
status: "passed",
|
||||||
|
task_id: "TASK-WP7-07",
|
||||||
|
test_id: "TDD-WP7-REL-001-final-release-record",
|
||||||
|
});
|
||||||
|
writeJson(join(securityCase, "result.json"), {
|
||||||
|
evidence_refs: ["scan-report.json", "allowlist.json"],
|
||||||
|
status: "passed",
|
||||||
|
task_id: "TASK-WP7-07",
|
||||||
|
test_id: "TDD-WP7-SEC-001-artifact-leak-scan",
|
||||||
|
});
|
||||||
|
writeJson(join(runDirectory, "evidence.json"), {
|
||||||
|
cases: [
|
||||||
|
{ missing_evidence: [], status: "passed", test_id: "TDD-WP7-REL-001-final-release-record" },
|
||||||
|
{ missing_evidence: [], status: "passed", test_id: "TDD-WP7-SEC-001-artifact-leak-scan" },
|
||||||
|
],
|
||||||
|
deferred_external_tasks: release.deferredExternalTasks,
|
||||||
|
release_sha256: finalEvidence.release_sha256,
|
||||||
|
run_id: runId,
|
||||||
|
status: "passed",
|
||||||
|
zip_sha256: finalEvidence.zip_sha256,
|
||||||
|
schema_version: "1.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
deferred_external_tasks: release.deferredExternalTasks,
|
||||||
|
release_sha256: finalEvidence.release_sha256,
|
||||||
|
run_id: runId,
|
||||||
|
status: "passed",
|
||||||
|
zip_sha256: finalEvidence.zip_sha256,
|
||||||
|
}, null, 2));
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { buildWp706PrefreezeReport } from "../../scripts/lib/wp7-06-prefreeze.mjs";
|
||||||
|
|
||||||
|
const expectedTrace = {
|
||||||
|
errors: [],
|
||||||
|
status: "passed",
|
||||||
|
summary: {
|
||||||
|
acceptanceCriteria: 52,
|
||||||
|
errorCategories: 9,
|
||||||
|
featureModules: 13,
|
||||||
|
parentFamilies: 89,
|
||||||
|
penProductFrames: 18,
|
||||||
|
productContracts: 19,
|
||||||
|
requirements: 109,
|
||||||
|
tasks: 52,
|
||||||
|
testCases: 117,
|
||||||
|
uiPages: 22,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const upstream = {
|
||||||
|
"TASK-WP7-01": { branch: "codex/wp7-01", head: "1".repeat(40), merged: true, status: "passed" },
|
||||||
|
"TASK-WP7-02": { branch: "codex/wp7-02", head: "2".repeat(40), merged: true, status: "passed" },
|
||||||
|
"TASK-WP7-03": { branch: "codex/wp7-03", head: "3".repeat(40), merged: true, status: "deferred_nonblocking_first_version" },
|
||||||
|
"TASK-WP7-04": { branch: "codex/wp7-04", head: "4".repeat(40), merged: true, status: "deferred_nonblocking_first_version" },
|
||||||
|
"TASK-WP7-05": { branch: "codex/wp7-05", head: "5".repeat(40), merged: true, status: "passed" },
|
||||||
|
};
|
||||||
|
|
||||||
|
test("builds the exact WP7-06 pre-freeze closure without pretending deferred suppliers passed", () => {
|
||||||
|
const report = buildWp706PrefreezeReport({
|
||||||
|
currentCommit: "a".repeat(40),
|
||||||
|
releaseExists: false,
|
||||||
|
trace: expectedTrace,
|
||||||
|
upstream,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(report.status, "passed");
|
||||||
|
assert.equal(report.release_json_written, false);
|
||||||
|
assert.deepEqual(report.trace_summary, expectedTrace.summary);
|
||||||
|
assert.deepEqual(report.deferred_external_tasks, ["TASK-WP7-03", "TASK-WP7-04"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects missing lineage and premature RELEASE.json", () => {
|
||||||
|
assert.throws(() => buildWp706PrefreezeReport({
|
||||||
|
currentCommit: "a".repeat(40),
|
||||||
|
releaseExists: false,
|
||||||
|
trace: expectedTrace,
|
||||||
|
upstream: { ...upstream, "TASK-WP7-05": { ...upstream["TASK-WP7-05"], merged: false } },
|
||||||
|
}), /WP7_06_UPSTREAM_NOT_MERGED:TASK-WP7-05/);
|
||||||
|
|
||||||
|
assert.throws(() => buildWp706PrefreezeReport({
|
||||||
|
currentCommit: "a".repeat(40),
|
||||||
|
releaseExists: true,
|
||||||
|
trace: expectedTrace,
|
||||||
|
upstream,
|
||||||
|
}), /WP7_06_RELEASE_WRITTEN_PREMATURELY/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects trace drift and unsupported upstream statuses", () => {
|
||||||
|
assert.throws(() => buildWp706PrefreezeReport({
|
||||||
|
currentCommit: "a".repeat(40),
|
||||||
|
releaseExists: false,
|
||||||
|
trace: { ...expectedTrace, summary: { ...expectedTrace.summary, testCases: 116 } },
|
||||||
|
upstream,
|
||||||
|
}), /WP7_06_TRACE_COUNT_MISMATCH:testCases/);
|
||||||
|
|
||||||
|
assert.throws(() => buildWp706PrefreezeReport({
|
||||||
|
currentCommit: "a".repeat(40),
|
||||||
|
releaseExists: false,
|
||||||
|
trace: expectedTrace,
|
||||||
|
upstream: { ...upstream, "TASK-WP7-03": { ...upstream["TASK-WP7-03"], status: "passed" } },
|
||||||
|
}), /WP7_06_UNSUPPORTED_STATUS:TASK-WP7-03/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { test } from "node:test";
|
||||||
|
|
||||||
|
import { buildFinalReleaseRecord, scanReleaseFiles, validateFinalEvidence } from "../../scripts/lib/wp7-07-final-release.mjs";
|
||||||
|
|
||||||
|
function release() {
|
||||||
|
return buildFinalReleaseRecord({
|
||||||
|
appVersion: "0.0.0",
|
||||||
|
browsers: [
|
||||||
|
{ brand: "Google Chrome", fullVersion: "150.0.7871.187" },
|
||||||
|
{ brand: "Microsoft Edge", fullVersion: "151.0.4129.59" },
|
||||||
|
],
|
||||||
|
buildCommit: "a".repeat(40),
|
||||||
|
frozenFromCommit: "b".repeat(40),
|
||||||
|
recordedAt: "2026-08-04T06:00:00.000Z",
|
||||||
|
windows: { arch: "x64", build: "26200.8875", displayVersion: "25H2" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("TDD-WP7-REL-001 creates a browser-gate compatible first-version record", () => {
|
||||||
|
const record = release();
|
||||||
|
assert.equal(record.finalRelease, true);
|
||||||
|
assert.equal(record.fixedPort, 43121);
|
||||||
|
assert.deepEqual(record.deferredExternalTasks, ["TASK-WP7-03", "TASK-WP7-04"]);
|
||||||
|
assert.deepEqual(record.browsers.map(({ brand }) => brand).sort(), ["Google Chrome", "Microsoft Edge"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("TDD-WP7-SEC-001 rejects credential shapes and absolute user paths", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "dada-wp7-07-scan-"));
|
||||||
|
mkdirSync(join(root, "logs"));
|
||||||
|
writeFileSync(join(root, "logs", "diagnostic.txt"), "credential=key-abcdefghijklmnop C:\\Users\\person\\private.txt\n");
|
||||||
|
const scan = scanReleaseFiles({ roots: [root] });
|
||||||
|
assert.equal(scan.status, "failed");
|
||||||
|
assert.deepEqual(new Set(scan.findings.map(({ rule }) => rule)), new Set(["absolute_user_path", "credential_shape"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("TDD-WP7-REL-001 binds release and package hashes only after a zero-finding scan", () => {
|
||||||
|
assert.deepEqual(validateFinalEvidence({
|
||||||
|
packageManifest: { release_status: "first_version_internal", zip_sha256: "C".repeat(64) },
|
||||||
|
release: release(),
|
||||||
|
releaseSha256: "D".repeat(64),
|
||||||
|
scan: { findings: [], status: "passed" },
|
||||||
|
}), { release_sha256: "D".repeat(64), status: "passed", zip_sha256: "C".repeat(64) });
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user