Prefer explicit Playwright browser base URL

This commit is contained in:
rcourtman 2026-04-10 17:26:18 +01:00
parent 367208d489
commit 659c5f5988
5 changed files with 175 additions and 10 deletions

View file

@ -109,7 +109,13 @@ server-side update execution surfaces.
browser base URL precedence so repo-root browser proofs attach to the live
managed hot-dev shell or runtime-state browser URL instead of silently
falling back to the embedded `:7655` frontend when a managed browser shell
is already the active truth.
is already the active truth. When both `PLAYWRIGHT_BASE_URL` and
`PULSE_BASE_URL` are present, browser attachment must prefer
`PLAYWRIGHT_BASE_URL` while backend-oriented setup and health helpers may
still use `PULSE_BASE_URL`. That shared helper must also honor
`PULSE_E2E_REPO_ROOT` for runtime-state and managed-session discovery so
isolated verification harnesses can relocate managed runtime state without
mutating the live repo root.
## Current State
@ -440,6 +446,11 @@ explicit base URL, runtime-state file, and any active managed `hot-dev`
session. If `hot-dev-bg` is already running, ad hoc Playwright/browser helpers
must prefer the managed shell on `http://127.0.0.1:5173` instead of silently
teaching the backend port as the default local browser target.
When both `PLAYWRIGHT_BASE_URL` and `PULSE_BASE_URL` are set, the shared
browser helper must treat `PLAYWRIGHT_BASE_URL` as the browser truth and leave
`PULSE_BASE_URL` available for backend-oriented health checks and setup
traffic, so split browser/backend proof can target fresh frontend code without
rewiring the API-side contract.
That defaulting rule must live in one shared integration helper rather than
being duplicated between config and helper files, so future browser-target
changes cannot leave Playwright navigation and browser/API helper calls

View file

@ -700,6 +700,9 @@ test_integration_readme_uses_managed_backend_restart_wrapper() {
assert_contains "integration readme names the owner process" "${output}" "owner process"
assert_contains "integration readme documents recovery layout proof" "${output}" "tests/17-recovery-layout.spec.ts"
assert_contains "integration readme documents patrol runtime-state proof" "${output}" "tests/18-patrol-runtime-state.spec.ts"
assert_contains "integration readme documents explicit browser override precedence" "${output}" "PLAYWRIGHT_BASE_URL"
assert_contains "integration readme documents backend base split" "${output}" "backend-oriented base"
assert_contains "integration readme documents split browser/backend example" "${output}" "PLAYWRIGHT_BASE_URL='http://127.0.0.1:4174'"
assert_not_contains "integration readme no longer documents raw backend restart script" "${output}" "./scripts/hot-dev-bg.sh backend-restart"
}
@ -709,7 +712,7 @@ test_integration_quick_start_distinguishes_embedded_frontend_from_hot_dev() {
assert_contains "integration quick start labels 7655 as embedded frontend" "${output}" "Pulse test UI (embedded frontend)"
assert_contains "integration quick start points local managed dev runtime to 5173" "${output}" "http://127.0.0.1:5173"
assert_contains "integration quick start names hot-dev browser shell" "${output}" "canonical hot-dev browser shell"
assert_contains "integration quick start names hot-dev browser shell" "${output}" "hot-dev browser shell"
}
test_acceptance_doc_distinguishes_embedded_frontend_from_hot_dev() {
@ -754,9 +757,10 @@ test_integration_helpers_prefer_managed_hot_dev_runtime() {
local output
output="$(cat "${ROOT_DIR}/tests/integration/tests/helpers.ts")"
assert_contains "integration helpers import shared browser defaults" "${output}" "import { preferredBrowserBaseURL, readRuntimeState } from './runtime-defaults';"
assert_contains "integration helpers import preferred browser helper" "${output}" "preferredBrowserBaseURL, readRuntimeState"
assert_contains "integration helpers import shared browser defaults" "${output}" "runtime-defaults"
assert_contains "integration helpers delegate browser context base url to shared helper" "${output}" "baseURL: preferredBrowserBaseURL(),"
assert_contains "integration helpers delegate api request base url to shared helper" "${output}" "const baseURL = preferredBrowserBaseURL().replace(/\\/+\$/, '');"
assert_contains "integration helpers delegate api request base url to shared helper" "${output}" "const baseURL = preferredBrowserBaseURL().replace"
}
test_integration_runtime_defaults_centralize_managed_browser_detection() {
@ -769,6 +773,68 @@ test_integration_runtime_defaults_centralize_managed_browser_detection() {
assert_contains "runtime defaults expose preferred browser base url" "${output}" "export const preferredBrowserBaseURL ="
}
test_integration_runtime_defaults_prefer_explicit_browser_override() {
local output
output="$(
ROOT_DIR="${ROOT_DIR}" \
node --input-type=module <<'EOF'
import path from 'node:path';
const rootDir = process.env.ROOT_DIR;
const modulePath = path.join(rootDir, 'tests', 'integration', 'tests', 'runtime-defaults.ts');
const { preferredBrowserBaseURL } = await import(`file://${modulePath}`);
console.log(
preferredBrowserBaseURL({
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:4174',
PULSE_BASE_URL: 'http://127.0.0.1:7655',
}),
);
EOF
)"
assert_contains "runtime defaults prefer explicit Playwright browser override" "${output}" "http://127.0.0.1:4174"
assert_not_contains "runtime defaults do not reuse backend base as browser target when override exists" "${output}" "http://127.0.0.1:7655"
}
test_integration_runtime_defaults_honor_repo_root_override() {
local output
output="$(
ROOT_DIR="${ROOT_DIR}" \
node --input-type=module <<'EOF'
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
const rootDir = process.env.ROOT_DIR;
const modulePath = path.join(rootDir, 'tests', 'integration', 'tests', 'runtime-defaults.ts');
const { managedDevBrowserBaseURL, runtimeStatePath } = await import(`file://${modulePath}`);
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'pulse-runtime-defaults-'));
await fs.mkdir(path.join(repoRoot, 'tmp'), { recursive: true });
await fs.writeFile(path.join(repoRoot, 'tmp', 'hot-dev.bg.pid'), `${process.pid}\n`, 'utf8');
try {
console.log(
`stateMatches=${runtimeStatePath({ PULSE_E2E_REPO_ROOT: repoRoot }) === path.join(repoRoot, 'tmp', 'e2e-runtime-state.json')}`,
);
console.log(
`managed=${managedDevBrowserBaseURL({
PULSE_E2E_REPO_ROOT: repoRoot,
FRONTEND_DEV_HOST: '127.0.0.1',
FRONTEND_DEV_PORT: '4174',
})}`,
);
} finally {
await fs.rm(repoRoot, { recursive: true, force: true });
}
EOF
)"
assert_contains "runtime defaults resolve runtime-state path from overridden repo root" "${output}" "stateMatches=true"
assert_contains "runtime defaults resolve managed dev browser url from overridden repo root" "${output}" "managed=http://127.0.0.1:4174"
}
test_clean_mock_alerts_prefers_managed_runtime() {
local test_dir fake_bin alert_history fake_hot_dev_bg action_log output
test_dir="$(mktemp -d)"
@ -1012,7 +1078,14 @@ main() {
test_hot_dev_bg_script_advertises_managed_entrypoint
test_hot_dev_bg_usage_prefers_managed_wrappers
test_integration_readme_uses_managed_backend_restart_wrapper
test_integration_quick_start_distinguishes_embedded_frontend_from_hot_dev
test_acceptance_doc_distinguishes_embedded_frontend_from_hot_dev
test_playwright_defaults_prefer_managed_hot_dev_runtime
test_root_playwright_wrapper_prefers_managed_browser_runtime
test_integration_helpers_prefer_managed_hot_dev_runtime
test_integration_runtime_defaults_centralize_managed_browser_detection
test_integration_runtime_defaults_prefer_explicit_browser_override
test_integration_runtime_defaults_honor_repo_root_override
test_clean_mock_alerts_prefers_managed_runtime
test_dev_check_uses_managed_runtime_status
test_backend_restart_requires_managed_runtime

View file

@ -149,6 +149,20 @@ npm test -- tests/16-dev-runtime-recovery.spec.ts tests/17-recovery-layout.spec.
This mode attaches Playwright to the canonical dev browser entrypoint on `http://127.0.0.1:5173`, uses the repo-root managed runtime wrappers as the control surface, and writes browser runtime connection state for Playwright instead of targeting the backend port directly. Those wrappers are backed by `scripts/hot-dev-bg.sh`, but the wrapper surface is the canonical operator contract.
When you run the wrapper as `npm run dev:verify`, the managed launcher hands a verification-lock path into the integration runner, and the runner holds that lock for the actual pretest, Playwright, and posttest lifetime so unrelated backend source churn does not invalidate the recovery proofs mid-run.
When no explicit `PULSE_BASE_URL`, `PLAYWRIGHT_BASE_URL`, or runtime-state file is present, the shared integration browser-default helper now prefers the managed hot-dev browser shell on `http://127.0.0.1:5173` whenever `hot-dev-bg` is already running, and only falls back to the embedded frontend on `:7655` when there is no managed dev session to attach to.
When both are set, `PLAYWRIGHT_BASE_URL` now wins for the browser target while
`PULSE_BASE_URL` remains the backend-oriented base for pretest health checks,
explicit API helpers, and non-browser setup work. Use that split when you need
Playwright on fresh frontend code but still want backend provisioning or
health checks against the canonical Pulse API port.
Example:
```bash
cd tests/integration
PULSE_E2E_SKIP_DOCKER=1 \
PULSE_BASE_URL='http://127.0.0.1:7655' \
PLAYWRIGHT_BASE_URL='http://127.0.0.1:4174' \
npm test -- tests/30-setup-platform-connections-handoff.spec.ts --project=chromium
```
If the managed runtime is not already running, the harness starts it. If you need the harness to reclaim existing unmanaged `5173`/`7655` listeners first, add `PULSE_E2E_HOT_DEV_TAKEOVER=1`.

View file

@ -0,0 +1,61 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
managedDevBrowserBaseURL,
preferredBrowserBaseURL,
runtimeStatePath,
} from '../tests/runtime-defaults.ts';
test('preferredBrowserBaseURL lets PLAYWRIGHT_BASE_URL override the browser target', () => {
const resolved = preferredBrowserBaseURL({
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:4174',
PULSE_BASE_URL: 'http://127.0.0.1:7655',
});
assert.equal(resolved, 'http://127.0.0.1:4174');
});
test('preferredBrowserBaseURL still falls back to PULSE_BASE_URL when no browser override exists', () => {
const resolved = preferredBrowserBaseURL({
PULSE_BASE_URL: 'http://127.0.0.1:7655',
});
assert.equal(resolved, 'http://127.0.0.1:7655');
});
test('runtimeStatePath resolves the default path from an overridden integration repo root', () => {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-runtime-defaults-'));
try {
assert.equal(
runtimeStatePath({
PULSE_E2E_REPO_ROOT: repoRoot,
}),
path.join(repoRoot, 'tmp', 'e2e-runtime-state.json'),
);
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
});
test('managedDevBrowserBaseURL keeps the configured host and port when a managed session exists', () => {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-managed-dev-runtime-'));
fs.mkdirSync(path.join(repoRoot, 'tmp'), { recursive: true });
fs.writeFileSync(path.join(repoRoot, 'tmp', 'hot-dev.bg.pid'), `${process.pid}\n`);
try {
const resolved = managedDevBrowserBaseURL({
FRONTEND_DEV_HOST: '127.0.0.1',
FRONTEND_DEV_PORT: '4174',
PULSE_E2E_REPO_ROOT: repoRoot,
});
assert.equal(resolved, 'http://127.0.0.1:4174');
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
});

View file

@ -3,8 +3,6 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..', '..', '..');
const managedHotDevPidPath = path.join(repoRoot, 'tmp', 'hot-dev.bg.pid');
export type RuntimeState = {
baseURL?: string;
@ -13,12 +11,20 @@ export type RuntimeState = {
const trim = (value: unknown): string => String(value ?? '').trim();
const repoRootFromEnv = (env: NodeJS.ProcessEnv = process.env): string =>
trim(env.PULSE_E2E_REPO_ROOT) || path.resolve(__dirname, '..', '..', '..');
const managedHotDevPidPath = (env: NodeJS.ProcessEnv = process.env): string =>
path.join(repoRootFromEnv(env), 'tmp', 'hot-dev.bg.pid');
export const runtimeStatePath = (env: NodeJS.ProcessEnv = process.env): string => {
const configuredPath = trim(env.PULSE_E2E_RUNTIME_STATE_PATH);
if (configuredPath === '') {
return path.resolve(repoRoot, 'tmp', 'e2e-runtime-state.json');
return path.resolve(repoRootFromEnv(env), 'tmp', 'e2e-runtime-state.json');
}
return path.isAbsolute(configuredPath) ? configuredPath : path.resolve(repoRoot, configuredPath);
return path.isAbsolute(configuredPath)
? configuredPath
: path.resolve(repoRootFromEnv(env), configuredPath);
};
export const readRuntimeState = (
@ -45,7 +51,7 @@ export const managedDevBrowserBaseURL = (
env: NodeJS.ProcessEnv = process.env,
): string | null => {
try {
const pid = Number.parseInt(fs.readFileSync(managedHotDevPidPath, 'utf8').trim(), 10);
const pid = Number.parseInt(fs.readFileSync(managedHotDevPidPath(env), 'utf8').trim(), 10);
if (!Number.isInteger(pid) || pid <= 0) {
return null;
}
@ -61,8 +67,8 @@ export const managedDevBrowserBaseURL = (
export const preferredBrowserBaseURL = (
env: NodeJS.ProcessEnv = process.env,
): string =>
trim(env.PULSE_BASE_URL) ||
trim(env.PLAYWRIGHT_BASE_URL) ||
trim(env.PULSE_BASE_URL) ||
loadRuntimeBaseURL(env) ||
managedDevBrowserBaseURL(env) ||
'http://localhost:7655';