Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,9 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-07-30 | PR-1497 | 978459f8788568be7aa0dd4d6a8b309a3d2d077e | PR #1497 final current-main review and typecheck repair | APPROVE after fixing Error.code narrowing in the offline readiness test; no remaining P0-P2 findings. | check:codex-cloud PASS; full Vitest 444 files / 4644 passed / 3 skipped; readiness focused 6/6; tsc --noEmit PASS; issue and ledger guards PASS; Prettier and diff checks PASS |
| 2026-07-30 | PR-1497 | 5bcf26b12b89e89539a3dfc903155cc49475a68b | PR #1497 append-only ledger reconciliation | APPROVE: existing ledger order restored; exact PR diff is three append-only review rows; no remaining findings | typecheck PASS on repaired code; parent unit coverage PASS; branch-review-ledger PASS; unresolved threads 0; fresh hosted CI required |
| 2026-07-30 | PR-1497 | b0243464df533a94199b670f1bf0563d84d3f4d6 | PR #1497 combined exact-head review after concurrent main sync | APPROVE; retained all append-only records and the type-safe timeout diagnostic; no remaining P0-P2 findings. | check:codex-cloud PASS; full Vitest 444 files / 4644 passed / 3 skipped; readiness 6/6; tsc --noEmit PASS; issue/ledger/format/diff/final audit PASS |
| 2026-07-30 | codex/playwright-container-alignment | 5ce50f64993a43efb00c4f8cfa86c26c895b8532 | issue 121 container browser fallback | approved: managed browser remains preferred; immutable-container fallback is explicit, newest-compatible, logged, unit-pinned, and launch-proven | verify:cheap 443 files/4631 pass; focused vitest 37/37; fallback Chromium launch; focused Playwright 1/1; check:rag:fixtures; outstanding guard; diff check |
| 2026-07-30 | codex/playwright-container-alignment | 70a603087ae7ea9c0e6db0701aa13ffdddb79081 | preinstalled Chromium fallback | P2 fixed: Linux fallback now filters by process architecture, preventing an x64-only shell from being selected on arm64. No remaining findings. | 2 files/38 tests; Prettier; ESLint; git diff --check |
| 2026-07-30 | codex/playwright-container-alignment | 936cab24f00202081aad780f8712152409a212d3 | container browser fallback review fixes | approved: automated P2s fixed by architecture filtering and designated /opt/pw-browsers root; generic stale caches fail closed | focused vitest 11/11; ESLint; Prettier; outstanding guard; diff check |
| 2026-07-30 | PR-1507 | 12e5c8c977a873a7b900596fdc310a1a78d863f0 | PR #1507 final current-main review | APPROVE; architecture-specific container lookup and immutable-root fail-closed fallback are preserved after current-main reconciliation; no remaining P0-P2 findings. | focused Vitest 2 files / 40 tests PASS; tsc --noEmit PASS; issue/ledger/format/diff guards PASS |
| 2026-07-30 | PR-1507 | 12e5c8c977a873a7b900596fdc310a1a78d863f0 | PR #1507 current-main merge and ledger normalization | APPROVED — no findings after current-main sync; feature diff unchanged and ledger reduced to three unique append-only review rows. | focused Vitest 2 files/40 tests PASS; outstanding-issues PASS; branch-review-ledger PASS; diff check PASS; merge-tree f8f88bd79706c2fb36637014a25e93af6e08eb99 |
| 2026-07-30 | PR-1507 | 5e22b89f7bdb73335d12a0cf4091915615b20dd7 | PR #1507 remote ancestry reconciliation | APPROVED — identical-tree remote merge ancestry reconciled without content change; no remaining findings. | focused Vitest 2 files/40 tests PASS on identical tree; issue and ledger guards PASS; diff check PASS; merge-tree d6594063a4aa2c5f8b7a9ec72c1c41c94e6937fa |
2 changes: 1 addition & 1 deletion docs/outstanding-issues.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Reference examples: `tests/icon-button.dom.test.tsx` (accessible-name contract),

## Playwright ownership

The repository runner exclusively builds and serves each Playwright production app. It selects a safe port, verifies `/api/local-project-id`, uses an isolated `.next-playwright/<run-id>` build directory, replaces provider configuration with inert loopback values, and removes its server and output on success, failure, or signal. Playwright configuration never starts a server. The production boot guard permits this demo profile only when the output is isolated, provider mode is offline, credentials are absent, and the Supabase URL is the inert `127.0.0.1:1` target. Before acquiring the heavy lock or building, the runner preflights the Chromium (or requested Firefox/WebKit) executable — including the default `chrome-headless-shell` binary and any `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` override — and exits non-zero immediately when it is missing, so a launch-infra failure cannot be mistaken for product-test failures after a multi-minute build.
The repository runner exclusively builds and serves each Playwright production app. It selects a safe port, verifies `/api/local-project-id`, uses an isolated `.next-playwright/<run-id>` build directory, replaces provider configuration with inert loopback values, and removes its server and output on success, failure, or signal. Playwright configuration never starts a server. The production boot guard permits this demo profile only when the output is isolated, provider mode is offline, credentials are absent, and the Supabase URL is the inert `127.0.0.1:1` target. Before acquiring the heavy lock or building, the runner preflights the Chromium (or requested Firefox/WebKit) executable — including the default `chrome-headless-shell` binary and any `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` override — and exits non-zero immediately when it is missing, so a launch-infra failure cannot be mistaken for product-test failures after a multi-minute build. The designated download-disabled container image is the one exception: when `PLAYWRIGHT_BROWSERS_PATH` is exactly `/opt/pw-browsers` and the client-pinned shell is absent, the runner selects the newest preinstalled shell for the current platform and CPU architecture, then passes its exact path to Playwright. It logs that fallback before the build; generic shared caches still fail closed even when `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`, so a developer cache cannot silently bless a stale browser.

When capturing Playwright or `verify:phone-chrome` output through a shell pipe (`cmd 2>&1 | tee …`), enable `set -o pipefail` (or avoid the pipe). Without it, bash reports the pipeline exit from `tee` (`0`) while the log still ends in `N failed` — a measurement artifact that previously looked like a green-when-broken gate (outstanding-issues #120). The Node runners themselves already propagate Playwright’s exit status.

Expand Down
103 changes: 100 additions & 3 deletions scripts/playwright-browser-preflight.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { existsSync, readdirSync } from "node:fs";
import path from "node:path";
import { chromium, firefox, webkit } from "playwright";

Expand Down Expand Up @@ -46,6 +46,65 @@ const CHROMIUM_HEADLESS_SHELL_LAYOUTS = Object.freeze({
"chrome-win64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"],
});

const PREINSTALLED_CHROMIUM_LAYOUTS = Object.freeze({
linux: {
x64: [
["chrome-headless-shell-linux64", "chrome-headless-shell"],
["chrome-linux", "headless_shell"],
],
arm64: [["chrome-linux", "headless_shell"]],
},
darwin: {
x64: [["chrome-headless-shell-mac-x64", "chrome-headless-shell"]],
arm64: [["chrome-headless-shell-mac-arm64", "chrome-headless-shell"]],
},
win32: [["chrome-headless-shell-win64", "chrome-headless-shell.exe"]],
});

function preinstalledChromiumLayouts(platform = process.platform, architecture = process.arch) {
if (platform === "linux" || platform === "darwin") {
return PREINSTALLED_CHROMIUM_LAYOUTS[platform][architecture] ?? [];
}
return PREINSTALLED_CHROMIUM_LAYOUTS[platform] ?? [];
}

/**
* Find the newest headless shell supplied by a download-disabled container.
*
* This is intentionally narrower than scanning every Playwright cache: a stale
* developer cache must still fail closed. The caller separately validates the
* designated immutable-container root before invoking this search.
*/
export function newestPreinstalledChromiumHeadlessShell(
browsersRoot,
{
fileExists = existsSync,
readDirectory = readdirSync,
platform = process.platform,
architecture = process.arch,
} = {},
) {
if (!browsersRoot) return null;
let directories;
try {
directories = readDirectory(browsersRoot, { withFileTypes: true });
} catch {
return null;
}
const revisions = directories
.filter((entry) => entry.isDirectory() && /^chromium_headless_shell-\d+$/.test(entry.name))
.map((entry) => ({ name: entry.name, revision: Number(entry.name.slice("chromium_headless_shell-".length)) }))
.sort((left, right) => right.revision - left.revision);
const layouts = preinstalledChromiumLayouts(platform, architecture);
for (const directory of revisions) {
for (const layout of layouts) {
const executable = path.join(browsersRoot, directory.name, ...layout);
if (fileExists(executable)) return executable;
}
}
return null;
}

/**
* Derive the default headless-shell binary Playwright launches for Chromium
* tests. `chromium.executablePath()` points at full Chrome for Testing; the
Expand Down Expand Up @@ -96,7 +155,17 @@ function browserFamilyForProject(project) {
return PROJECT_BROWSER_FAMILIES[project] ?? null;
}

export function resolvePlaywrightBrowserExecutable(family, env = process.env) {
export function resolvePlaywrightBrowserExecutable(
family,
env = process.env,
{
managedChromiumPath = defaultChromiumHeadlessShellPath(),
fileExists = existsSync,
platform = process.platform,
architecture = process.arch,
containerBrowsersRoot = platform === "linux" ? "/opt/pw-browsers" : null,
} = {},
) {
if (family === "chromium") {
const override = env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH?.trim();
if (override) {
Expand All @@ -106,9 +175,37 @@ export function resolvePlaywrightBrowserExecutable(family, env = process.env) {
source: "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH",
};
}
if (managedChromiumPath && fileExists(managedChromiumPath)) {
return {
family,
path: managedChromiumPath,
source: "playwright chromium-headless-shell",
};
}
const downloadsDisabled = /^(?:1|true)$/i.test(env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD?.trim() ?? "");
const exposedBrowsersRoot = env.PLAYWRIGHT_BROWSERS_PATH?.trim();
const normalizedExposedRoot = exposedBrowsersRoot?.replaceAll("\\", "/").replace(/\/+$/, "");
const normalizedContainerRoot = containerBrowsersRoot?.replaceAll("\\", "/").replace(/\/+$/, "");
const designatedContainerRoot =
normalizedExposedRoot && normalizedContainerRoot && normalizedExposedRoot === normalizedContainerRoot;
if (downloadsDisabled && designatedContainerRoot) {
const preinstalled = newestPreinstalledChromiumHeadlessShell(exposedBrowsersRoot, {
fileExists,
platform,
architecture,
});
if (preinstalled) {
return {
family,
path: preinstalled,
source: "preinstalled container Chromium (PLAYWRIGHT_BROWSERS_PATH)",
managedPath: managedChromiumPath,
};
}
}
return {
family,
path: defaultChromiumHeadlessShellPath(),
path: managedChromiumPath,
source: "playwright chromium-headless-shell",
};
}
Expand Down
11 changes: 10 additions & 1 deletion scripts/run-playwright.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,16 @@ const mockupProjectRequested =
// Fail loud on missing browser binaries before the heavy lock or production build.
// Otherwise launch failures surface as "N failed" product tests and are easy to misread
// when a caller pipes output without `pipefail` (outstanding-issues #120).
assertPlaywrightBrowsersReady(playwrightArgs);
const browserPreflight = assertPlaywrightBrowsersReady(playwrightArgs);
const preinstalledChromium = browserPreflight.checked.find(
(entry) => entry.source === "preinstalled container Chromium (PLAYWRIGHT_BROWSERS_PATH)",
);
if (preinstalledChromium && !process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH = preinstalledChromium.path;
console.error(
`[playwright] Managed Chromium is unavailable; using the preinstalled container browser at ${preinstalledChromium.path}.`,
);
}

const runId = `${process.pid}-${Date.now()}`;
const relativeRunRoot = `.next-playwright/${runId}`;
Expand Down
82 changes: 82 additions & 0 deletions tests/playwright-browser-preflight.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
defaultChromiumHeadlessShellPath,
newestPreinstalledChromiumHeadlessShell,
playwrightBrowserPreflight,
playwrightProjectNames,
requestedPlaywrightBrowserProjects,
Expand Down Expand Up @@ -83,6 +87,84 @@ describe("playwright browser preflight", () => {
});
});

it("selects the newest shell only for the designated download-disabled container root", () => {
const root = mkdtempSync(join(tmpdir(), "pw-container-browsers-"));
const older = join(root, "chromium_headless_shell-1194", "chrome-linux", "headless_shell");
const newer = join(root, "chromium_headless_shell-1200", "chrome-linux", "headless_shell");
try {
mkdirSync(join(older, ".."), { recursive: true });
mkdirSync(join(newer, ".."), { recursive: true });
writeFileSync(older, "");
writeFileSync(newer, "");

expect(newestPreinstalledChromiumHeadlessShell(root, { platform: "linux", architecture: "x64" })).toBe(newer);

const managedPath = join(root, "chromium_headless_shell-1234", "missing");
expect(
resolvePlaywrightBrowserExecutable(
"chromium",
{
NODE_ENV: "test",
PLAYWRIGHT_BROWSERS_PATH: root,
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1",
},
{
managedChromiumPath: managedPath,
platform: "linux",
architecture: "x64",
containerBrowsersRoot: root,
},
),
).toMatchObject({
family: "chromium",
path: newer,
source: "preinstalled container Chromium (PLAYWRIGHT_BROWSERS_PATH)",
managedPath,
});
expect(
resolvePlaywrightBrowserExecutable(
"chromium",
{
NODE_ENV: "test",
PLAYWRIGHT_BROWSERS_PATH: root,
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1",
},
{ managedChromiumPath: managedPath, platform: "linux", architecture: "x64" },
),
).toEqual({
family: "chromium",
path: managedPath,
source: "playwright chromium-headless-shell",
});
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it("does not select an x64-only container shell on Linux arm64", () => {
const root = mkdtempSync(join(tmpdir(), "pw-container-architectures-"));
const x64Only = join(
root,
"chromium_headless_shell-1300",
"chrome-headless-shell-linux64",
"chrome-headless-shell",
);
const arm64Compatible = join(root, "chromium_headless_shell-1200", "chrome-linux", "headless_shell");
try {
mkdirSync(join(x64Only, ".."), { recursive: true });
mkdirSync(join(arm64Compatible, ".."), { recursive: true });
writeFileSync(x64Only, "");
writeFileSync(arm64Compatible, "");

expect(newestPreinstalledChromiumHeadlessShell(root, { platform: "linux", architecture: "x64" })).toBe(x64Only);
expect(newestPreinstalledChromiumHeadlessShell(root, { platform: "linux", architecture: "arm64" })).toBe(
arm64Compatible,
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it("fails closed when the required Chromium binary is missing", () => {
const result = playwrightBrowserPreflight(["--project=chromium"], {
NODE_ENV: "test",
Expand Down
2 changes: 2 additions & 0 deletions tests/test-runner-safety.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,8 @@ describe("provider-safe test environment", () => {
);
expect(preflight).toContain("chromium_headless_shell");
expect(preflight).toContain("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH");
expect(preflight).toContain("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD");
expect(runner).toContain("process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH = preinstalledChromium.path");
expect(packageJson.scripts["test:e2e:pr"]).toContain('--grep-invert "@quarantine|@mockup"');
expect(packageJson.scripts["test:e2e:regression"]).toContain('--grep-invert "@critical|@quarantine|@mockup"');
expect(baseUrl.indexOf("if (!allowEnsure)")).toBeLessThan(baseUrl.indexOf("findExistingLocalProjectUrl();"));
Expand Down
Loading