Non-blocking points of the review of PR #47.
- TOKEN and INGEST_TOKEN are no longer read from the environment, like
the URLs: two inherited values that happened to be equal broke the
compartmentalisation cases. Pinned in the proxy case, which now runs
with one inherited value for both.
- The refusal cases run each bad value (not a number, 0, above 65535)
on each of the two variables.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test-curl.sh booted its servers on 3099 and 3098, hardcoded, and
ci.test.js runs it whole on every `npm test`: two runs side by side got
in each other's way, at least one of them failing every time.
- TEST_CURL_PORT and TEST_CURL_NOINGEST_PORT choose the ports, 3099 and
3098 staying the defaults. Names of their own rather than PORT, which
a dev shell often exports already.
- BASE_URL and NOINGEST_URL are derived from the ports and no longer
read from the environment: overridden alone, they sent every curl away
from the servers the script had started.
- A value outside 1-65535 and two identical ports are refused up front,
with the variable named.
- ci.test.js gives every case free ports of its own (freePorts() keeps
its probes open together so they are distinct), and no case listens on
3099 or 3098 any more. New cases: two passes at once, the four
refusals, source pins on the defaults and on the derived URLs, and a
first-try connection check that pins the `wait` of the trap, which the
fixed ports used to pin by accident.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second review of PR #45, non-blocking: two behaviours were right by hand
and held by no test.
- a listener on 3099 that closes at once, then one that accepts and stalls:
the port rule is "anything but connection refused", not "an HTTP answer"
- a server that stays alive and never listens: the wait ends on its
deadline, with a message and exit 1
Also moves the port check above the "Fixtures" comment it had split from
its mkdir lines, and counts four protections in CLAUDE.md, not three.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of PR #45: wait_for did not keep a neighbour out. With a listener
already on 3099, the server started by the script only dies of EADDRINUSE
after the wait loop has reached the neighbour, and the cases then ran
against it.
- require_free checks both ports before anything starts: free means
"connection refused" (curl exit 7), as in scripts/boot-from-copy.sh
- wait_for runs on a clock deadline, and checks the process again after a
successful probe
- the EXIT trap waits for the servers it killed, so a run started right
after finds the two ports free
ci.test.js adds a real neighbour on each port, and bounds the two
failed-start cases to 3 s: a message that does not stop the run is now red.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Behind a proxy from the environment, test-curl.sh sent its 18 curl lines to
the proxy instead of the servers it boots: with a dead proxy it exited 7
after 11 s without a line of output.
- export NO_PROXY and no_proxy, as scripts/boot-from-copy.sh does
- unset FORCE_COLOR: node colours a number even into a pipe, and 8 cases
compared "3" against a coloured "3"
- wait_for checks the server process before the port and stops the run with
a message when a server died or never answered
- the EXIT trap guards each kill: under set -e, a failed kill on the first
server ended the trap and left the second one running on 3098
ci.test.js runs the whole script three times: behind a fake proxy that
answers everything, and with a fake node that fails for each port in turn.
Fixes#41
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four cases on top of the 41 from #26: the file served byte for byte
(non-canonical fixture, compared with cmp so the trailing newline counts),
no_data when absent, 401 without a token and 401 with the ingest token.
Refs #27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test-curl.sh predates vitest and still claimed to be the authoritative suite
for one endpoint. It is now the socket-level pass, and its header says so:
vitest covers the logic, this script covers what only a real client and a
real TCP connection can show.
Twenty-four cases for POST /hosts/<id> and GET /hosts, chosen for what they
prove rather than for coverage: the 413 answered before the connection is
cut, the two tokens refusing each other's routes in both directions, 401
landing ahead of 403 so an unauthenticated caller cannot probe the allowlist,
the route regex turning a malformed id and a traversal attempt into 404
before any handler runs, and the ingest-persist-read round trip closing on a
GET that shows the pushed hostname online.
Two cases needed care to be worth anything:
The hostile payload splices "__proto__" into the JSON as text. Written as
__proto__: in an object literal it would set the prototype and JSON.stringify
would emit nothing, leaving the case asserting against a payload that never
carried the key. It now checks the response and the persisted file, whose key
set must be exactly the whitelist.
The 413 case runs under set -e while the server destroys the socket right
after flushing the response, so curl can exit 55/56 having already read the
status line. Every probe goes through an http_code helper that absorbs that;
a curl which truly got nothing reports 000, which fails the case rather than
passing it quietly. -H "Expect:" also suppresses the 100-continue handshake,
which otherwise changes which side notices the reset first.
The fail-closed 503 needs a server booted without HOSTS_INGEST_TOKEN, so the
script now runs a second instance on 3098 for it, and confirms reads still
answer 200 there — the two tokens are independent, including in absence.
The EXIT trap dereferenced an unset SERVER_PID under set -u, which made it
error out instead of cleaning up when anything failed before the boot. Both
PIDs are initialised and the kills guarded.
CLAUDE.md: the vitest count was stale (244 -> 251, auth.test.js 19 -> 26) and
test-curl.sh was undocumented.
Refs #16
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sergent renameSync() rotates reports/ -> reports/archive/ at 07:30 UTC daily,
so for ~22h per day the only copy of a fresh scan lives in archive/. The
handler now scans both directories and concatenates with top-level priority
on filename collision. archive/ missing is a silent skip.
Tests : 17/17 in test-curl.sh (11 existing + 6 new for archive coverage).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the SSH/rsync canal between Max's workstation cron and the VPS
for fetching defenseur scan reports. The defenseur-auto orchestrator now
pulls reports/defenseur-X_<date>*.json over HTTPS, reusing HEALTH_TOKEN.
The handler mirrors the style of index.js (HTTP native, no framework),
includes the same isScanReport guard as defenseurs/src/report.ts (filters
out defenseur-auto_*.json run reports), and validates the date param
against /^\d{4}-\d{2}-\d{2}$/ to short-circuit path traversal before any
filesystem access.
Validated by test-curl.sh — 11 cases covering auth, validation, date
filter, isScanReport filter, sort order, GET-only and 404 paths.
Spike: ~/claude-code/.spikes/archived/endpoint-reports-sur-vps-health-api-pour/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>