b6c0f5e1a2 split the transport helpers out of env.js into onion.js and left this test requiring the old module. apiFetchOnion and apiGetOnion appear nowhere in env.js, so all four cases died on 'apiFetchOnion is not a function' rather than exercising the onion retry they describe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A consumer proved its upload by logging the administrator into the Filer UI in
a browser and walking its JSON listing. The filer moved into the optional
console role with the split, so that path depended on a role a node need not
deploy, and since the engine declares only the api canonical, the lookup for
the filer URL raised outright for every consumer with seaweedfs enabled.
The shared helper now lists the bucket through ListObjectsV2 on the engine,
signed with the consumer's own identity rather than the administrator's. That
drops the console dependency and the Keycloak round trip per check, and proves
the per-consumer identity in s3.json reaches its own bucket.
SigV4 for the browser side is hand-rolled on node:crypto; its vectors are the
ones the engine probe's Python signer is pinned to, and a test fails if the two
drift apart. The signer deliberately does not set Host: Playwright derives it
from the URL and that is the value signed.
isAuthChain moves to the keycloak helper, where it belongs; web-app-funkwhale
pulls it through the personas barrel.
Measured against SeaweedFS 4.47: ETag and LastModified are returned, nested
prefixes list flat and complete, an in-place overwrite changes the token while
an untouched object keeps it, and s3.js completes the round trip end to end.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0a52433dcb gave the standalone request fixture a Tor-resilient GET,
because the bundled socks client caps the CONNECT at 30s and a cold
onion circuit fails a request regardless of its timeout. POSTs and calls
made through context.request still went out once, and a circuit Tor
rejected ("Socks5 proxy rejected connection") was not recognised as
transient at all.
apiFetchOnion now takes the verb from opts.method and calls
request.fetch, so it serves both APIRequestContexts; apiGetOnion becomes
its GET shorthand. The rejection joins the transient patterns. Retries
stay onion-only and transport-only: clearnet keeps a single attempt and
HTTP or other errors re-throw at once.
The Mattermost LiteLLM spec routes its login, users/me, users/username
and channels/direct calls through it. The /posts call that sends the
prompt stays a single attempt, because a replayed post would message the
bot twice. The Hermes administrator spec reads /v1/models through
apiGetOnion.
env.test.js pins a POST surviving a rejected circuit, the single
clearnet attempt, no retry of a non-transport error over Tor, and the
GET shorthand.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
performKeycloakLogin polled the page URL with toContain(canonicalDomain).
On the Keycloak auth page the app domain already sits in redirect_uri,
so the poll was satisfied before the login had completed. The denial
path and the biber/admin iframe checks used the same substring test.
All of them now compare hostnameOf(url) with the canonical domain, which
is a bare hostname (env.js reads it with /\.onion$/).
LOGIN_CONTROL_NAME never matched a login link that carries an
icon-font glyph: Playwright's accessible name includes the private-use
character, and the pattern only allowed whitespace before the text. The
post-login waitFor({state: "hidden"}) therefore passed at once, even
before oidc.js had swapped the link. The pattern now admits \p{Co}
before the text, so that wait really waits for the swap.
dom.test.js pins both: glyph-prefixed login controls match while Logout
and Accessories do not, an IdP URL carrying the app domain in
redirect_uri is rejected, a look-alike suffix is rejected, and
about:blank yields an empty host.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
confirmKeycloakLogoutIfPrompted waited the full 2s times the onion
multiplier for Keycloak's confirmation button even when Keycloak had
already redirected away because the end-session request carried an
id_token_hint. It now races the button against the page leaving the
logout endpoint (waitUntil: commit) and still clicks the button when
Keycloak renders the prompt. The dashboard's own confirmLogoutIfNeeded
stops the same way, and its poll interval no longer scales, so a slow
target is noticed sooner rather than later.
inAppLogout's first candidate poll ignored a visible settings link, so a
surface without a recognisable logout control or menu trigger, such as
the Mastodon home timeline, burned 30s times the multiplier before the
existing settings fallback ran. A visible settings link now ends that
poll as well; logout controls and menu triggers are still checked first
in every round. The settings selector moves to settingsLinksOn so both
callers share it.
Traces of run 34421944328: the dashboard's confirm probe expired after
2.03s on clearnet and 10.07s over Tor, and Mastodon's biber logout
polled 165s over Tor before reaching the settings page. Replayed in
Chromium, a Mastodon-shaped surface logs out in 3.24s instead of 33.45s
and a redirecting Keycloak logout returns in 0.20s instead of 2.00s,
while a prompting one is still confirmed. In the traced persona logouts
of six artifacts every other poll found a candidate in its first round,
so their path does not change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three waits in the logout flow were blind sleeps scaled by the onion multiplier.
A timeout is an upper bound and shrinks to nothing when the answer arrives early,
but page.waitForTimeout always burns its full duration, so multiplying it by five
spends the time whether the target is slow or not. The settle before the logout
sweep cost three seconds on clearnet and fifteen on an onion, every persona, every
role; the dropdown settle cost that again per menu trigger tried; and the poll
interval of waitForAnyLogoutCandidate was multiplied too, which made detection
five times coarser exactly where the target is slowest.
So the sleeps become waitForLogoutControl, a bounded poll that keeps the scaled
deadline and drops the scaling from the interval. It returns the moment a
logout-shaped element is visible, which is the condition the click that follows
needs anyway. Worst case is unchanged: with nothing to find it runs the deadline
out, exactly what the sleep did unconditionally. The first of the two
waitForAnyLogoutCandidate calls around the settle went with it; its result was
discarded, and the second still carries the thirty second deadline that covers a
navbar arriving late.
Four unit tests pin the contract, since the argument for the change is entirely
about when it stops waiting: it returns without sleeping when the control is
already there, returns as soon as one appears, costs at most the deadline when
none ever does, and polls on a fixed 250ms interval no multiplier touches.
Measured context, from run 34348821577: the same dashboard logout test takes
161.0 to 164.0s over Tor and 34.1 to 34.8s on clearnet across five jobs spanning
two distros, both deploy modes and three filesystems. A 1.9% spread across five
runner VMs is not network latency, it is a constant being multiplied, and the
ratio 4.73 is the multiplier itself. Solving the pair puts 32.0s of that test
inside scaled waiting against 2.3s of work. This commit removes the part of that
which is a sleep; the larger part is timeout caps on waits that expire and are
swallowed by a catch, which is a separate defect and stays for now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
33a778891 added the first JavaScript unit test that reaches into a
Playwright helper. csp.js imports `expect` from @playwright/test at module
scope, so requiring it drags an npm package into a suite that is
contractually free of them: Makefile:837 calls the target "node:test, no
npm dependency", scripts/tests/unit/javascript.sh installs only node, and
node_modules/ is gitignored. On a fresh CI checkout the import fails and
the whole JavaScript suite dies, not just the new file. It passes here
only because this machine has node_modules, so neither the local run nor
`make quality-high` can see the break.
The suite now intercepts that single request through Module._load and
hands back a six-line `expect` implementing the one matcher these paths
use, restoring the hook immediately afterwards. Probed: the request never
reaches resolution, `@playwright/test` never enters require.cache.
Also corrects a test name. "an enabled injector without a base URL is not
asserted" claimed to pin `if (!host) continue`, and does not: with no base
URL the host is "" and `includes("")` is true, so the suite stays green
with the guard deleted - verified by removing it. 33a778891's message
overstates this in two places, saying the suite pins every guard in
assertCspInjections and that dropping either toLowerCase trips exactly one
test. Dropping `host.toLowerCase()` trips nothing either, because
`new URL(x).hostname` is already lowercased.
Keeping the unit test rather than dropping it is a deliberate call: the
mirrored-unit-test lint exempts `test-*` roles as circular, so this file is
voluntary. It has already earned its place by catching two live mutants,
the enforced-over-report-only precedence and the csp-side toLowerCase.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
readCspString re-fetched the current URL through the Playwright API
request context and, when that failed, waited actionTimeout (30s) for a
<meta http-equiv="Content-Security-Policy"> that no template in this
repo emits. On a healthy runner that path works and costs one extra
request; when name resolution degrades it collapses, because the API
request context resolves DNS separately from the browser: the re-fetch
burns its 10s cap and the meta wait then drains whatever is left of the
caller's test budget.
That is what killed web-svc-logout's guest persona in run 32597248629,
job Pretix#0, whose swarm manager had a single resolver on 127.0.0.1
with dnsmasq no longer answering. Every guest test on that host inflated
to ~40s and the logout one, loading two external assets on top, died on
its 60s timeout having asserted nothing. The same role in Bluesky#0 and
YOURLS#0 of the same run, same role set, finished in 0.85s and 0.82s -
so this is a resilience fix, not a repair of a broken assertion.
gotoOnion now installs a page.on("response") recorder, so the header is
taken from the document the browser already received. That also covers
the surface reached through an SSO round trip, where no navigation
Response is in scope at the assertion - only guest.js holds one, biber,
admin and the suitecrm saml spec discard theirs. The re-fetch stays as
the fallback for a spec that navigates without gotoOnion; the meta
fallback is deleted.
The suite pins the header precedence (enforced over report-only) and
every guard in assertCspInjections: a disabled injector, a gate that
throws, an enabled injector without a base URL, a missing gate, a second
enabled injector, and the case-insensitive host match. Inverting the
precedence or dropping toLowerCase each trips exactly one test.
Verified against a headless Chromium and a local server counting
requests: the header survives a 302, no extra request is issued, a
missing injector host still fails the assertion, and a header-less page
falls back to exactly one re-fetch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>