fix(hub): an outage read as "not measured", and the map stopped moving - #22
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The green lights were all on. Two of the reasons they could never turn red.
An outage could not be drawn
getJSONthrows on any non-2xx before parsing, andsignalandsignalmapanswer 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 tolisted— so an outage and a service we never looked at drew the same mark.signalhas no aggregate backstop either (city covers npc, alpha, algora, ao, bridge, signalmap), so itsdownstate was structurally undrawable.This is rule 4 of
HEALTH_CONTRACT.md, which we were on the wrong side of:fetchServiceHealthnow reads through a newgetJSONWithStatusand 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 supplydown; a 4xx says our address is wrong, not that the service is unwell.The 503s stay where they are.
mossland-signal/scripts/deploy.shgates on that status code, and the contract names it a documented exception.The map stopped moving
setActivity()was called once, atcreate(), 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
fetchServiceHealthto the pre-fix reader fails 5 of the 15 tests and nothing else.What is verified, and what is not
fetchServiceHealthacross all eight cases, against a real HTTP serverrequestAnimationFramenever fires in a hidden pane, soupdate()does not run there at all. Worth a look on the deployed page.🤖 Generated with Claude Code