Skip to content

fix(hub): an outage read as "not measured", and the map stopped moving - #22

Merged
mikim merged 2 commits into
mainfrom
claude/api-health-check-status-05910d
Sep 10, 2026
Merged

mikim merged 2 commits into
mainfrom
claude/api-health-check-status-05910d

Conversation

@mikim

@mikim mikim commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

The green lights were all on. Two of the reasons they could never turn red.

An outage could not be drawn

getJSON throws on any non-2xx before parsing, and signal and signalmap answer 503 on purpose when their feed has nothing good left to serve. Their contract-shaped body was discarded whole, the entry dropped, and the node fell back to listed — so an outage and a service we never looked at drew the same mark. signal has no aggregate backstop either (city covers npc, alpha, algora, ao, bridge, signalmap), so its down state was structurally undrawable.

This is rule 4 of HEALTH_CONTRACT.md, which we were on the wrong side of:

The body is authoritative; the HTTP status is not. Keeping the two separate is what lets a consumer tell unreachable from reachable and unwell.

fetchServiceHealth now reads through a new getJSONWithStatus and believes a body that names its own state whatever the status line says. A thrown fetch still means unmeasured — DNS failure, a CORS wall, an abort say we could not see the service, which is a different claim. Only a 5xx we could not parse makes us supply down; a 4xx says our address is wrong, not that the service is unwell.

The 503s stay where they are. mossland-signal/scripts/deploy.sh gates on that status code, and the contract names it a documented exception.

The map stopped moving

setActivity() was called once, at create(), where all it could do was record a baseline it had nothing to compare against. It was absent from the 500ms block, so the ring sweep animated once on load and not a single signal mote was ever emitted — while the About panel claims each mote is one ingested signal and the sweep is a real health refresh.

Tests and CI, because there were none

This repo shipped with no tests and no CI, and the service layer is where that costs something: every conclusion becomes a coloured dot, and a confidently wrong dot looks exactly like a right one. Adds vitest and a typecheck/test/build gate.

Verified by mutation — reverting fetchServiceHealth to the pre-fix reader fails 5 of the 15 tests and nothing else.

What is verified, and what is not

  • ✅ fetchServiceHealth across all eight cases, against a real HTTP server
  • ✅ typecheck, 15 tests, build
  • ✅ the ecosystem panel renders identically to production (no regression while everything is healthy — which is correct)
  • ⚠️ the mote/sweep behaviour is code-verified only. It needs a visible browser: requestAnimationFrame never fires in a hidden pane, so update() does not run there at all. Worth a look on the deployed page.

🤖 Generated with Claude Code

mikim and others added 2 commits September 10, 2026 13:17
Two defects, both of which made the map claim more than it knew.

1. A declared outage could not be drawn at all. getJSON throws on any non-2xx
   before parsing, and `signal` and `signalmap` answer 503 on purpose when
   their feed has nothing good left to serve. Their perfectly contract-shaped
   body was discarded whole, the entry dropped, and the node fell back to
   "not health-checked" — so an outage and a service we never looked at drew
   the same mark. `signal` has no aggregate backstop either (city covers npc,
   alpha, algora, ao, bridge, signalmap), so its down state was undrawable.

   This is rule 4 of HEALTH_CONTRACT.md, which we were on the wrong side of:
   the body is authoritative and the HTTP status is not, and keeping them
   separate is what lets a reader tell *unreachable* from *reachable and
   unwell*. fetchServiceHealth now reads through getJSONWithStatus and
   believes a body that names its own state whatever the status line says.

   A thrown fetch still means unmeasured. DNS failure, a CORS wall, an abort:
   those say we could not see the service, which is a different claim from
   the service telling us it is unwell. Only a 5xx we could not parse makes
   us supply "down" — a 4xx says our address is wrong, not that the service
   is unwell, and calling that down would be a false alarm.

2. setActivity() was called once, at create(), where all it could do was
   record a baseline it had nothing to compare against. It was absent from
   the 500ms block, so the ring sweep animated once on load and not a single
   signal mote was ever emitted — while the About panel claims each mote is
   one ingested signal and the sweep is an actual health refresh. Both are
   diffing calls, so re-running them costs a comparison.

Verified against a local server across all eight cases: a 503 carrying
{"status":"degraded"} now reads degraded; an unparseable 502 reads down; 404
and 429 stay unmeasured; city's status-less aggregate still returns null; an
off-contract "running" still passes through untranslated; and an unreachable
host still throws rather than reporting down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pixel-agent-lab shipped with no tests and no CI at all, and the service layer
is exactly where that costs something: every conclusion it draws becomes a
coloured dot, and a dot that is confidently the wrong colour is indistinguish-
able from one that is right. The 503-read-as-unmeasured bug fixed in a219d5b
survived that way.

Adds vitest, a `typecheck`/`test` script pair, and a three-step CI gate
(typecheck, test, build) on every branch push and PR into main.

The suite covers what a reader concludes from an endpoint's answer:
fetchServiceHealth across 200, a declared 503, an unparseable 5xx, 4xx, a
status-less aggregate, an off-contract value and an unreachable host; and
kindOf's service/artifact/link split, whose rules are ordered rather than
obvious — `artifact` must win over tier `developer` or llms.txt becomes a
link. Driven against a real HTTP server rather than a mocked fetch, since
half of what is asserted is how the reader treats a status line it did not
expect, and a mock is free to be wrong in the same direction as the code.

Verified by mutation: reverting fetchServiceHealth to the pre-a219d5b reader
fails 5 of the 15, and nothing else.

`tsconfig.json` now includes `tests` and `vitest.config.ts`. A test file that
no longer compiles is a test that stopped guarding anything, and `npm test`
alone would report that as a passing suite of whatever was left.

CI tracks `lts/*` rather than pinning a patch: a gate that asserts a version
the runner no longer ships fails at Install and skips everything after it,
which looks identical to a green run nobody read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mikim
mikim merged commit 5c4bcb0 into main Sep 10, 2026
2 checks passed
@mikim
mikim deleted the claude/api-health-check-status-05910d branch September 10, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant