7538b8a0b turned this probe from isVisible() into waitFor() and claimed the
EspoCRM 1 and 2 failures with it. It was already in ce2db79ee, the commit run
31864526217 tested, and EspoCRM 1 failed anyway.
The wait now happens; it is just not onion-scaled. The trace of that run records
the probe as {'selector': "input[type='password']:visible >> nth=0", 'state':
'visible', 'timeout': '10000'} and it burns the full 10003.9 ms before erroring,
while the neighbouring wait at admin.js:145 carries 'timeout': '75000' from
resolveTimeout(15_000). This one call is the only timeout in the personas tree
that does not go through resolveTimeout, and utils/env.js:53-59 documents why
that matters: onion gotos deliberately return at DCL because "the caller's
explicit selector waits (onion-scaled) cover app boot".
They did not cover it here. All three attempts quit at DCL+10.1s with zero
/api/v1/* requests in the network log, i.e. App.start() never ran; four of the
six preload fonts were still in flight, the slowest completing 18.3s after DCL.
The base moves 10_000 -> 15_000 so the onion budget matches admin.js:145 rather
than inventing a third constant. Clearnet is unaffected: resolveTimeout
multiplies by 1 off-onion.
Not measured, and the reason to expect another round: DCL -> password field was
never observed on an onion circuit in that run, because every attempt quit
before it. 75s is sized from the parts, not from an observation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
uri reports a connection error as status -1. The readiness gate only had an upper bound, so -1 satisfied it and the retry loop settled on the first attempt against a service that was not up yet - the suite then ran against a dead endpoint instead of waiting for it.
Same fix as 7511852bd1 on feature/svc-net-tor, found independently; that branch is not merged here and carries it alongside unrelated onion changes and a diverged variable naming. This adds the regression test it lacks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Playwright declares the timeout option on locator.isVisible() and isHidden() as deprecated and ignored - both return immediately. 7538b8a0b already fixed two call sites in this file and deliberately left four, arguing that is the password field STILL there and is a Login control STILL showing are correctly answered at once. That holds for the native login path it measured. It does not hold for the OIDC popup path.
keycloak.js polls the POPUP url and is satisfied once that lands on the callback, while the opener has not re-rendered yet - and admin.js inspects the opener. The trace of EspoCRM variant 0 shows the check starting 5.5 ms after the credential submit and taking 10.7 ms, the error-context snapshot shows the Sign in button visible but disabled, mid-submit, and the network log has oauth-callback.php and api/v1/App/user both returning 200 about 150 ms after the assertion had already failed. Biber passed the same journey in one attempt because biber.js:147 waits; administrator burned three attempts because it did not. That commit assumed EspoCRM 0 failed for an unrelated back-channel reason; it fails at admin.js:191, inside this branch.
The wait uses base 12000, not the 60000 biber carried. resolveTimeout multiplies by 5 on an onion target, so this is 60 s there. Across 13 onion traces the largest wait of any kind that ever SUCCEEDED was 790 ms and the worst navigation 1893 ms; 60 s is 76 times the former. At 60000 the same line would burn 300 s per attempt, and it burns only on the path that already fails, which is the path that has to stay diagnosable. Biber moves to the same base so the two personas cannot drift again.
The three remaining ignored arguments are deleted rather than kept. 7538b8a0b called them inert but harmless; inert is right, harmless is the part that cost it two call sites - the argument reads as a wait that never existed. A repository lint now rejects the construct for isVisible and isHidden only; isChecked, isDisabled, isEditable and isEnabled take a real timeout and are deliberately not matched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
236578d3b converted this exact fallback in admin.js and left its twin in
biber.js untouched. Both personas do the same thing: when the Keycloak round
trip did not complete cleanly, fall back to looking for an authentication
marker on the page, and budget fifteen seconds for it. isVisible does not
spend that budget -- Playwright ignores the timeout option there and answers
from the current DOM.
The cost is measured, not argued. In run 31658241728 the EspoCRM 2 trace shows
the same API asked for ten seconds and returning in 0.20 s, while the network
log has the SPA finishing its boot at 0.66 s and the fallback navigations
firing at 0.91 s. A surface that renders one second late reads as
unauthenticated.
waitFor spends the budget only when the marker is absent, which is precisely
the case that is answered wrongly today; when the marker is there it returns as
soon as it appears. So a passing run pays nothing and a failing run pays the
fifteen seconds it was always meant to pay. admin.js has paid them since
236578d3b.
Ten further isVisible({timeout}) call sites in this directory were reviewed and
deliberately left alone: they ask whether something is visible right now
(passwordStillVisible, loginStillVisible, the frame scans, clickFirstVisible),
and clickFirstVisible would lose five seconds per candidate if it waited.
Not verified by a run: the mechanism is proven from the EspoCRM trace and the
parity with admin.js, not from an observed biber probe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Playwright's isVisible() is an immediate, non-retrying check; the timeout option
it accepts is ignored. Two call sites in the shared admin persona passed a
substantial timeout and behaved as if it applied:
:75 gates the whole native-login path, called with probeTimeout=10_000 (:101)
:141 the fallback that decides whether an authenticated surface was reached,
called with resolveTimeout(15_000)
The trace of EspoCRM variant 1 shows the option never reaching the call. Every
navigation carries its own budget -- {"url": ..., "timeout": 60000} -- while the
probes record only {"selector": "input[type='password']:visible >> nth=0",
"strict": true}, with no timeout field at all. The first probe answers false
220 ms after it starts, against the 10 s it was given; the three fallback probes
that follow take 36, 32 and 29 ms. The whole native-login attempt, four
navigations and four probes, is over 1.15 s after it began.
That is not enough for an SPA. The DOM snapshot at the moment of the first probe
is ["BODY", {"data-id": "espocrm"}, ["DIV", {"class": "container content"}]] --
an empty shell, with espo.js and espo-main.js still in flight. The login form the
probe waits for renders later, so the persona falls through to /login, /admin/
and /admin, collects three Apache 404s, and asserts that the administrator never
reached an authenticated surface.
Both sites move to waitFor({state: "visible", timeout}), which is the waiting
form and is already the idiom elsewhere in this directory.
The four remaining isVisible({timeout}) calls in the file stay as they are: they
ask whether something is visible right now -- is the password field STILL there
after submitting, is a Login control STILL showing -- where an immediate answer
is the correct semantics and the timeout argument is inert but harmless.
Fixes the EspoCRM 1 and 2 failures and applies to every SPA the shared persona
drives; it does not touch EspoCRM 0, whose OIDC back-channel fails for an
unrelated reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit packed the personas into one archive but still transferred them per role. orchestrate.yml runs 01_setup.yml once and then loops 02_run_one.yml over every app plus a retry pass, which is where the 22 transfers came from. Moving the staging into the setup phase leaves exactly one.
Each container now mounts the shared tree read-only at /e2e/tests/personas, the same pattern node_modules already uses. Both import directions were checked: specs require ./personas from /e2e/tests and land in the mount, while personas/utils/env.js requires ../../timeouts and reaches two levels up into the writable per-app staging dir, where the shared helpers are still placed. The nested mount therefore does not cut the relative imports.
Measured saving is 4.1 of the 4.2 minutes the copy cost in odoo 0, since only one transfer remains.
Not verified: the nested bind mount was not executed. Docker orders mounts by destination depth so /e2e lands before /e2e/tests/personas, but if that ordering did not hold the spec would fail loudly on require, not silently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copying a directory with ansible.builtin.copy walks it file by file, one round trip each. Measured in the green onion job odoo 0 from the log timestamps: 22 invocations at 10 to 13 seconds, 4.2 minutes total, for 15 files totalling 108 kB - about 0.7 s per file, all of it connection overhead rather than payload.
The tree is now packed once on the controller and transferred as a single archive. Uncompressed on purpose: at this size gzip saves 28 kB and would add a second binary that has to exist on every distro image. tar itself was verified present in the CI fedora image and in the arch and centos node images.
Equivalence checked rather than assumed. The old copy used a trailing slash and therefore copied the contents; `tar -C personas .` does the same. Building the archive produces 17 entries - 15 files plus ./ and ./utils/ - so the subdirectory layout survives.
This continues 6e14d12b5, which collapsed the per-file tasks into one task. That left the per-file transfer inside that task, which is what this removes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
75 commits: the socialhome role reimplemented against the project it actually
deploys, native-login specs for funkwhale and shopware, a window.open IdP
handoff and a settings-link fallback in the shared persona helpers, a swarm
converge budget that gives up on a terminal task, and a long tail of role
fixes.
Eleven conflicts. The shared persona helpers (keycloak.js, logout.js) take the
incoming structure — they gained real behaviour this branch never had — with
this branch's resolveTimeout convention re-applied over it and
confirmKeycloakLogoutIfPrompted re-attached at every return point; without it
the Keycloak SSO session outlives the logout. playwright.config.js keeps this
branch's version: both sides set actionTimeout, but ours routes through an env
var and 15s is too short behind an onion. socialhome's spec takes the incoming
content — 2dbd6eb10 found the role described the wrong upstream project
entirely — and keeps gotoOnion and expectHstsWhenTls, since demanding HSTS over
a plaintext onion is a false failure. Both READMEs were regenerated rather than
merged by hand.
Three collisions arrived with no conflict marker at all, because each side
changed a different place in a different file:
- group_vars/all/18_swarm.yml declared swarm_stack_converge_retries twice.
Same value, so nothing breaks today; editing the documented one would have
silently changed nothing.
- svc-net-tor and web-app-socialhome both claimed 192.168.74.0/24. Socialhome,
the newer role, moves to 192.168.75.0/24.
- web-app-socialhome/meta/schema.yml arrived as a new file, so ef5ea7cb5's
rename never saw it. Its credential would not have resolved and the role
would have deployed with an unusable administrator account.
The incoming specs also needed this branch's conventions: raw timeouts wrapped,
four page.goto calls routed through gotoOnion, a request.get given an
onion-aware timeout, and 21 roles marked playwright-service-flag over the tor
block this branch added to them.
make quality green on the merge result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
clickOidcLoginLink watched only the opener. EspoCRM hands off to
Keycloak via window.open, so the opener correctly never navigates and
the helper abandoned a fully rendered Keycloak form in the child window;
the persona then died 45s later at the generic 'did NOT reach an
authenticated surface' gate. The trace shows BrowserContext.page with an
openerPageId, and the click itself succeeded - 'element is visible,
enabled and stable' with no error on the after-event. The [disabled]
Sign in button in the snapshot is EspoCRM's own signIn() disableForm(),
set after the click, not before it.
The helper now races the opener against the popup with Promise.any over
branches that only fulfil once a page reaches openid-connect/auth, and
returns that page; biber and admin pass it to performKeycloakLogin.
Promise.any rather than race: EspoCRM closes the popup after it
authenticates the opener by XHR, and a race would let that losing branch
resolve null and truncate the opener's budget. Measured on a stub
harness: popup closed at 500ms with the opener reaching the IdP at
1200ms returns the opener, where a race returns null. The 15s popup
window matches the idiom already used in the nextcloud addon specs; the
2s window this replaces would have returned null for any adapter that
runs PKCE or discovery before window.open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each of the seven addon specs opened a fresh context and ran the full SSO
round-trip. Over Tor that is roughly twelve minutes apiece, and odoo 0 of run
31340965955 died on 'Test timeout of 600000ms exceeded' inside
gotoOnion -> loginToOdoo -> contacts.spec.js:13.
authenticatedContext caches the storage state after the first login and restores
it for later callers. A restored state that no longer renders the web client is
discarded and the full login runs, so an expired session degrades to today's
behaviour rather than failing the spec.
Coverage is not reduced: the SSO flow keeps its own dedicated tests in
playwright.spec.js:98 and :160 (admin and biber, log in / verify ui / log out),
which this change does not touch. The seven addon logins were repetition of an
already-covered path, and no addon spec asserts anything about the login or
needs an anonymous session. What does change is isolation -- the specs now share
one restored session instead of each starting clean.
The TEST_E2E_PLAYWRIGHT_ONION_TEST_TIMEOUT bump belongs in this commit: four
specs lost their own test.setTimeout(resolveTimeout(180_000)), which resolved to
900s on an onion. Without the bump those four would regress from 900s to 600s.
Cause B is untouched: the global ONION_RUN_TIMEOUT of 5400s left eight tests
unstarted. The cache should relieve it, but that is arithmetic on measured
single-login times, not an observed run, and raising a timeout that spans every
onion role needs its own measurement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gates that assert no sign-in affordance survived the OIDC round-trip
matched /log\s*in|sign\s*in|sso/i unanchored, so the pattern hit ordinary
navigation entries of an already authenticated page as substrings:
Accessories, Login Enabled, Blog inbox, Asset Login History. Any of them made
the gate report 'OIDC login did NOT establish a session' while the login had in
fact succeeded. Snipe-IT 2 of run 31340965955 failed exactly that way.
LOGIN_CONTROL_NAME anchors both ends and still admits the 'with ...' suffix, so
'Sign in with Keycloak' keeps matching and the pattern is not narrowed into
matching nothing.
The pre-login fallbacks in admin.js:58 and biber.js:79 stay deliberately broad:
they hunt for a sign-in control on the UNAUTHENTICATED landing page after the
anchored attempt misses, where no authenticated menu exists to collide with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
clickOidcLoginLink assumed the element matching the persona's anchored login pattern is the provider trigger. On mobilizon it is not: the navbar carries a plain Login router link, which the anchored pattern matches, while the actual provider button sits on the page that link opens and is labelled with the instance name, SSO Login (INFINITO.EXAMPLE). The helper clicked the navbar link, landed on /login, waited fifteen seconds for openid-connect/auth, returned false, and the persona then failed its authenticated-surface assertion with the login page as its final URL.
Retrying with the loose pattern would not have helped. On the landing page the provider button does not exist at all, and on /login the navbar link stays visible and sits ahead of the button in DOM order, so both patterns resolve to it a second time. What is missing is a second resolution on the page the click opened.
Run the existing resolution again, but only when the click navigated somewhere new without reaching the auth endpoint. That second pass additionally accepts a provider control by accessible name, which is the only thing that separates the button from the navbar link. Pass one keeps its locator chain, its timeouts and its order, so a role whose strict candidate already is the provider trigger returns on the first pass and never pays for the retry; guest.js does not call this helper at all, and the remaining callers are biber.js, admin.js and the mastodon seaweedfs spec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ACCOUNT_MENU_NAME_RE matched profile unanchored, so Mastodon's public 'Profiles directory' link counted as an account menu. The sweep clicked it, navigated the authenticated SPA onto a Rails page with no settings link, and the settings fallback added in 141ff0645 then ran with nothing to find.
Across all 53 traces of run 31195451847 the only names that lose their match are 'profiles directory' and 'browse profiles' - the target - plus the class name profiler-toolbar, which is never an accessible name.
actionTimeout was unset, so Playwright's default of 0 let a click hang until the whole 300s test budget expired; Fediwall 2 reports an error with an empty body instead of a clean failure because of it. The slowest successful action in those traces takes 1.6s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mastodon keeps no logout on the surface the persona lands on, so the helper gave up after two steps. Its trace carries three /settings/preferences links and no sign_out anywhere, and the universal-logout service that would inject a fallback stays passive because Mastodon authenticates natively rather than through oauth2-proxy.
The step runs only after both existing ones fail, follows the first visible same-origin settings link, and repeats the two searches there. Relative hrefs only, so it cannot leave the app; no /account match, which would hit Mastodon's own /accounts/ profile URLs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
clickOidcLoginLink only ever matched accessible names, so a role rendering its own login form beside the SSO entry won the match and the persona submitted the native form instead of reaching Keycloak. GitLab's sign-in button was clicked while form[action=/users/auth/openid_connect] and data-testid=oidc-login-button sat unused next to it; Mastodon's entry is named 'Login or Register', which no login regex matches.
The provider locator runs first with a 5s window and falls through to the existing strict/loose path, so roles without OIDC markup are unaffected. It requires the full protocol name with a separator rather than a bare 'openid' substring: the only href of that shape anywhere in roles/ is Mastodon's own login link.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Sign-in button matched ACCOUNT_MENU_NAME_RE, so inAppLogout clicked it as a menu trigger, and a username field matched the auth marker's data-testid rule, so an unauthenticated login page counted as an authenticated surface. The second one is why a broken auth chain surfaced as 'no in-app logout control reachable'.
Traces from CI run 31195451847 show the sign-in button clicked at CALL@123 and CALL@137, the marker returning true at ret@85, and the match resolving to input#user_login at @145 with no POST to /users/sign_in anywhere in the network log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Magento's admin form names its inputs as a form array (login[username], login[password]), which none of the exact name matches covered, so the persona typed into nothing and the native login failed. A suffix match covers array-style names generally, as used by Zend derivatives, Rails and Symfony.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three conflicts, none resolved by picking a side wholesale:
- Makefile: took the incoming ACT_INPUTS, which routes through SWARM_DISTROS
instead of inlining the same expression a second time.
- test-e2e-playwright/02_run_one.yml: took the incoming rc handling. An absent
rc now counts as failure; the previous default(0) let an aborted Playwright
run pass silently, contradicting the debug message right above it.
- web-app-peertube spec: combined both. Kept gotoOnion and resolveTimeout,
which this branch's lint enforces, and adopted the incoming retry-inside-poll
for the OIDC button plus its networkidle wait.
An aborted Playwright run - watchdog SIGTERM, task timeout - returns a result dict with neither stdout nor rc. The reporting task dereferenced both, so it became the fatal error itself: the log ended on "object of type 'dict' has no attribute 'stdout'" while the real cause, a disk-exhaustion abort with rc 75, sat eight thousand lines earlier. Guard all three reads, fall back to msg for stderr because that is what Ansible fills on an abort, and treat an absent rc as a failure so the run is recorded and named by the aggregate check rather than vanishing behind a template error.
The distro sweep's summary also mislabelled the distros it never started: they kept the initial "not reached within the budget" note even when the sweep aborted on a failure with 12214 seconds of budget left, pointing at a budget problem that did not exist. Say what happened instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BigBlueButton is never served over Tor (WebRTC needs UDP/ICE), but in an
onion deployment its issuer only exists as an onion address. The test
browser reaches clearnet or the onion net, never both, since the private
tor network has no exit — so the login round-trip timed out. A real Tor
Browser handles this fine, which is why the gate belongs in the spec and
not in the role.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
seaweedfs disables its frontend on tor nodes by contract, which drops
the filer/master vhosts and leaves nginx.conf unrendered. The readiness
probe still hit the canonical filer domain and died on
TLSV1_UNRECOGNIZED_NAME, and the config mount kept nginx.conf in the
stack file, which docker stack deploy validates and rejects. Both now
carry the same frontend condition the role already renders into its
test env.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Playwright image was pulled once per role, and the shared helpers
and persona modules were staged file by file. The pull now happens once
for the whole run, and the personas tree is copied in a single task.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The login-still-visible probe sampled 2s right after the redirect,
before keycloak-js finished the code→token exchange over Tor; wait
Tor-scaled for the login control to disappear instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add tests/lint/repository/test_task_file_numbering.py: numbered task
files use exactly two leading digits, a numbered *_core.yml must be
00_core.yml, per directory the numbers are unique/consecutive and start
at 01 (00 with a core file), numbered includes appear in ascending
order, and shared helpers live in tasks/utils/ instead of the numbered
chain.
Apply it across every role: rename NN_core.yml to 00_core.yml with a
consecutive down-shift of the siblings, move letter-suffix children into
a subdirectory named after their parent task, relocate helpers included
from more than one place into tasks/utils/, and update every reference
(include_tasks, path_absolute, .ansible-lint-ignore, requirement docs).
Legacy narration comments in the touched files are deleted; the upstream
xwiki.properties reference block opts out via nocheck. The unused
MAILU_IP6_PUBLIC var (only ever named in now-removed comments) is dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cold onion probes need curl to outlive tor's internal 120s descriptor
wait; the recreate path must force-update the tor_tor service in swarm
instead of spawning a competing compose publisher; and a degraded Tor
circuit fails single suites with pure timeouts, so failed roles get one
retry on rotated circuits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrite all 254 role descriptions to a uniform noun-phrase schema
(function-first, no leading verb, no trailing period, <=120 chars) and
enforce it via the extended description lint. Rename 12 role README H1
titles for clarity (e.g. Backup Container to Local -> Backup Docker
Volumes, CSS Service -> Corporate Design, NGINX Legal -> Imprint) and
regenerate their Cosmos and Quick Setup sections. Overview table
regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roles declared under meta/main.yml dependencies now appear on both the
Dependencies and Dependents side, labelled with the glossary gear
symbol and connected by a fixed 1:1 edge; the legend explains it. All
role READMEs regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Edges now carry their relation: solid 1:1 for fixed consumption, dashed
0..1 for conditional (group-membership gated) consumption. Below every
diagram the generator renders a reading key for the edge kinds and the
node markers, with the glyphs drawn from the shared symbol glossary.
All role READMEs regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add cli/build/readme: derives each role README's managed sections from
templates/roles/README.md.j2.tmpl (the Cosmos mermaid diagram from
meta/services.yml, an invokable-gated Quick Setup, and Credits).
The Cosmos diagram shows dependencies, every declared service, and
dependents (roles that consume this one, matched by role name and by
provides provision-name in both directions). Role nodes carry deploy-mode
emoji markers (compose/swarm/host); disabled and shared services get a
stop mark; dependents are capped at 12.
test_readme enforces the Quick Setup body is byte-exact with the template
render. `make readme-generate` and `make cosmos` regenerate; `make cosmos`
also runs inside `make quality`.
Refactor: utils/symbol_glossary.py is the single source of truth for the
word/emoji mapping (unique project-wide), consumed by the complexity
matrix and the Cosmos builder. utils/roles/deploy.py holds role_has_stack
and role_deploy_modes, shared by both so the logic never drifts.
Add `make lint-mermaid` and its CI workflow, rendering every diagram via
mermaid-cli; the render step provisions chrome-headless-shell and unzip
on demand so it works in the minimal container.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Playwright onion gate probed the app onion over Tor and silently
SKIPPED the whole suite when it did not answer — a broken onion could
go green. Remove the gate: the probe now waits hard (40x15s, covering
normal 2-5 min descriptor publication) and an unreachable onion fails
the deploy loudly. Reachability is part of what is under test.
The readiness probe treated transport-level failures (status: -1) as
'< 500', satisfying its until condition on attempt 1 and never using
its retries (nextcloud [0,1] died to TLSV1_UNRECOGNIZED_NAME this way).
Require a real HTTP status >= 100 so transport errors retry.
Tor pins one long-lived entry guard; its own log showed ours degraded
('failing more circuits than usual ... 38 collapsed, 21 timed out'),
which throttles every flow of the node at once — the most plausible
cause of the 89-minute descriptor-publication outage and the
intermittent ERR_SOCKS_CONNECTION_FAILED in E2E. New config key
services.tor.use_entry_guards (default true keeps the Tor default and
production anonymity posture); the matrix variant pins it false so test
deployments render UseEntryGuards 0 and a single bad relay cannot stall
the node. Anonymity is not under test there, only reachability.
Verified: full svc-net-tor closure deploy over onion with the gateless
semantics, failed=0, every onion suite executed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The injected universal-logout control navigates to Keycloak's
end-session endpoint without id_token_hint, so modern Keycloak renders
a logout-confirmation page. Nothing clicked it: the SSO session and the
oauth2-proxy cookie survived, and the prometheus spec's strict
post-logout assertion (redirect back to Keycloak auth) timed out on
onion nodes across five CI jobs (svc-net-tor, discourse, svc-bkp,
fedora/manjaro env).
inAppLogout now confirms the Keycloak prompt (#kc-logout /
logout-confirm form) after clicking the logout control, completing the
real user journey for every persona flow. The prometheus spec drops its
bespoke fire-and-forget /logout goto in favour of the shared
inAppLogout plus a networkidle settle; the strict step-6 redirect
assertion is unchanged.
Verified live over onion: prometheus suite 5 passed / 2 gated skips.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve every 3+-file same-prefix group flagged by the new prefix lint
by moving the files into a subfolder named after the shared prefix and
dropping the prefix from the file names, updating all references:
- utils/env/handlers/infinito_*.py -> infinito/ (dir/, package_cache/
nested a second level); registry imports and the four handler-dir
globs switched to iter_project_files
- utils/install/lint/ansible_*.py -> ansible/ (aliased imports)
- roles/sys-svc-compose/files/swarm_*.{py,sh} -> files/swarm/
- web-app-erpnext/web-app-zammad files/scripts/apply_*.{py,rb} -> apply/
- web-app-odoo files/sql/{select,update}-* and sql/oidc/{grant,insert,
select,update}-* -> matching subfolders
- web-app-keycloak tasks/update/05_ldap*.yml -> 05_ldap/ (internal
includes made role_path-absolute)
- plugins/filter/get_*.py -> plugins/filter/get/ (filter names are the
filters() dict keys, so Jinja usage is unaffected; ansible discovers
the one-level-deep subdir)
Ansible lookup dirs, addon dirs and .github/workflows keep their flat
layout (the filename is the public id / GitHub only discovers flat).
Unit tests mirror the new source layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add DIR_BACKUPS to utils.paths and INFINITO_DIR_BACKUPS /
INFINITO_DIR_SECRETS env handlers, derive the recover-console constants
(including NFS_EXPORT_STATE) from utils.paths and the nfs export SPOT,
and switch every remaining self-built /var/lib/infinito subpath - the
secrets recover targets, the swarm extras backup source, the users
token-store candidate chain, the CI recover drill, the swarm NFS
diagnostics and the playwright reports dir - to those constants. Tests
consume the env constants directly; narration comments in the touched
files are dropped per the comment lint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.
Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).
Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote the Firefox onion fixture (secure-context .onion prefs, SOCKS
remote-DNS) from web-app-dashboard to test-e2e-playwright as the single
source of truth; the dashboard copy now re-exports it. Stage the shared
fixture on all three paths (deploy runner, lint-playwright, rerun-spec).
gotoOnion: onion navigations default to waitUntil=domcontentloaded —
heavy chunked SPAs (Element) serialise 30+ bundle requests over Tor, so
the load event regularly exceeds the navigation cap; callers' explicit
onion-scaled selector waits cover app boot instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gotoOnion hard-coded a 60s page.goto timeout, so a cold full-page load
over freshly built Tor circuits aborted before the onion answered and
the deploy gate flaked. The deploy now passes an onion-aware
PLAYWRIGHT_NAVIGATION_TIMEOUT (180s onion / 60s clearnet) plus a larger
onion per-test budget (600s), and gotoOnion honors the env instead of
the hard-coded ceiling. Fast loads are unaffected; only slow onion
navigations may use the larger budget.
Verified: full deploy matrix green with zero page.goto timeouts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chromium never treats plaintext .onion origins as a secure context, so
keycloak-js (PKCE via crypto.subtle) could not run. A custom Playwright
browser fixture launches Firefox with the Tor-Browser prefs
(dom.securecontext.allowlist_onions, origin/mixed-content handling) for
onion targets and keeps Chromium for clearnet; all dashboard specs share
it via the register pattern.
keycloak-js is now vendored from the deployment's own CDN as an ES
module (jsdelivr is unreachable over Tor) with CORS opened on the CDN
proxy. Personas and the oidc-login spec wait for adapter readiness and
navbar settle before driving login/logout, which the async code->token
exchange needs over Tor latency.
Verified: full deploy matrix green (onion 14/14 twice, clearnet pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
keycloak.js/logout.js wrap every wait in resolveTimeout so Tor's higher latency does not abort the OIDC adapter/logout waits. biber.js/admin.js drop the onion skip (personas now execute over onion instead of being skipped) and add a sharpen guard: a completed OIDC round-trip that still shows a Login control fails explicitly as a login failure, not a misleading 'no logout control'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>