Runbook 02a — BASE_URL discipline and proof

Validate that environment configuration controls navigation targets and that test code does not hardcode domains or rely on implicit defaults. This runbook is mode-aware: default mode forbids localhost; recording mode (CI_NAT_MODE=1) allows an explicit localhost exception.

env-driven config fail-fast validation no hardcoded hosts deterministic targeting mode-aware boundary

Objective + success criteria

Objective: Verify that the target environment is provided by the runtime environment (WEB_BASE_URL/API_BASE_URL, with optional BASE_URL compatibility) and that missing or incorrect values produce fast, diagnosable failures.

Rule: Targets are explicit and logged at runtime. Missing values fail fast with a clear message before any UI navigation.

Default boundary (CI_NAT_MODE!=1): localhost/loopback is forbidden for SUT HTTP. No port forwarding, no SSH tunnels, no “localhost as environment.”

Recording exception (CI_NAT_MODE=1): localhost is allowed only when explicitly set (e.g., http://127.0.0.1:3000) for sanitized recordings.

Success criteria:

  • When WEB_BASE_URL/API_BASE_URL (or compatibility BASE_URL) are unset, validation fails immediately with a clear error
  • When targets are incorrect, failure occurs at DNS/connection/timeout layer and points to the unreachable target
  • When targets are correct, tests target the intended SUT host and proceed to application assertions
  • Project contains no hardcoded hostnames used for environment selection

Environment contract

Prefer WEB_BASE_URL and API_BASE_URL. Use BASE_URL only as a compatibility alias (set it from WEB_BASE_URL if needed).

# Default (bridged/LAN)
export CI_NAT_MODE="0"
export WEB_BASE_URL="http://sut.testlab:3000"
export API_BASE_URL="http://sut.testlab:3001/api"

# Optional compatibility
export BASE_URL="${WEB_BASE_URL}"

Recording mode example (CI_NAT_MODE=1)

# Temporary for sanitized recordings
export CI_NAT_MODE="1"
export WEB_BASE_URL="http://127.0.0.1:3000"
export API_BASE_URL="http://127.0.0.1:3001/api"
export BASE_URL="${WEB_BASE_URL}"

Exact commands

1) Inspect configured target (Runner)

node -e '
console.log("CI_NAT_MODE=", process.env.CI_NAT_MODE || "0");
console.log("WEB_BASE_URL=", process.env.WEB_BASE_URL || "");
console.log("API_BASE_URL=", process.env.API_BASE_URL || "");
console.log("BASE_URL=", process.env.BASE_URL || "");
'

2) Fail-fast validation block (Runner/CI)

node -e '
const nat = process.env.CI_NAT_MODE === "1";
const web = process.env.WEB_BASE_URL || process.env.BASE_URL || "";
const api = process.env.API_BASE_URL || "";

if (!web) { console.error("CONFIG ERROR: WEB_BASE_URL (or BASE_URL) must be set"); process.exit(2); }
if (!api) { console.error("CONFIG ERROR: API_BASE_URL must be set"); process.exit(2); }

if (!nat) {
  const bad = /(localhost|127\.0\.0\.1|0\.0\.0\.0)/i;
  if (bad.test(web)) { console.error("CONFIG ERROR: WEB_BASE_URL cannot be localhost in default mode"); process.exit(2); }
  if (bad.test(api)) { console.error("CONFIG ERROR: API_BASE_URL cannot be localhost in default mode"); process.exit(2); }
}

console.log("CONFIG OK:", { CI_NAT_MODE: nat ? "1" : "0", WEB_BASE_URL: web, API_BASE_URL: api });
'

3) Unset targets and prove fail-fast (Runner)

unset WEB_BASE_URL API_BASE_URL BASE_URL
node -e 'console.log("WEB_BASE_URL=", process.env.WEB_BASE_URL || ""); console.log("API_BASE_URL=", process.env.API_BASE_URL || ""); console.log("BASE_URL=", process.env.BASE_URL || "")'

# run the validation block above; it must exit non-zero with clear messaging

4) Set an incorrect target and run (Runner)

export CI_NAT_MODE="0"
export WEB_BASE_URL="http://sut.testlab:3999"
export API_BASE_URL="http://sut.testlab:3001/api"
export BASE_URL="${WEB_BASE_URL}"

# Optional: prove reachability fails outside Playwright
curl -sv --max-time 3 "${WEB_BASE_URL}/" || true

# Then run a single proof test 
npx playwright test -g "RW\.NET\.001" --project=chromium --reporter=line

5) Set correct targets and run (Runner)

export CI_NAT_MODE="0"
export WEB_BASE_URL="http://sut.testlab:3000"
export API_BASE_URL="http://sut.testlab:3001/api"
export BASE_URL="${WEB_BASE_URL}"

curl -sS -o /dev/null -D - --max-time 3 "${WEB_BASE_URL}/" | head -n 12
curl -sS -o /dev/null -D - --max-time 3 "${API_BASE_URL}/tags" | head -n 12

npx playwright test -g "RW\.NET\.001" --project=chromium --reporter=line

6) Recording mode example (CI_NAT_MODE=1)

export CI_NAT_MODE="1"
export WEB_BASE_URL="http://127.0.0.1:3000"
export API_BASE_URL="http://127.0.0.1:3001/api"
export BASE_URL="${WEB_BASE_URL}"

curl -sS -o /dev/null -D - --max-time 3 "${WEB_BASE_URL}/" | head -n 12
curl -sS -o /dev/null -D - --max-time 3 "${API_BASE_URL}/tags" | head -n 12

npx playwright test -g "RW\.NET\.001" --project=chromium --reporter=line

7) Prove “no hardcoded hosts” (Repo)

# These should return nothing (or only documentation references)
grep -R "http://sut\.testlab" -n pages tests fixtures playwright.config.* || true
grep -R "http://127\.0\.0\.1" -n pages tests fixtures playwright.config.* || true
grep -R "localhost:" -n pages tests fixtures playwright.config.* || true

If the project still uses a legacy variable (e.g., BASE_URL_TODO), document it here and map it to WEB_BASE_URL explicitly rather than adding more implicit fallback behavior.

Expected outputs (logs, screenshots, artifacts)

Targets unset Fail immediately with a configuration error. No UI navigation required.
Targets incorrect Failure occurs at DNS/connection/timeout layer; logs clearly show the target used.
Targets correct Navigation hits the intended SUT host and proceeds into app-level assertions.
Default boundary When CI_NAT_MODE!=1, localhost targets are rejected.
Recording exception When CI_NAT_MODE=1, localhost targets are allowed and succeed.
Evidence to capture Console echo of vars + fail-fast output + Playwright artifacts if enabled.

Sanitize hostnames and LAN IPs if publishing outputs.

Failure modes + how to diagnose

Silent fallback (BASE_URL not enforced)

Symptom: tests run using an implicit default target when vars are missing.

Diagnosis: inspect config and logs for any default navigation target.

grep -R "baseURL" -n playwright.config.* fixtures pages tests || true
grep -R "process\.env\." -n fixtures pages tests playwright.config.*

Incorrect variable name used

Symptom: you set vars but framework reads something else.

Diagnosis: search for environment reads and standardize on WEB_BASE_URL/API_BASE_URL.

grep -R "process\.env\." -n fixtures pages tests playwright.config.*

Targets correct but connectivity fails

Symptom: config looks right; network errors occur.

Diagnosis: validate reachability outside Playwright.

curl -sv --max-time 3 "${WEB_BASE_URL}/" || true
curl -sv --max-time 3 "${API_BASE_URL}/tags" || true
nslookup sut.testlab || true

Why it matters (production relevance)

Environment-driven targeting prevents configuration drift. It enables deterministic execution across developer machines, CI runners, and staged environments without code changes, improving diagnosability and reducing “works on my machine” artifacts. Mode-aware rules prevent accidental boundary bypass while still supporting sanitized recordings.