Prefer managed browser shell in integration defaults

This commit is contained in:
rcourtman 2026-03-25 16:53:55 +00:00
parent 2bd7c39b9b
commit 5d08a917f6
5 changed files with 70 additions and 8 deletions

View file

@ -285,6 +285,13 @@ the embedded frontend or test/standalone UI. They may not present `7655` as
the generic local Pulse browser target in a way that can be confused with the
managed hot-dev shell, whose canonical browser entrypoint remains
`http://127.0.0.1:5173`.
That same browser-target rule applies to the integration harness defaults.
`tests/integration/playwright.config.ts` and the shared integration browser/API
helpers may only fall back to `http://localhost:7655` after honoring an
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.
That runtime-guidance rule also applies to successful launcher startup output:
`hot-dev-bg` must identify `http://127.0.0.1:5173` as the browser entrypoint
and present `7655` as the managed backend dependency, rather than advertising

View file

@ -612,6 +612,25 @@ test_acceptance_doc_distinguishes_embedded_frontend_from_hot_dev() {
assert_contains "acceptance doc distinguishes managed hot-dev shell" "${output}" "managed hot-dev browser shell on \`http://127.0.0.1:5173\`"
}
test_playwright_defaults_prefer_managed_hot_dev_runtime() {
local output
output="$(cat "${ROOT_DIR}/tests/integration/playwright.config.ts")"
assert_contains "playwright config checks managed hot-dev pid" "${output}" "managedHotDevPidPath"
assert_contains "playwright config computes managed browser base url" "${output}" "managedDevBrowserBaseURL"
assert_contains "playwright config prefers runtime-state first" "${output}" "loadRuntimeBaseURL() ||"
assert_contains "playwright config prefers managed dev browser before embedded fallback" "${output}" "managedDevBrowserBaseURL() ||"
}
test_integration_helpers_prefer_managed_hot_dev_runtime() {
local output
output="$(cat "${ROOT_DIR}/tests/integration/tests/helpers.ts")"
assert_contains "integration helpers check managed hot-dev pid" "${output}" "managedHotDevPidPath"
assert_contains "integration helpers compute managed browser base url" "${output}" "managedDevBrowserBaseURL"
assert_contains "integration helpers prefer managed browser before embedded fallback" "${output}" "managedDevBrowserBaseURL() ||"
}
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)"

View file

@ -148,6 +148,7 @@ 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 integration browser defaults now prefer the managed hot-dev browser shell on `http://127.0.0.1:5173` whenever `hot-dev-bg` is already running, and only fall back to the embedded frontend on `:7655` when there is no managed dev session to attach to.
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

@ -4,16 +4,18 @@ import { fileURLToPath } from 'node:url';
import { defineConfig, devices } from '@playwright/test';
const configDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(configDir, '..', '..');
const managedHotDevPidPath = path.join(repoRoot, 'tmp', 'hot-dev.bg.pid');
const runtimeStatePath = (() => {
const configuredPath = String(process.env.PULSE_E2E_RUNTIME_STATE_PATH || '').trim();
if (configuredPath === '') {
return path.resolve(configDir, '..', '..', 'tmp', 'e2e-runtime-state.json');
return path.resolve(repoRoot, 'tmp', 'e2e-runtime-state.json');
}
return path.isAbsolute(configuredPath)
? configuredPath
: path.resolve(configDir, '..', '..', configuredPath);
return path.isAbsolute(configuredPath) ? configuredPath : path.resolve(repoRoot, configuredPath);
})();
const trim = (value: unknown): string => String(value ?? '').trim();
const loadRuntimeBaseURL = (): string | null => {
try {
const raw = fs.readFileSync(runtimeStatePath, 'utf8');
@ -26,6 +28,21 @@ const loadRuntimeBaseURL = (): string | null => {
}
};
const managedDevBrowserBaseURL = (): string | null => {
try {
const pid = Number.parseInt(fs.readFileSync(managedHotDevPidPath, 'utf8').trim(), 10);
if (!Number.isInteger(pid) || pid <= 0) {
return null;
}
process.kill(pid, 0);
const host = trim(process.env.FRONTEND_DEV_HOST) || '127.0.0.1';
const port = trim(process.env.FRONTEND_DEV_PORT) || '5173';
return `http://${host}:${port}`;
} catch {
return null;
}
};
/**
* Playwright configuration for Pulse update integration tests
* See https://playwright.dev/docs/test-configuration
@ -65,6 +82,7 @@ export default defineConfig({
process.env.PULSE_BASE_URL ||
process.env.PLAYWRIGHT_BASE_URL ||
loadRuntimeBaseURL() ||
managedDevBrowserBaseURL() ||
'http://localhost:7655',
/* Allow testing against self-signed TLS when explicitly enabled */

View file

@ -9,14 +9,14 @@ import { fileURLToPath } from 'node:url';
import { Browser, Page, expect } from '@playwright/test';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..', '..', '..');
const managedHotDevPidPath = path.join(repoRoot, 'tmp', 'hot-dev.bg.pid');
const runtimeStatePath = (() => {
const configuredPath = String(process.env.PULSE_E2E_RUNTIME_STATE_PATH || '').trim();
if (configuredPath === '') {
return path.resolve(__dirname, '..', '..', '..', 'tmp', 'e2e-runtime-state.json');
return path.resolve(repoRoot, 'tmp', 'e2e-runtime-state.json');
}
return path.isAbsolute(configuredPath)
? configuredPath
: path.resolve(__dirname, '..', '..', '..', configuredPath);
return path.isAbsolute(configuredPath) ? configuredPath : path.resolve(repoRoot, configuredPath);
})();
type RuntimeState = {
@ -40,6 +40,21 @@ const runtimeBaseURL = (): string | null => {
: null;
};
const managedDevBrowserBaseURL = (): string | null => {
try {
const pid = Number.parseInt(fs.readFileSync(managedHotDevPidPath, 'utf8').trim(), 10);
if (!Number.isInteger(pid) || pid <= 0) {
return null;
}
process.kill(pid, 0);
const host = String(process.env.FRONTEND_DEV_HOST || '').trim() || '127.0.0.1';
const port = String(process.env.FRONTEND_DEV_PORT || '').trim() || '5173';
return `http://${host}:${port}`;
} catch {
return null;
}
};
const runtimePrimaryAPIToken = (): string => {
const parsed = readRuntimeState();
return typeof parsed?.primaryAPIToken === 'string' ? parsed.primaryAPIToken.trim() : '';
@ -383,6 +398,7 @@ export async function createAuthenticatedStorageState(
process.env.PULSE_BASE_URL ||
process.env.PLAYWRIGHT_BASE_URL ||
runtimeBaseURL() ||
managedDevBrowserBaseURL() ||
'http://localhost:7655',
});
const page = await context.newPage();
@ -620,6 +636,7 @@ export async function apiRequest(page: Page, endpoint: string, options: any = {}
process.env.PULSE_BASE_URL ||
process.env.PLAYWRIGHT_BASE_URL ||
runtimeBaseURL() ||
managedDevBrowserBaseURL() ||
'http://localhost:7655'
).replace(/\/+$/, '');