diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4200857..dab40bbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,4 +31,4 @@ jobs: contents: read # Caller выбирает revision, который чекаутит worker, включая код PR. Сам код # считается недоверенным и исполняется только внутри одноразовой ячейки. - uses: Labpics-Team/lab-colors/.github/workflows/ci-worker.yml@1461bc2ed60142aed3a8723e618b883be6418156 + uses: Labpics-Team/lab-colors/.github/workflows/ci-worker.yml@beecd257371a7a6421079b0d8207a109969aa332 diff --git a/packages/colors/bench/private-program-wasm.json b/packages/colors/bench/private-program-wasm.json new file mode 100644 index 00000000..8c647b94 --- /dev/null +++ b/packages/colors/bench/private-program-wasm.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "role": "private-program-consumer", + "artifact": "packages/colors/private-program/labcolors_private_program.wasm", + "toolchain": { + "rust": "1.96.0", + "rustcCommit": "ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96", + "cargo": "1.96.0", + "cargoCommit": "30a34c6821b57de0aaec83a901aca39f88f6778c", + "target": "wasm32-unknown-unknown", + "profile": "release", + "feature": "private-fixture", + "node": "24.14.0", + "binaryenRelease": "version_117", + "binaryenNodeArchiveSha256": "2d5a42f2d167a7cc2b4b6664c44c5ace1690d13db4f527324f052afbad461a07", + "binaryenComponentSha256": { + "wasm-opt.js": "c0b4bc26f1a588dc686ae36b32c4fea3d7b99f4fb8a1778d0ba4129f326f8449", + "wasm-opt.wasm": "d823328d8fcad3a59aa605c61d1620d30b9156f086d30ff3246b43c32526856b", + "wasm-opt.worker.js": "5b7952731f6ea1d5954db968e45b13f862853e6ef14a03b3f35d036f6136b624" + }, + "wasmOptFlags": "-Oz --enable-bulk-memory --enable-nontrapping-float-to-int" + }, + "measurement": { + "source": "github-actions-run-31473003387", + "platform": "linux-x64", + "rawBytes": 339336 + }, + "policy": { + "maxRawBytes": 339336, + "basis": "exact optimized private Program artifact from GitHub Actions Linux", + "gzip": "diagnostic-only" + } +} diff --git a/packages/colors/test/javascript-source-contract.mjs b/packages/colors/test/javascript-source-contract.mjs new file mode 100644 index 00000000..684ebfd9 --- /dev/null +++ b/packages/colors/test/javascript-source-contract.mjs @@ -0,0 +1,89 @@ +function tokens(source) { + const result = []; + for (let index = 0; index < source.length; ) { + const character = source[index]; + const next = source[index + 1]; + if (/\s/u.test(character)) { + index += 1; + } else if (character === "/" && next === "/") { + index = source.indexOf("\n", index + 2); + if (index === -1) break; + } else if (character === "/" && next === "*") { + index = source.indexOf("*/", index + 2); + if (index === -1) throw new Error("unterminated JavaScript block comment"); + index += 2; + } else if (character === '"' || character === "'") { + const quote = character; + const start = index; + index += 1; + while (index < source.length && source[index] !== quote) { + index += source[index] === "\\" ? 2 : 1; + } + if (index >= source.length) throw new Error("unterminated JavaScript string literal"); + const raw = source.slice(start, index + 1); + result.push({ type: "string", value: quote === '"' ? JSON.parse(raw) : raw.slice(1, -1) }); + index += 1; + } else if (character === "`") { + index += 1; + while (index < source.length && source[index] !== "`") { + index += source[index] === "\\" ? 2 : 1; + } + if (index >= source.length) throw new Error("unterminated JavaScript template literal"); + index += 1; + } else if (/[A-Za-z_$]/u.test(character)) { + const start = index; + index += 1; + while (/[A-Za-z0-9_$]/u.test(source[index] ?? "")) index += 1; + result.push({ type: "identifier", value: source.slice(start, index) }); + } else { + result.push({ type: "punctuator", value: character }); + index += 1; + } + } + return result; +} + +export function chromeArguments(source) { + const sourceTokens = tokens(source); + for (let index = 0; index < sourceTokens.length - 4; index += 1) { + if ( + sourceTokens[index].type !== "string" || + sourceTokens[index].value !== "goog:chromeOptions" || + sourceTokens[index + 1].value !== ":" || + sourceTokens[index + 2].value !== "{" + ) { + continue; + } + let objectDepth = 1; + for (let cursor = index + 3; cursor < sourceTokens.length && objectDepth > 0; cursor += 1) { + const token = sourceTokens[cursor]; + if (token.value === "{") objectDepth += 1; + if (token.value === "}") objectDepth -= 1; + if ( + objectDepth === 1 && + token.type === "identifier" && + token.value === "args" && + sourceTokens[cursor + 1]?.value === ":" && + sourceTokens[cursor + 2]?.value === "[" + ) { + const args = []; + let nestedDepth = 0; + let element = []; + for (let argument = cursor + 3; argument < sourceTokens.length; argument += 1) { + const argumentToken = sourceTokens[argument]; + if (nestedDepth === 0 && (argumentToken.value === "," || argumentToken.value === "]")) { + if (element.length === 1 && element[0].type === "string") args.push(element[0].value); + element = []; + if (argumentToken.value === "]") return args; + continue; + } + if (["[", "{", "("].includes(argumentToken.value)) nestedDepth += 1; + if (["]", "}", ")"].includes(argumentToken.value)) nestedDepth -= 1; + element.push(argumentToken); + } + throw new Error("unterminated goog:chromeOptions args array"); + } + } + } + throw new Error("goog:chromeOptions args array is absent"); +} diff --git a/packages/colors/test/private-program-ci-contract.test.mjs b/packages/colors/test/private-program-ci-contract.test.mjs index e8b33213..f0282737 100644 --- a/packages/colors/test/private-program-ci-contract.test.mjs +++ b/packages/colors/test/private-program-ci-contract.test.mjs @@ -12,12 +12,14 @@ import { dirname, join, resolve } from "node:path"; import { test } from "node:test"; import { fileURLToPath } from "node:url"; +import { chromeArguments } from "./javascript-source-contract.mjs"; + const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, "../../.."); const read = (...parts) => readFileSync(join(root, ...parts), "utf8"); const normalizeNewlines = (value) => value.replaceAll("\r\n", "\n"); -const CALLER_WORKER_SHA = "1461bc2ed60142aed3a8723e618b883be6418156"; +const CALLER_WORKER_SHA = "beecd257371a7a6421079b0d8207a109969aa332"; const CALLER_WORKER_REFERENCE = ` uses: Labpics-Team/lab-colors/.github/workflows/ci-worker.yml@${CALLER_WORKER_SHA}`; const RUNTIME_BUDGET_COMMAND = " run: node scripts/check-wasm-size-budget.mjs"; @@ -182,12 +184,15 @@ function assertPrivateMutationDeadline(workflow) { assert.ok(70 > 40 + 20 + 5, "outer timeout must exceed declared budgets and teardown headroom"); } -test("Stage A keeps the public caller pinned to the pre-Stage-B immutable worker", () => { +test("Stage B activates the public caller at the merged Stage A worker commit", () => { const caller = read(".github", "workflows", "ci.yml"); assertImmutableCaller(caller); for (const mutation of [ caller.replace(CALLER_WORKER_SHA, "0".repeat(40)), + caller.replace(CALLER_WORKER_SHA, "main"), + caller.replace(CALLER_WORKER_SHA, CALLER_WORKER_SHA.slice(0, 12)), + caller.replace(CALLER_WORKER_REFERENCE, ""), caller.replace(CALLER_WORKER_REFERENCE, `${CALLER_WORKER_REFERENCE}\n${CALLER_WORKER_REFERENCE}`), caller.replace( CALLER_WORKER_REFERENCE, @@ -261,6 +266,20 @@ test("worker binds the browser proof to the exact verified tarball bytes", () => assert.throws(() => assertWorkerOrderAndRoles(reordered)); }); +test("private Program browser proof owns the CI Chrome launch invariant", () => { + const browserProof = read("scripts", "test-private-program-browser.mjs"); + assert.ok( + chromeArguments(browserProof).includes("--no-sandbox"), + "the userspace CfT proof must opt out of an unavailable host sandbox", + ); + const flagInCommentOnly = browserProof.replace( + ' "--no-sandbox",', + ' // "--no-sandbox",', + ); + assert.notEqual(flagInCommentOnly, browserProof); + assert.equal(chromeArguments(flagInCommentOnly).includes("--no-sandbox"), false); +}); + test("private mutation keeps its own deadline reachable inside the wasm job", () => { const worker = read(".github", "workflows", "ci-worker.yml"); assertPrivateMutationDeadline(worker); diff --git a/packages/colors/test/private-program-mutation-contract.test.mjs b/packages/colors/test/private-program-mutation-contract.test.mjs index bb4e2c74..28ca6f45 100644 --- a/packages/colors/test/private-program-mutation-contract.test.mjs +++ b/packages/colors/test/private-program-mutation-contract.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; -import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; +import { lstat, mkdir, mkdtemp, readdir, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; @@ -16,6 +16,7 @@ import { parseMutationTimeoutPolicy, validateMutationWasm, } from "../../../scripts/test-private-program-mutations.mjs"; +import { copyDeclaredCargoRegistryIndex } from "../../../scripts/build-private-program.mjs"; const EXPECTED_MUTATIONS = Object.freeze([ [ @@ -24,7 +25,7 @@ const EXPECTED_MUTATIONS = Object.freeze([ "rust-wasm", "crates/labcolors-core/src/private_fixture.rs", "6d43e030e5d6f4df67b319276d3d9ea1ab33d5b91a2094019ce278476cecd412", - "PrivateProgramConsumerError: private Program run failed with status 6", + "PrivateProgramConsumerError: private Program consumer: run failed with status 6", ], [ "hard-constraint-deletion", @@ -32,7 +33,7 @@ const EXPECTED_MUTATIONS = Object.freeze([ "rust-wasm", "crates/labcolors-core/src/private_fixture.rs", "0e43717331468ae6fb6c65bb6ba441cd5260b49b37ad3c6927a9bbf02494638b", - "PrivateProgramConsumerError: private Program run failed with status 6", + "PrivateProgramConsumerError: private Program consumer: run failed with status 6", ], [ "final-recheck-call-edge-deletion", @@ -40,7 +41,7 @@ const EXPECTED_MUTATIONS = Object.freeze([ "rust-wasm", "crates/labcolors-core/src/program_session.rs", "8e218fddef0b8a13f044b91b7927faeb50e501a54ecb205c05696d6510fe615e", - "PrivateProgramConsumerError: private Program run failed with status 8", + "PrivateProgramConsumerError: private Program consumer: run failed with status 8", ], [ "session-observed-update-bypass", @@ -48,7 +49,7 @@ const EXPECTED_MUTATIONS = Object.freeze([ "rust-wasm", "crates/labcolors-core/src/private_fixture.rs", "8770e1a30407e9cd9ec3ee32feed44cd2b58a0d39be3c636cb428be8b1c472fe", - "PrivateProgramConsumerError: shipping trace permits exactly one SetAll callback", + "PrivateProgramConsumerError: private Program consumer: shipping trace permits exactly one SetAll callback", ], [ "external-attachment-handoff-binding-bypass", @@ -56,7 +57,7 @@ const EXPECTED_MUTATIONS = Object.freeze([ "rust-wasm", "crates/labcolors-core/src/private_fixture.rs", "59f5bc640fbc270db4dbbda5572e339c90aa839d61cab0ac155ef8db5e1e5d59", - "PrivateProgramConsumerError: private Program run failed with status 7", + "PrivateProgramConsumerError: private Program consumer: run failed with status 7", ], [ "javascript-publish-deletion", @@ -64,7 +65,7 @@ const EXPECTED_MUTATIONS = Object.freeze([ "javascript", "packages/colors/private-program/consumer.js", "354125be94020475d0e34beb0f4498474cf243adb89d3bb591589ff5d485af0b", - "Error: private Program browser fixture: computed background is the exact expected CSS literal", + "Error: private Program browser fixture: computed background is the exact expected CSS literal; expected \"rgba(64, 64, 64, 0.5)\", got \"rgba(0, 0, 0, 0)\"", ], ]); @@ -73,6 +74,20 @@ const CI_WORKER = readFileSync( "utf8", ); +const directoryLinkType = process.platform === "win32" ? "junction" : "dir"; + +async function assertTreeContainsNoLinks(root) { + const pending = [root]; + while (pending.length > 0) { + const directory = pending.pop(); + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + assert.equal(entry.isSymbolicLink(), false, `copied registry index contains a link: ${path}`); + if (entry.isDirectory()) pending.push(path); + } + } +} + test("private Program mutation IDs bind six exact source transformations", () => { assert.deepEqual( PRIVATE_PROGRAM_MUTATION_CASES.map( @@ -154,6 +169,147 @@ test("exact source mutation rejects missing, repeated, and no-op anchors", () => ); }); +test("isolated offline mutation builds copy the declared Cargo registry index", async () => { + const temporary = await mkdtemp(join(tmpdir(), "labcolors-mutation-cargo-index-")); + const declaredCargoHome = join(temporary, "declared-cargo"); + const isolatedCargoHome = join(temporary, "isolated-cargo"); + try { + await mkdir(join(declaredCargoHome, "registry", "index", "registry.example"), { + recursive: true, + }); + await writeFile( + join(declaredCargoHome, "registry", "index", "registry.example", "config.json"), + "{}\n", + ); + await copyDeclaredCargoRegistryIndex(isolatedCargoHome, { declaredCargoHome }); + assert.equal( + readFileSync( + join(isolatedCargoHome, "registry", "index", "registry.example", "config.json"), + "utf8", + ), + "{}\n", + ); + } finally { + await rm(temporary, { recursive: true, force: true }); + } +}); + +test("isolated Cargo index materializes a symlinked index root", async () => { + const temporary = await mkdtemp(join(tmpdir(), "labcolors-mutation-cargo-index-root-link-")); + const declaredCargoHome = join(temporary, "declared-cargo"); + const physicalIndex = join(temporary, "physical-index"); + const isolatedCargoHome = join(temporary, "isolated-cargo"); + try { + await mkdir(join(declaredCargoHome, "registry"), { recursive: true }); + await mkdir(join(physicalIndex, "registry.example"), { recursive: true }); + await writeFile(join(physicalIndex, "registry.example", "config.json"), "{}\n"); + await symlink(physicalIndex, join(declaredCargoHome, "registry", "index"), directoryLinkType); + + await copyDeclaredCargoRegistryIndex(isolatedCargoHome, { declaredCargoHome }); + + const copiedIndex = join(isolatedCargoHome, "registry", "index"); + assert.equal((await lstat(copiedIndex)).isSymbolicLink(), false); + await assertTreeContainsNoLinks(copiedIndex); + } finally { + await rm(temporary, { recursive: true, force: true }); + } +}); + +test("isolated Cargo index accepts a symlinked declared-home ancestor", async () => { + const temporary = await mkdtemp(join(tmpdir(), "labcolors-mutation-cargo-index-ancestor-link-")); + const physicalCargoHome = join(temporary, "physical-cargo"); + const declaredCargoHome = join(temporary, "declared-cargo-link"); + const sourceIndex = join(physicalCargoHome, "registry", "index"); + const isolatedCargoHome = join(temporary, "isolated-cargo"); + try { + await mkdir(join(sourceIndex, "registry.example", "snapshot"), { recursive: true }); + await writeFile(join(sourceIndex, "registry.example", "snapshot", "config.json"), "{}\n"); + await symlink( + join(sourceIndex, "registry.example", "snapshot"), + join(sourceIndex, "registry.example", "current"), + directoryLinkType, + ); + await symlink(physicalCargoHome, declaredCargoHome, directoryLinkType); + + await copyDeclaredCargoRegistryIndex(isolatedCargoHome, { declaredCargoHome }); + + assert.equal( + readFileSync( + join(isolatedCargoHome, "registry", "index", "registry.example", "current", "config.json"), + "utf8", + ), + "{}\n", + ); + } finally { + await rm(temporary, { recursive: true, force: true }); + } +}); + +test("isolated Cargo index materializes contained source links", async () => { + const temporary = await mkdtemp(join(tmpdir(), "labcolors-mutation-cargo-index-target-links-")); + const declaredCargoHome = join(temporary, "declared-cargo"); + const sourceIndex = join(declaredCargoHome, "registry", "index"); + const isolatedCargoHome = join(temporary, "isolated-cargo"); + try { + await mkdir(join(sourceIndex, "registry.example", "snapshot"), { recursive: true }); + await writeFile(join(sourceIndex, "registry.example", "snapshot", "config.json"), "{}\n"); + await symlink( + join(sourceIndex, "registry.example", "snapshot"), + join(sourceIndex, "registry.example", "current"), + directoryLinkType, + ); + + await copyDeclaredCargoRegistryIndex(isolatedCargoHome, { declaredCargoHome }); + + await assertTreeContainsNoLinks(join(isolatedCargoHome, "registry", "index")); + } finally { + await rm(temporary, { recursive: true, force: true }); + } +}); + +test("isolated Cargo index rejects a source link escaping the declared index", async () => { + const temporary = await mkdtemp(join(tmpdir(), "labcolors-mutation-cargo-index-escape-")); + const declaredCargoHome = join(temporary, "declared-cargo"); + const sourceIndex = join(declaredCargoHome, "registry", "index"); + const outside = join(temporary, "outside-index"); + const isolatedCargoHome = join(temporary, "isolated-cargo"); + try { + await mkdir(join(sourceIndex, "registry.example"), { recursive: true }); + await mkdir(outside, { recursive: true }); + await writeFile(join(outside, "config.json"), "{}\n"); + await symlink(outside, join(sourceIndex, "registry.example", "escape"), directoryLinkType); + + await assert.rejects( + copyDeclaredCargoRegistryIndex(isolatedCargoHome, { declaredCargoHome }), + /symlink resolves outside the declared Cargo index/u, + ); + } finally { + await rm(temporary, { recursive: true, force: true }); + } +}); + +test("isolated Cargo index rejects a dangling source link", async () => { + const temporary = await mkdtemp(join(tmpdir(), "labcolors-mutation-cargo-index-dangling-")); + const declaredCargoHome = join(temporary, "declared-cargo"); + const sourceIndex = join(declaredCargoHome, "registry", "index"); + const isolatedCargoHome = join(temporary, "isolated-cargo"); + try { + await mkdir(join(sourceIndex, "registry.example"), { recursive: true }); + await symlink( + join(sourceIndex, "missing"), + join(sourceIndex, "registry.example", "dangling"), + directoryLinkType, + ); + + await assert.rejects( + copyDeclaredCargoRegistryIndex(isolatedCargoHome, { declaredCargoHome }), + /registry index symlink is dangling/u, + ); + } finally { + await rm(temporary, { recursive: true, force: true }); + } +}); + test("a mutation kill requires its real-browser assertion and semantic marker", () => { const definition = PRIVATE_PROGRAM_MUTATION_CASES[0]; const killed = Object.freeze({ @@ -162,7 +318,7 @@ test("a mutation kill requires its real-browser assertion and semantic marker", stdout: "", stderr: "Error: private Program browser proof: browser assertion failed: " + - "PrivateProgramConsumerError: private Program run failed with status 6", + "PrivateProgramConsumerError: private Program consumer: run failed with status 6", }); assert.equal(assertMutationSpecificBrowserFailure(definition, killed), true); assert.throws( @@ -189,7 +345,7 @@ test("a mutation kill requires its real-browser assertion and semantic marker", ...killed, stderr: "private Program browser proof: browser assertion failed: wrong failure", }), - /mutation-specific failure evidence/u, + /mutation-specific failure evidence: expected=.*actual=/u, ); assert.throws( () => diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index 52786006..42b1ab9a 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -52,6 +52,7 @@ import { POINT_SUPPORT_EVIDENCE_FILES, WCAG22_EVIDENCE_FILES, } from "../../../scripts/release-evidence.mjs"; +import { chromeArguments } from "./javascript-source-contract.mjs"; const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, "../../.."); @@ -2174,6 +2175,16 @@ test("the atomic output sink has one bounded pinned-Chrome browser gate", () => assert.match(source, /const chromeDriverPath = await executableFromEnv\("CHROMEDRIVER_PATH"\);/u); assert.match(source, /startChromeDriver\(\s*chromeDriverPath,/u); assert.match(source, /binary: chromePath,/u); + assert.ok( + chromeArguments(source).includes("--no-sandbox"), + "the userspace CfT proof must opt out of an unavailable host sandbox", + ); + const flagOutsideChromeArguments = source.replace( + ' "--no-sandbox",', + ' "--window-size=800,600",\n // "--no-sandbox"', + ); + assert.notEqual(flagOutsideChromeArguments, source); + assert.equal(chromeArguments(flagOutsideChromeArguments).includes("--no-sandbox"), false); assert.match(source, /spawn\(executable, \["--port=0"\]/u); assert.match( source, diff --git a/scripts/build-private-program.mjs b/scripts/build-private-program.mjs index 30c0c827..48f1c278 100644 --- a/scripts/build-private-program.mjs +++ b/scripts/build-private-program.mjs @@ -944,6 +944,22 @@ export async function assertCanonicalCargoConfigurationAbsent(options = {}) { return true; } +export async function copyDeclaredCargoRegistryIndex( + cargoHome, + { declaredCargoHome = resolve(environmentValue(process.env, "CARGO_HOME")?.trim() || join(homedir(), ".cargo")) } = {}, +) { + const sourceIndex = resolve(declaredCargoHome, "registry", "index"); + const targetIndex = resolve(cargoHome, "registry", "index"); + let canonicalSourceIndex; + try { + canonicalSourceIndex = await realpath(sourceIndex); + } catch (error) { + fail(`cannot resolve declared Cargo registry index ${sourceIndex}: ${error.message}`); + } + await assertRegistryIndexLinksContained(canonicalSourceIndex); + await cp(canonicalSourceIndex, targetIndex, { recursive: true, dereference: true }); +} + async function assertRegistryIndexLinksContained(root) { const pending = [root]; while (pending.length > 0) { @@ -984,14 +1000,8 @@ async function createBuildSandbox({ canonical }) { await mkdir(temporaryDirectory, { recursive: true, mode: 0o700 }); if (canonical) { await mkdir(cargoHome, { recursive: true, mode: 0o700 }); - const declaredCargoHome = resolve( - environmentValue(process.env, "CARGO_HOME")?.trim() || join(homedir(), ".cargo"), - ); - const sourceIndex = resolve(declaredCargoHome, "registry", "index"); - const targetIndex = resolve(cargoHome, "registry", "index"); try { - await assertRegistryIndexLinksContained(sourceIndex); - await cp(sourceIndex, targetIndex, { recursive: true }); + await copyDeclaredCargoRegistryIndex(cargoHome); } catch (error) { fail(`canonical build requires the declared Cargo registry index: ${error.message}`); } diff --git a/scripts/test-browser-output-sink.mjs b/scripts/test-browser-output-sink.mjs index ff5750a1..e7d5536a 100644 --- a/scripts/test-browser-output-sink.mjs +++ b/scripts/test-browser-output-sink.mjs @@ -1335,6 +1335,7 @@ async function main() { "--disable-sync", "--metrics-recording-only", "--no-first-run", + "--no-sandbox", "--window-size=800,600", ], }, diff --git a/scripts/test-private-program-browser.mjs b/scripts/test-private-program-browser.mjs index e8039f8a..63abf4a7 100644 --- a/scripts/test-private-program-browser.mjs +++ b/scripts/test-private-program-browser.mjs @@ -614,6 +614,7 @@ async function main() { "--disable-sync", "--metrics-recording-only", "--no-first-run", + "--no-sandbox", "--no-proxy-server", "--password-store=basic", "--safebrowsing-disable-auto-update", diff --git a/scripts/test-private-program-mutations.mjs b/scripts/test-private-program-mutations.mjs index 5de672e6..acfaee68 100644 --- a/scripts/test-private-program-mutations.mjs +++ b/scripts/test-private-program-mutations.mjs @@ -35,6 +35,7 @@ import { PRIVATE_PROGRAM_CONSUMER_PATH, PRIVATE_PROGRAM_WASM_PATH, PRIVATE_PROGRAM_WASM_SURFACE, + copyDeclaredCargoRegistryIndex, validatePrivateProgramWasmSurface, } from "./build-private-program.mjs"; import { PRIVATE_PROGRAM_BROWSER_PASS_RECEIPT } from "./test-private-program-browser.mjs"; @@ -91,7 +92,7 @@ export const PRIVATE_PROGRAM_MUTATION_CASES = Object.freeze([ ), replacement: "", expectedBrowserAssertion: - "PrivateProgramConsumerError: private Program run failed with status 6", + "PrivateProgramConsumerError: private Program consumer: run failed with status 6", }), mutation({ id: "hard-constraint-deletion", @@ -107,7 +108,7 @@ export const PRIVATE_PROGRAM_MUTATION_CASES = Object.freeze([ ), replacement: "", expectedBrowserAssertion: - "PrivateProgramConsumerError: private Program run failed with status 6", + "PrivateProgramConsumerError: private Program consumer: run failed with status 6", }), mutation({ id: "final-recheck-call-edge-deletion", @@ -141,7 +142,7 @@ export const PRIVATE_PROGRAM_MUTATION_CASES = Object.freeze([ ), replacement: lines(" let has_hard_violation = false;"), expectedBrowserAssertion: - "PrivateProgramConsumerError: private Program run failed with status 8", + "PrivateProgramConsumerError: private Program consumer: run failed with status 8", }), mutation({ id: "session-observed-update-bypass", @@ -161,7 +162,7 @@ export const PRIVATE_PROGRAM_MUTATION_CASES = Object.freeze([ " })", ).slice(0, -1), expectedBrowserAssertion: - "PrivateProgramConsumerError: shipping trace permits exactly one SetAll callback", + "PrivateProgramConsumerError: private Program consumer: shipping trace permits exactly one SetAll callback", }), mutation({ id: "external-attachment-handoff-binding-bypass", @@ -178,7 +179,7 @@ export const PRIVATE_PROGRAM_MUTATION_CASES = Object.freeze([ " ),", ), expectedBrowserAssertion: - "PrivateProgramConsumerError: private Program run failed with status 7", + "PrivateProgramConsumerError: private Program consumer: run failed with status 7", }), mutation({ id: "javascript-publish-deletion", @@ -193,7 +194,7 @@ export const PRIVATE_PROGRAM_MUTATION_CASES = Object.freeze([ ), replacement: lines(" frozenPublication(outputBinding, css);"), expectedBrowserAssertion: - "Error: private Program browser fixture: computed background is the exact expected CSS literal", + "Error: private Program browser fixture: computed background is the exact expected CSS literal; expected \"rgba(64, 64, 64, 0.5)\", got \"rgba(0, 0, 0, 0)\"", }), ]); @@ -316,7 +317,11 @@ export function assertMutationSpecificBrowserFailure(definition, result) { .slice(assertionLine.indexOf(BROWSER_ASSERTION_PREFIX) + BROWSER_ASSERTION_PREFIX.length) .trim(); if (actualAssertion !== definition.expectedBrowserAssertion) { - fail(`${definition.id} did not emit its mutation-specific failure evidence`); + fail( + `${definition.id} did not emit its mutation-specific failure evidence: ` + + `expected=${JSON.stringify(definition.expectedBrowserAssertion)} ` + + `actual=${JSON.stringify(actualAssertion)}`, + ); } return true; } @@ -1090,6 +1095,7 @@ async function executeMutationProof({ tarball, expectedSha256 }, policy) { mkdir(resolve(root, "cargo-home"), { recursive: true }), mkdir(resolve(root, "cargo-temp"), { recursive: true }), ]); + await copyDeclaredCargoRegistryIndex(resolve(root, "cargo-home")); const executors = await resolveCanonicalRustExecutors(runner); const optimizer = await resolveCanonicalOptimizer(root, runner); const cargo = cargoEnvironment({ workspace, root, target, executors }); diff --git a/scripts/test_mutation.py b/scripts/test_mutation.py index bf50c3f1..0fb067d6 100644 --- a/scripts/test_mutation.py +++ b/scripts/test_mutation.py @@ -1849,7 +1849,7 @@ def test_reusable_workers_bound_jobs_and_binaryen_transport(self) -> None: ci_caller = (workflows / "ci.yml").read_text(encoding="utf-8") admitted_ci_worker = ( "uses: Labpics-Team/lab-colors/.github/workflows/ci-worker.yml@" - "1461bc2ed60142aed3a8723e618b883be6418156" + "beecd257371a7a6421079b0d8207a109969aa332" ) self.assertEqual(ci_caller.count("ci-worker.yml@"), 1) self.assertIn(admitted_ci_worker, ci_caller)