fix(engine): retry on 429 instead of a false SKIP, and correct the call count

Measured 2026-08-04: 13 script invocations in one shell loop tripped an
anonymous Forgejo rate limit at 26 requests, because each invocation makes
TWO calls (org listing + catalog marketplace.json), not the ONE this repo's
own CLAUDE.md claimed. That line went stale when INSTALL-TRUTH added the
second call and nobody updated the count it depended on.

fetchWithRetry wraps both calls, honoring Retry-After on HTTP 429 instead of
silently falling back to SKIP. TDD: 5 new tests inject a fake fetch and sleep
to drive the retry/backoff/give-up paths without touching the network.

A sweep across every repo still doesn't belong in this engine — that's
org-ops's job by this file's own header — but a single repo's self-check
should not read as broken just because the forge was briefly busy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01496ZWasKPnA627crFBXWhe
This commit is contained in:
Kjell Tore Guttormsen 2026-08-04 12:12:49 +02:00
commit d1b6274924
8 changed files with 119 additions and 13 deletions

View file

@ -51,10 +51,16 @@ would recreate, in data, exactly the drift this plugin exists to remove.
- **Three outcomes on references.** "No match" and "match on a known non-repo"
must stay distinct findings. Collapsing them hides real loss inside correct
text — the exact defect class this gate exists to catch.
- **One API call, anonymous.** The org listing carries description and topics
already; per-repo fetching trips the rate limiter (HTTP 429). It reads without
a token, so the gate works for any reader — a public plugin whose documented
check only runs for its author is a broken plugin.
- **Two API calls per invocation, anonymous, with 429 retry.** The org listing
(description + topics) is one; the catalog's `marketplace.json` for
INSTALL-TRUTH is the other (added after this used to say "one call" — that
line went stale and stayed stale until a 13-repo shell loop trusted it and
tripped the rate limiter at 26 requests). Both go through `fetchWithRetry`,
which honors `Retry-After` on HTTP 429 rather than silently reporting SKIP.
Both are anonymous — no token — so the gate works for any reader, not only
someone holding one. A sweep across every repo still does not belong here:
it needs the listing fetched once, not once per invocation, which is a
different shape of caller (org-ops), not a flag on this engine.
- **Codepoints, not bytes, not UTF-16 units.** Use `[...s].length`. An em-dash
exposes only the byte layer; astral characters expose the rest.
- **The reader decides a link's level, not just what is required.** Root