diff --git a/scripts/lib/browser-scenario-service-workers-e2e.mjs b/scripts/lib/browser-scenario-service-workers-e2e.mjs index 3af2f4f8d6..bf88717ac5 100644 --- a/scripts/lib/browser-scenario-service-workers-e2e.mjs +++ b/scripts/lib/browser-scenario-service-workers-e2e.mjs @@ -12,6 +12,20 @@ import { browserScenarioSchema } from "../../dist/domain/browserScenario.js"; import { runScenarioCli } from "./browser-scenario-verifier.mjs"; import { verifyScenarioPolicySetup } from "./browser-scenario-service-worker-setup-e2e.mjs"; +const diagnosticError = (error) => + error instanceof Error + ? { + name: error.name, + message: error.message.slice(0, 1_000), + reason: error.reason, + stack: error.stack?.slice(0, 2_000), + cause: + error.cause instanceof Error + ? error.cause.stack?.slice(0, 3_000) + : typeof error.cause, + } + : { kind: typeof error }; + const startSite = async () => { let otherOrigin; const server = createServer((request, response) => { @@ -83,6 +97,7 @@ export async function verifyScenarioServiceWorkers(endpoint, targetId) { first.setOtherOrigin(second.origin); second.setOtherOrigin(first.origin); const observer = await chromium.connectOverCDP(endpoint); + console.log("Service-worker fixture browser:", observer.version()); let provider = createBrowserScenarioProvider(process.env); let session; let failure; @@ -154,7 +169,42 @@ export async function verifyScenarioServiceWorkers(endpoint, targetId) { state: "visible", }, ]); - const cleanCapture = (await runScenarioCli(clean)).normalized_result; + let cleanCapture; + try { + cleanCapture = (await runScenarioCli(clean)).normalized_result; + } catch (cause) { + // Retain the original failure. One bounded replay navigates only this + // verifier-owned target and exposes causes the CLI does not serialize. + const state = await page + .locator("#state") + .evaluate( + (element) => ({ + text: element.textContent?.slice(0, 100), + blocked: element.getAttribute("data-blocked"), + nativeRegister: navigator.serviceWorker.register + .toString() + .includes("[native code]"), + }), + undefined, + { timeout: 1_000 }, + ) + .catch((error) => ({ unavailable: String(error).slice(0, 500) })); + console.error("Service-worker CLI failure state:", { + url: page.url(), + state, + }); + const replay = await provider + .captureScenario(clean, { signal: AbortSignal.timeout(5_000) }) + .then((result) => + result.ok ? { ok: true } : diagnosticError(result.error), + ) + .catch(diagnosticError); + console.error( + "Service-worker diagnostic replay (original CLI failure retained):", + replay, + ); + throw cause; + } assert.ok(cleanCapture.steps.every(({ status }) => status === "completed")); assert.equal(cleanCapture.browser.cleanup, "disconnected-external"); await assertRestored(); diff --git a/tests/boundary/cli/doctorHopperDiscovery.test.ts b/tests/boundary/cli/doctorHopperDiscovery.test.ts index a637f73914..12df465d77 100644 --- a/tests/boundary/cli/doctorHopperDiscovery.test.ts +++ b/tests/boundary/cli/doctorHopperDiscovery.test.ts @@ -1,5 +1,5 @@ -import { access, chmod, mkdir, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises"; +import { delimiter, join } from "node:path"; import { expect } from "vitest"; @@ -18,11 +18,35 @@ cliTest.skipIf(!["darwin", "linux"].includes(process.platform))( '#!/bin/sh\nprintf launched > "$REA_HOPPER_TEST_MARKER"\nexit 99\n', ); await chmod(launcher, 0o700); + const loaderMarker = join(home, "loader-checked-launcher"); + if (process.platform === "linux") { + // A shell fixture needs controlled loader diagnostics on Linux. This + // checks discovery and path preservation, not actual Hopper compatibility. + const loader = join(home, "bin", "ldd"); + await writeFile( + loader, + [ + "#!/bin/sh", + 'test "$#" -eq 1 || exit 87', + 'test "$1" = "$HOPPER_LAUNCHER_PATH" || exit 88', + 'printf "%s" "$1" > "$REA_HOPPER_LDD_TEST_MARKER"', + 'printf "%s\\n" "libc.so.6 => /fixture/libc.so.6 (0x1)"', + "", + ].join("\n"), + ); + await chmod(loader, 0o700); + } const result = await cli.run({ arguments: ["doctor", "--provider", "hopper", "--json"], cwd: home, environment: { + ...(process.platform === "linux" + ? { + PATH: [join(home, "bin"), process.env.PATH ?? ""].join(delimiter), + REA_HOPPER_LDD_TEST_MARKER: loaderMarker, + } + : {}), HOME: home, USERPROFILE: home, HOPPER_LAUNCHER_PATH: launcher, @@ -32,28 +56,37 @@ cliTest.skipIf(!["darwin", "linux"].includes(process.platform))( timeoutMs: 20_000, }); - const executableOnHost = process.platform === "darwin"; + const hopperReady = process.platform === "darwin"; expect(result.signal).toBeNull(); - expect(result.exitCode).toBe(executableOnHost ? 0 : 1); + expect(result.exitCode).toBe(hopperReady ? 0 : 1); expect(result.json).toMatchObject({ - healthy: executableOnHost, + healthy: hopperReady, environment_healthy: false, - ...(executableOnHost ? { hopperPath: launcher } : {}), + hopperPath: launcher, checks: expect.arrayContaining([ expect.objectContaining({ name: "hopper", - ok: executableOnHost, + ok: true, detail: launcher, - classification: executableOnHost ? "healthy" : "config_drift", - ...(executableOnHost - ? {} - : { remediation: expect.stringContaining("HOPPER_LAUNCHER_PATH") }), + classification: "healthy", }), + ...(process.platform === "linux" + ? [ + expect.objectContaining({ + name: "hopper-version", + ok: false, + detail: launcher, + classification: "config_drift", + remediation: expect.stringContaining("HOPPER_LAUNCHER_PATH"), + }), + ] + : []), ]), }); - if (!executableOnHost) expect(result.json).not.toHaveProperty("hopperPath"); await expect(access(launchMarker)).rejects.toMatchObject({ code: "ENOENT", }); + if (process.platform === "linux") + expect(await readFile(loaderMarker, "utf8")).toBe(launcher); }, );