Landing blocker for #61 and #63: the deployment checkout is 12 commits behind and `merge --ff-only` refuses while tracked paths are dirty. Discard is not available here (workspace policy: no `checkout`/`stash` on shared state), so the dirt is committed. - `agent/APPEND_SYSTEM.md`: the point-of-use / one-work-log / PR-link doctrine, already byte-identical to `origin/main` (`cmp` against the trunk blob: no difference). No new prose — the commit exists so the trunk merge can proceed. - `agent/config.yml` and `agent/managed-skills/**`: the machine-live tier the heartbeat owns (AGENTS.md § Live-sync tiers). `ALLOW_SECRETS=1` was used, deliberately and after auditing the staged diff by hand: the only credential-shaped strings in it are two copies of `AKIA0000000000000EXAMPLE` — the inert all-zero AWS example id — and they appear inside the two skill pages that teach that exact placeholder shape. This checkout is still running the pre-#61 hook; the merged gate (value-scoped placeholder filter, `71601f7`) exempts this value, so the refusal dies with this landing. No `--no-verify` was used: the gate ran and its verdict is recorded here. Refs #61
35 lines
2.1 KiB
Markdown
35 lines
2.1 KiB
Markdown
---
|
|
name: e2e-port-squat-triage
|
|
description: "When an e2e seam on a fixed port fails weirdly (404s on routes that exist, stale behavior, health probe passes but app seems ancient), audit for leftover dev/debug servers squatting the port before believing any red or green."
|
|
---
|
|
|
|
# E2E fixed-port squat triage
|
|
|
|
## Symptom classes (all seen with a stale server squatting the seam's fixed port)
|
|
- Routes that exist in current `dist`/source return 404 ("Cannot POST /api/...") while unit tests through the same app factory pass.
|
|
- A seam that is *green* but exercises old code: its setup curls `http://host:PORT/health` and gets a response from **whoever owns the port** — a leftover server, not the one it just spawned (the spawned one dies or idles with EADDRINUSE, sometimes still showing in `pgrep`).
|
|
- Behavior matches an older build (missing mounts/guards) despite a fresh build step.
|
|
|
|
## Triage recipe (before touching code)
|
|
1. Who owns the port:
|
|
```bash
|
|
ss -tlnp 2>/dev/null | grep :PORT
|
|
```
|
|
2. List every server process with **start times**:
|
|
```bash
|
|
for p in $(pgrep -f "path/to/server-entry"); do ps -o pid,lstart,cmd -p $p --no-headers; done
|
|
```
|
|
3. Compare start times against the build artifact timestamps (`ls -la dist/...`). A server older than the dist you just built is a liar — its in-memory modules predate the code under test.
|
|
4. Kill all leftovers, then re-check: `pgrep -f "server-entry" || echo all-dead`.
|
|
5. Rerun the seam. Only now is red/red and green/green meaningful.
|
|
|
|
## Why it happens
|
|
- `kill %1` in **non-interactive** bash fails silently (no job control) — use explicit PIDs or `pkill -f <pattern>`.
|
|
- Seam setups that "wait for health" cannot distinguish their own server from a squatter on the same port; a pre-flight "port must be free" assertion is the permanent fix (assert *before* spawn).
|
|
|
|
## Hardening (if you own the seam)
|
|
Add to setup, before spawning: fail fast if the port is already listening — turns the silent liar into a loud error:
|
|
```bash
|
|
ss -tln "sport = :PORT" | grep -q LISTEN && { echo "port PORT busy — kill leftovers first" >&2; return 1; }
|
|
```
|