Runbook 02 — Explicit Targeting + Reachability (Sprint 2)

Enforce explicit targets (WEB_BASE_URL/API_BASE_URL) and prove the system is reachable over the network by hostname in default (bridged/LAN) mode. Supports a temporary NAT recording mode (CI_NAT_MODE=1) where localhost is an explicit exception.

target discipline compose availability port exposure runner reachability dependency boundary mode-aware proof

Objective + success criteria

Objective: Run tests only against an explicitly named, network-reachable SUT. Validate service availability locally on the SUT host and remotely from the runner via hostname.

Rule: No implicit defaults. If an env var is missing, fail fast. Targets are explicit and logged: WEB_BASE_URL and API_BASE_URL.

Boundary rule (default): In BRIDGED/LAN mode (CI_NAT_MODE!=1), the runner must reach the SUT by stable name (e.g., sut.testlab). No port forwarding, no SSH tunnels, no “localhost as environment”.

Recording exception: In NAT recording mode (CI_NAT_MODE=1), localhost is allowed because the SUT is exposed via local port-forwards (e.g., 127.0.0.1:3000, 127.0.0.1:3001). This is temporary and must be explicit.

Success criteria:

  • Targets are explicit (no implicit defaults)
  • Default mode rejects localhost/loopback for SUT HTTP
  • Service ports are exposed on the SUT host (LAN-reachable in default mode, not loopback-only)
  • HTTP response is returned locally on the SUT host
  • HTTP response is returned remotely from the runner using SUT hostname (default mode)
  • Dependencies are validated behind boundaries (DB verified from SUT, not exposed to runner)

✅ Sprint 2 — Completed (sanitized)

Sprint 2 is complete. The SUT is reachable by hostname from the runner, ports are explicitly published on the SUT host, and internal dependencies are validated strictly within the SUT boundary.

  • Runner DNS → TCP → HTTP reachability proven via sut.testlab (default mode)
  • Ports 3000 (web) and 3001 (api) are LAN-reachable (not loopback-only) in default mode
  • DB dependency verified from inside the SUT only (mongosh ping returns { ok: 1 })
  • No implicit targets; no “localhost as environment” pattern (except explicit NAT recording mode)
Private (operator): evidence blocks (actuals)
# SUT host (compose + published ports)
cd ~/conduit
docker compose up -d
docker compose ps
sudo ss -lntp | grep -E '(:3000\s|:3001\s)'

# SUT host (local HTTP)
curl -I http://127.0.0.1:3000/ | sed -n '1,8p'
curl -I http://127.0.0.1:3001/ | sed -n '1,12p'

# Runner (DNS + TCP + HTTP via hostname)
dscacheutil -q host -a name sut.testlab
nc -vz sut.testlab 3000
nc -vz sut.testlab 3001
curl -sS -D- http://sut.testlab:3000/ -o /dev/null | sed -n '1,8p'
curl -sS -D- http://sut.testlab:3001/ -o /dev/null | sed -n '1,12p'

# Dependency boundary proof (SUT host only)
docker exec -it conduit-mongo mongosh --quiet --eval 'db.adminCommand({ ping: 1 })'

Environment contract

Sprint 2 hardens “explicit targeting” by using separate web + API targets. BASE_URL may exist in older scripts, but Sprint 2’s contract is WEB_BASE_URL and API_BASE_URL.

# 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"

# Temporary (NAT recording mode)
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"

# Optional (compatibility only)
export BASE_URL="${WEB_BASE_URL}"

Guardrail: if CI_NAT_MODE!=1, targets must not contain localhost or 127.0.0.1.

Commands

0) Fail-fast: verify explicit targeting

Fail immediately if targets are missing or invalid for the selected mode.

: "${WEB_BASE_URL:?missing WEB_BASE_URL}"
: "${API_BASE_URL:?missing API_BASE_URL}"
echo "CI_NAT_MODE=${CI_NAT_MODE:-0}"
echo "WEB_BASE_URL=${WEB_BASE_URL}"
echo "API_BASE_URL=${API_BASE_URL}"

if [ "${CI_NAT_MODE:-0}" != "1" ]; then
  echo "${WEB_BASE_URL}" | grep -Eqi 'localhost|127\.0\.0\.1|0\.0\.0\.0' && { echo "FAIL: WEB_BASE_URL is localhost in default mode"; exit 1; }
  echo "${API_BASE_URL}" | grep -Eqi 'localhost|127\.0\.0\.1|0\.0\.0\.0' && { echo "FAIL: API_BASE_URL is localhost in default mode"; exit 1; }
fi

1) Service availability (SUT host)

Prove the stack is up and ports are published.

cd ~/conduit
docker compose up -d
docker compose ps

# verify listeners for expected ports
sudo ss -lntp | grep -E '(:3000\s|:3001\s)' || echo "NO LISTENER 3000/3001"
Private (operator): rebuild/pull + shutdown
cd ~/conduit
docker compose pull
docker compose up -d --build --remove-orphans
docker compose down

2) Local HTTP checks (SUT host)

Validate service response from the SUT host using the published ports.

curl -I http://127.0.0.1:3000/ | sed -n '1,8p'
curl -I http://127.0.0.1:3001/ | sed -n '1,12p'

3) Remote checks (Runner → SUT) — default mode

Default mode requires hostname-based reachability. This is where “no port forwarding” is enforced.

# Derive host from WEB_BASE_URL
SUT_HOST="$(echo "$WEB_BASE_URL" | sed -E 's#^https?://([^:/]+).*#\1#')"
echo "SUT_HOST=${SUT_HOST}"

# DNS proof (runner)
dscacheutil -q host -a name "${SUT_HOST}"

# TCP proof (runner)
nc -vz "${SUT_HOST}" 3000
nc -vz "${SUT_HOST}" 3001

# HTTP proof (runner) — must be by hostname in default mode
curl -sS -D- "http://${SUT_HOST}:3000/" -o /dev/null | sed -n '1,8p'
curl -sS -D- "http://${SUT_HOST}:3001/" -o /dev/null | sed -n '1,12p'

4) Recording mode example (CI_NAT_MODE=1)

Use this only when the SUT is operating in NAT mode and exposed via localhost port-forwards for recording safety. In this mode, runner reachability by hostname is not the goal; the explicit goal is “localhost is intentionally used and works.”

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"

# Boundary proof flips: localhost must respond
curl -fsS --max-time 5 "http://127.0.0.1:3000/" >/dev/null \
  && echo "PASS: localhost returned HTTP (NAT recording mode)" \
  || { echo "FAIL: localhost did not respond in NAT mode"; exit 1; }

# TCP proof (localhost)
nc -vz 127.0.0.1 3000
nc -vz 127.0.0.1 3001

# API identity proof
curl -sS -D- "http://127.0.0.1:3001/api/tags" -o /dev/null | sed -n '1,12p'

5) Dependency boundary proof (SUT host)

Validate internal dependencies from the SUT without exposing them to the runner.

docker exec -it conduit-mongo mongosh --quiet --eval 'db.adminCommand({ ping: 1 })'
Private (operator): DB state visibility
docker exec -it conduit-mongo mongosh --quiet --eval '
db = db.getSiblingDB("conduit");
db.getCollectionNames();
'

Public rule: outputs must be sanitized. Private blocks may contain actuals.

Expected outputs (logs, screenshots, artifacts)

Explicit targeting WEB_BASE_URL and API_BASE_URL printed in logs; missing vars fail fast.
docker compose ps Containers show Up state.
Port listening (default) SUT host shows listeners on LAN interface / 0.0.0.0:3000 and 0.0.0.0:3001 (not loopback-only).
Remote HTTP (default) Runner receives HTTP/1.1 status lines from sut.testlab endpoints.
Localhost HTTP (NAT mode) When CI_NAT_MODE=1, localhost checks succeed for 127.0.0.1:3000/3001.
DB ping (SUT) Mongo returns { ok: 1 }.

Failure modes + how to diagnose

Targets missing / wrong mode

Symptom: vars are blank, or default mode is accidentally pointing to localhost.

Action: verify the fail-fast block and the mode value.

Remote HTTP fails but local HTTP succeeds (default mode)

Symptom: SUT is healthy locally; runner cannot reach it.

Diagnosis (Runner):

nc -vz sut.testlab 3000
nc -vz sut.testlab 3001

Action: check firewall controls, port publishing (not loopback-only), and network segmentation.

CI_NAT_MODE=1 but localhost does not respond

Symptom: curl to 127.0.0.1:3000 times out/refuses.

Likely cause: port-forward missing, VM stopped, or services down.

Action: verify VirtualBox forwards and SUT service health.

Private (operator): route and IP-level checks
# keep raw IPs private
# route/arp, firewall rules, and host NIC checks

Why it matters (production relevance)

Availability validation reduces uncertainty before test execution. Target discipline ensures failures reflect real service or network conditions rather than incomplete startup or local substitution.

Default mode proves production-real reachability by stable name across the network boundary. NAT recording mode is a temporary, explicit exception used to avoid exposing LAN details during recordings.