Commit Graph
3 Commits
Author SHA1 Message Date
9534e1c25e fix(reach): retry a busy (429) reach report in the reach page (#2250)
Follow-up to #2127: @dborup's point 2 there, and @efiten's non-blocking
note on the approval ("worth checking the node page's reach panel shows
a retry hint on 429 rather than a generic error").

## What changes

- **The reach page retries a 429 itself.** It shows "Reach is busy
computing other reports, retrying in N s", waits `Retry-After` seconds,
and tries again.
- When the header is missing or is not a number of seconds (an
HTTP-date, 0, a negative number), it waits 2 s. It never waits more than
30 s.
- After 5 retries it shows the old error ("Failed to load reach: API 429
…").
- Leaving the page stops the retries, both while it waits and while a
request is still in flight. Picking another window while a request is in
flight also drops that request's retries.
- **`api()` does not retry 429.** Login, signup, activation, email
change, companion linking, settings PUT and channel proposals also
answer 429 from their own limits, and those must not auto-retry. `api()`
now puts `status` and `retryAfter` on the errors it throws, so a caller
can decide; the message text is unchanged. A unit test pins that `api()`
makes exactly one request on a 429.
- **Fix in `api()`:** the `_inflight` cleanup chain
(`promise.finally(...)`) rejected unhandled on every failed call, so
each API error also logged "Uncaught (in promise)". That would have
added up to 5 more per busy reach load. The chain now catches; callers
still get the rejection through the returned promise.

This works with the server as it is on master (instant 429,
`Retry-After: 5`), and with the bounded wait in #2249 (429 after 3 s,
`Retry-After: 2`). Neither PR depends on the other.

## What I checked

- **Suites.** `sh test-all.sh` passes, the XSS sink gate passes, and
eslint has 0 errors and no new warnings.
- **Unit tests** (`test-frontend-helpers.js`):
- `reachRetryDelayMs`: Retry-After values; fallback on missing, zero,
negative or HTTP-date values; the 30 s cap; giving up after 5 retries;
no retry on 400, 404, 500, 503 or network errors.
- `api()`: a 429 error carries `status` and `retryAfter`, makes exactly
one request, and leaves no unhandled rejection.
- The block sits above the file's `Promise.allSettled(pendingTests)`
summary. Async tests registered below that line are never awaited, so
they cannot fail the run; nothing below it is async today.
- **E2E** (`test-node-reach-e2e.js`, already in CI suite 2): reach
requests are answered through `page.route`, so the server does not have
to be made busy. It checks:
- two 429s, then the real report: the hint shows "retrying in 1 s", the
retries wait (at least 1.8 s between the first and third request), and
no error is shown;
  - always 429: 6 requests (1 + 5 retries), then the error;
  - leaving while it waits: no more requests;
- leaving while the first request is still in flight: no hint written,
no retry;
  - a 500 with Retry-After: one request, no retry.

On master's frontend the new section fails at the first busy hint.
Locally against the freshened `e2e-fixture.db`, the whole file takes
about 17 s (2 runs).
- **Mutations.** I wrote 15 from the code, and the tests caught all 15.
They include: `api()` drops `status` or `retryAfter`, `api()` retries
429 itself, retry any error, retry limit off by one, fallback or cap
changed, Retry-After 0 accepted, either "left the page" check removed,
the wait not honoured, the hint showing ms, the retry count ignored, no
error after giving up, and the cleanup chain unhandled again. Each
mutation was checked against the file the server actually served before
the tests ran.
- **Real browser** (Chromium via Playwright, on a copy of a
10.5M-observation database):
- Current master's server, 6 tabs opened at once on uncached 14-day
reports: 4 got 429 with `Retry-After: 5` and showed the hint, and all 6
reports rendered within 11.5 s (one tab retried twice). Without this
change those 4 tabs show "Failed to load reach: API 429".
- With #2249's server: 4 of 6 got 429 with `Retry-After: 2`, and all
rendered within 8.1 s.
- The only console errors were Chrome's own "Failed to load resource"
lines for the 429s; there was no "Uncaught (in promise)".

## Not checked

- **Mobile layout.** The hint reuses the existing `.nq-load` style, so I
did not take a screenshot at phone width.
- **Other browsers.** Firefox and Safari were not tried; the code is
plain `setTimeout` / `await`.
- **Window switches.** While the hint is showing, the time-range buttons
are not on screen; the same is true of the existing error and loading
states. Picking a new window means waiting, or going back and opening
reach again.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: nullrouten0 <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:58:24 +02:00
efitenandClaude Opus 5 25f8426d32 test: make every suite in tests/e2e run, and tell the truth when it does (#2053)
Closes #2037.

Step 1 (#2045) wired in the nine suites that already passed. This is
steps 2 and 3: the four that ran and failed, and the five that could not
run at all. After this, the count of suites in `tests/e2e` invoked by
nothing goes from 18 to 0.

## Step 2 — the four that ran and failed

Triaged against a fixture server in CI with full output kept, not the
four-line tail the first probe saved.

| suite | verdict |
|---|---|
| `test-packets-scope-column.js` | passes on master today. It failed on
2026-09-18, so something between the two fixed it. Wired in unchanged
rather than investigated. |
| `test-node-reach-e2e.js` | test wrong. It waited for the reach map
whenever any *link* had GPS; `public/node-reach.js` builds the map only
when the *node* has coordinates. Guaranteed 10s timeout on a node with
positioned neighbours and no position of its own. |
| `test-channel-modal-e2e.js` | both failures test-side. The Add
button's visible label was shortened to "+ Add" with the accessible name
moved to `aria-label` (`public/channels.js:746`); and
`.ch-section-mychannels` is conditional on the visitor having added a
channel, which has not happened at that point in the suite. |
| `test-touch-targets.js` | three test-side, two a product finding. |

The three test-side touch-target failures were the harness measuring
controls that are not shown: `.compare-btn` (the CTA was removed in
#1646, and `style.css` says so), `.ch-back-btn` (`display:none` outside
the mobile channels layout), and `.filter-toggle-btn` (`display:none` on
mobile since #1461; the control shown is the navbar mirror, which
`mobile-page-actions.js:70` builds as a `.nav-btn`, so it was already
measured). All three are dropped from the table with the reason recorded
in the file.

The remaining two are **not** a test problem: `.nav-btn` and
`.ch-icon-btn` are each declared twice in `public/style.css`, 48px in
the touch-target block and 44px in their own component rule, and the
later one wins. Rather than lower the blanket or hide the failures, the
suite now has `DEFAULT_MIN = 48` plus a `MIN_OVERRIDES` table holding
those two at their effective 44, so a third selector dropping to 44
still fails the build. The contradiction is **#2052**, with both ways
out costed; the override entries should go when it is settled.

## Step 3 — the five that could not run

None is deleted. I checked each selector and seam against the product
before deciding, and every one still targets a surface that exists and
that nothing else covers.

Four were written against `@playwright/test`, a runner the project
neither installs nor uses anywhere else. Adopting a second runner for
twelve tests costs more than porting them, and the precedent is already
set: `test-path-inspector-coverage-e2e.js` exists, as its own header
says, because `test-path-inspector-e2e.js` could not run. So they are
ported to the plain-node Chromium pattern the other 109 suites use.

- **`test-issue-1522-trace-url-sync-e2e.js`** — the trace hash in the
URL, both directions. `test-e2e-playwright.js` covers that the page
loads and searches; it never looks at the URL, which is the whole of
#1522.
- **`test-marker-outline-weight.js`** — the canvas pulse ring never
thins below 2px. There is no CSS rule to read and axe cannot see inside
a canvas, so sampling the seam is the only way. Added a guard that the
ring was actually visible, so the weight check cannot pass vacuously on
a pulse that never rendered.
- **`test-pr-1490-live-map-gpu-animations-e2e.js`** — the queue drains,
the engine sleeps again, the fading trails stay under the cap of 5, and
the canvas sits on `animationsPane` rather than under the markers.
- **`test-path-inspector-e2e.js`** — reduced to what nothing else
covers: the map side pane, the `/#/traces/<hash>` redirect, the tools
landing. Its standalone-page test duplicated the wired coverage suite
and is dropped. Its "switching candidate clears prior polyline" case
ended after the click with a comment and no assertion, which is the same
green-but-empty problem this issue is about; it now compares path
counts, and skips loudly when the fixture yields too few candidates.

The fifth, **`test-table-sort.js`**, needed `jsdom`, which was declared
nowhere. It is a unit test of `public/table-sort.js` filed under
`tests/e2e`, so: `jsdom` is a devDependency (lockfile updated, `npm ci`
stays consistent), the file moved to `tests/unit/`, and the
`domIntegration` group in `scripts/non-unit-tests.json` is gone with its
only member. It runs 22 tests. 20 passed immediately; 2 had rotted,
because #1648 M2 replaced the up/down glyphs with Phosphor sprites and
the direction moved out of `textContent` into the `<use href>`. Those
two now read the sprite ref and the `aria-sort` value, so they also
guard the accessible announcement.

## Verification

`tests/unit/test-table-sort.js` 22/22 and `test-test-inventory.js` pass
locally; the E2E suites need a fixture server, which I cannot build here
(no cgo toolchain since #1992), so CI is their first run as committed.
The triage above was measured in CI, not assumed.

## Not done

The per-assertion skips named in the second comment on #2037 are
untouched: the two flaky packet-detail cases, the fixture-data ones, and
the two `clientRxCoverage` suites that skip wholesale while reporting
success. Those need a fixture deployment with coverage enabled, which is
its own change. I have not opened it.

Option 2 from the issue, making `test-test-inventory.js` require a
`deploy.yml` line for every `tests/e2e` file, is also not here. It is
the right guard and it is now enforceable, since the list is finally at
zero, but it belongs in its own change where a red build means what it
says.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:45:11 +02:00
Alex B 893773338e chore(tests): move root test-*.js into tests/unit and tests/e2e (#2036)
Moves 290 root test-*.js into tests/unit (177, listed in test-all.sh) and tests/e2e (113, classified in scripts/non-unit-tests.json), per #1981 and PR-D of #1385. Root goes from 348 entries to 48. test-all.sh and test-fixtures/ stay put. The inventory guard now fails if a test reappears in the root or sits in the wrong folder.

Verified independently of the diff: the invoked sets are unchanged (test-all.sh 177 before and after, deploy.yml 96 before and after, both identical as sets), and a full local run of test-all.sh on master and on the branch produced 4702 output lines each whose only differences are absolute paths, stack-trace line numbers shifted by the REPO_ROOT line, the inventory wording and two perf ratios. The guard was mutation-checked: a test back in the root, a unit suite in tests/e2e, and a suite dropped from test-all.sh each make it exit 1. CI run 35246304316 ran 97 suites from tests/e2e and is green.

Follow-up 9335c51d finished the instruction files: no bare root test command is left in AGENTS.md, the squad charters, .github or docs, and every tests/ path they name resolves.

Merged by the interim maintainer without a second human reviewer: CI and the local runs above are the independent checks.

Known and deliberately out of scope: 18 of the 113 files in tests/e2e are invoked by no runner at all, and one of them cannot run anywhere because it requires jsdom, which is not a declared dependency. Tracked separately.
2026-09-17 19:06:07 +02:00