From ad86b4ddcc9fa5544ba4d06e3c178f38d7598d86 Mon Sep 17 00:00:00 2001 From: suyx Date: Tue, 4 Aug 2026 23:03:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(WP7-07):=20=E5=A2=9E=E5=8A=A0=E6=9C=80?= =?UTF-8?q?=E7=BB=88=E5=8F=91=E5=B8=83=E5=86=BB=E7=BB=93=E4=B8=8E=E6=B3=84?= =?UTF-8?q?=E6=BC=8F=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- scripts/lib/portable-package.mjs | 44 ++++-- scripts/lib/wp7-07-final-release.mjs | 96 ++++++++++++ scripts/run-wp7-07-validation.mjs | 157 ++++++++++++++++++++ tests/package/wp7-07-final-release.test.mjs | 47 ++++++ 5 files changed, 334 insertions(+), 14 deletions(-) create mode 100644 scripts/lib/wp7-07-final-release.mjs create mode 100644 scripts/run-wp7-07-validation.mjs create mode 100644 tests/package/wp7-07-final-release.test.mjs diff --git a/package.json b/package.json index 0d5670a..b7a4d81 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,9 @@ "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-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": { "@playwright/test": "1.62.0", diff --git a/scripts/lib/portable-package.mjs b/scripts/lib/portable-package.mjs index af6d4a0..3a7f43a 100644 --- a/scripts/lib/portable-package.mjs +++ b/scripts/lib/portable-package.mjs @@ -208,7 +208,7 @@ async function waitForHealth(child) { throw new Error("Packaged API did not become healthy on fixed port 43121.", { cause: lastError }); } -async function verifyExtractedPackage(zipPath, packageName) { +async function verifyExtractedPackage(zipPath, packageName, expectedSupport) { const extractRoot = mkdtempSync(join(tmpdir(), "dada-wp0-09-")); try { const escapedZip = zipPath.replaceAll("'", "''"); @@ -237,19 +237,21 @@ async function verifyExtractedPackage(zipPath, packageName) { const health = await waitForHealth(api); const releaseGate = await fetch(`http://127.0.0.1:${fixedPort}/api/v1/support/check`, { body: JSON.stringify({ - brands: [{ brand: "Google Chrome", version: "150" }], - full_version_list: [{ brand: "Google Chrome", version: "150.0.0.0" }], + brands: [{ brand: expectedSupport.brand, version: String(expectedSupport.major) }], + full_version_list: [{ brand: expectedSupport.brand, version: expectedSupport.fullVersion }], platform: "Windows", }), headers: { "content-type": "application/json", - "sec-ch-ua": '"Google Chrome";v="150"', - "sec-ch-ua-full-version-list": '"Google Chrome";v="150.0.0.0"', + "sec-ch-ua": `"${expectedSupport.brand}";v="${expectedSupport.major}"`, + "sec-ch-ua-full-version-list": `"${expectedSupport.brand}";v="${expectedSupport.fullVersion}"`, "sec-ch-ua-platform": '"Windows"', }, method: "POST", }); - if (releaseGate.status !== 426) throw new Error(`Candidate RELEASE.json unexpectedly passed with ${releaseGate.status}.`); + if (releaseGate.status !== expectedSupport.statusCode) { + throw new Error(`Packaged RELEASE.json support gate returned ${releaseGate.status}; expected ${expectedSupport.statusCode}.`); + } return { api: { executable: "runtime/node.exe", health, pid: api.pid, release_gate: { status_code: releaseGate.status }, status: "passed" }, native, @@ -291,7 +293,7 @@ function scanPackage(packageDirectory) { 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) { throw new Error("Portable package build requires frozen Node 24.13.0 on win-x64."); } @@ -351,7 +353,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 }); 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, browsers: [], build_commit: commit, @@ -360,16 +363,20 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu windows_build: null, }); 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.", "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.", "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.", "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")); @@ -381,7 +388,18 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu const zipSha256 = fileSha256(zipPath); const shaPath = `${zipPath}.sha256`; 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) => ({ path: relative(packageDirectory, path).replaceAll("\\", "/"), sha256: fileSha256(path), @@ -392,7 +410,7 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu files: fileEntries, fixed_port: fixedPort, package_name: packageName, - release_status: "candidate_unvalidated", + release_status: finalRelease ? releaseRecord.releaseStatus : "candidate_unvalidated", schema_version: "1.0", zip_sha256: zipSha256, }; diff --git a/scripts/lib/wp7-07-final-release.mjs b/scripts/lib/wp7-07-final-release.mjs new file mode 100644 index 0000000..35d55b3 --- /dev/null +++ b/scripts/lib/wp7-07-final-release.mjs @@ -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|home)\/)/i; +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() }; +} diff --git a/scripts/run-wp7-07-validation.mjs b/scripts/run-wp7-07-validation.mjs new file mode 100644 index 0000000..1ec1aef --- /dev/null +++ b/scripts/run-wp7-07-validation.mjs @@ -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)); diff --git a/tests/package/wp7-07-final-release.test.mjs b/tests/package/wp7-07-final-release.test.mjs new file mode 100644 index 0000000..51e6bd7 --- /dev/null +++ b/tests/package/wp7-07-final-release.test.mjs @@ -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) }); +});