Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).
This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.
Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:
* 7 `src:` values built from a role variable holding a bare filename
(svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
The variable doubles as the destination name inside the container, so the
language segment belongs at the call site, not in the variable.
* `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
18 roles ship exactly that and have no template fallback: the play would
have died on each.
* `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
interface now reads `files/javascript/javascript.js`.
* `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
path through `path_absolute(... 'files', ...)`.
* 4 `ansible.builtin.script:` invocations naming a bare command
(pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
* `dev-python/files/shell/install.sh` pointed at a sibling role's script.
Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.
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 and never wait. Passing resolveTimeout(10_000) to one therefore changes nothing at runtime and only tells the reader a wait happened that never did. That reading is what let 7538b8a0b conclude two genuine call sites were fine, and what hid the false negative b97cf8888 had to fix.
Sixty call sites across fifty-eight role-local specs lose the argument and nothing else. Every added line in this commit is a bare isVisible(); a grep for added lines that are not one returns zero, so a behaviour change cannot have entered here. Files whose diff also alters waiting semantics are deliberately not part of this commit.
A repository lint added in b97cf8888 keeps the construct out. It matches isVisible and isHidden only, because isChecked, isDisabled, isEditable and isEnabled take a real timeout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6f9142027 gave twelve consumer specs the onion skip that nextcloud already
carried, and mastodon is the one it missed. `git show 6f9142027 --stat` names no
mastodon file, and no commit on any ref ever added the gate to the role:
`git log --all -S'isOnionTarget' -- roles/web-app-mastodon/` is empty. Measured
before this change, the role reported 0 occurrences of isOnionTarget while
matrix, nextcloud and taiga reported 2 each; thirteen roles carried the skip,
now fourteen do.
The gap is not cosmetic, because the gate the spec did have guards the wrong
thing. skipUnlessServiceEnabled("seaweedfs") asks about the BACKEND service,
which is enabled on a Tor node; the filer's browser surface is not. The spec
therefore ran, and adminLoginFiler navigated to the clearnet filer host. On a
node whose Tor network is a private chutney instance with no clearnet exit that
host does not answer 404 -- it hangs, and Playwright reported net::ERR_TIMED_OUT.
The skip has to come before the backend gate for that reason, which is where the
thirteen siblings put it and where this change puts it. The message string is
byte-identical to theirs.
Not verified by a run: what is proven is the parity with thirteen siblings and
the domain derivation in timeouts.js:15-17, not an observed skip.
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>
The scenario never authenticated. It went to /auth/sign_in and then only
checked whether the URL had reached openid-connect/auth, on the assumption
that OMNIAUTH_ONLY=true makes Mastodon redirect into the OIDC chain. It does
not — Mastodon renders its own login page carrying an /auth/auth/openid_connect
link, so the condition was always false, performKeycloakLogin never ran, and
the run continued unauthenticated until it failed 1.5 min later on a missing
avatar input.
Route through clickOidcLoginLink first, exactly as personas/admin.js does; its
provider-shaped selector matches Mastodon's link. The persona logout tests on
this same role were already green through that helper — the spec had bypassed
it and rebuilt the condition by hand.
With the login repaired, a second defect surfaced behind it: newer Mastodon
moved profile editing to /profile/edit and left /settings/profile as a notice
page with no avatar input. Follow the pointer link when it is present, so the
spec still works against older builds.
Both causes come from the Playwright error-context snapshots, not from reading
the code: the first showed Mastodon's login page, the second the logged-in
settings navigation plus the relocation notice.
Verified: make compose-playwright role=web-app-mastodon pw="--grep seaweedfs"
=> 1 passed (30.3s), previously 3 x 1.5 min timeout. The check uploads and then
polls the Filer UI until a new bucket object appears, so Mastodon -> SeaweedFS
is proven end to end.
Also drops the file-header paragraph, whose OMNIAUTH_ONLY claim was the false
premise the broken conditional was built on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role's env template shipped no persona flags, so both authenticated
personas ran unconditionally. Every CI variant is deployed, and variant 1 pins
services.sso.enabled false -- there the administrator has nothing to log into:
tasks/05_administrator.yml creates the account with `tootctl accounts create`,
which neither sets nor returns a password, so ADMIN_PASSWORD (the Keycloak
secret) cannot drive Mastodon's native /auth/sign_in. biber is worse off: no
Mastodon account is provisioned for him at all, and the OIDC sign-in wired at
templates/env.j2:61-72 is the only path that would create one. Both flags now
follow services.sso.enabled, in the same ternary web-app-baserow already uses.
files/playwright/test-seaweedfs.js gated only on seaweedfs, but it drives the
same admin journey -- Keycloak sign-in, /settings/profile, avatar upload.
services.sso.enabled and services.seaweedfs.enabled are independent
group-membership expressions, so an inventory carrying seaweedfs without
keycloak was a hard failure. It now skips when the administrator persona is
blocked.
meta/services.yml carried `# nocheck: playwright-service-flag — object store,
no Playwright surface` above the seaweedfs key. The commit that added
test-seaweedfs.js made that false, and the suppression was dead anyway: the env
template declares SEAWEEDFS_SERVICE_ENABLED, so test_env_services_match never
needed an exemption. Deleted rather than reworded.
Two nocheck rationales asserted things that are not true: that mail delivery is
"covered by the sys-svc-mail integration tests" -- no such tests exist, the
coverage is web-app-mailu's own spec -- and that the shared CSP helper asserts
the CSS injector's origin, which personas/utils/csp.js:96 refutes with
`if (!host) continue` since no consumer role renders CSS_BASE_URL.
Not verified: test-seaweedfs.js has still never run against a live stack. Its
selectors were authored against upstream Mastodon's UI without execution; the
new guard only keeps it out of the variant where it could not pass, and says
nothing about whether it passes in the variant where it now runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_has_spec.py forces every roles/web-app-* to ship a Playwright spec and
calls a missing one a hard error, because the persona contract must never be
silently absent from the deploy capstone. Nothing forced the other half. The
runner discovers roles by rglob('templates/playwright.env.j2')
(discover_playwright_roles.py:53), so a spec without that file is staged
nowhere and executed never.
21 of 76 web-app roles were in exactly that state. Verified rather than
inferred: running the discovery filter directly returns 61 roles and none of
the 21 is among them, `make compose-playwright` refuses without a rendered
.env from a prior deploy, and `git log --all` on those paths shows the file
never existed on any branch -- the single hit is web-app-gitlab on a fork
branch. Their specs have never run once. 15 of the 21 deploy in CI today,
reporting green while their only functional verification never fires.
The new lint mirrors test_has_spec and carries a per-role opt-out that is
used nowhere: uniform beats exceptions.
Every one of the 21 now has a template written against its own
meta/services.yml rather than a shared skeleton -- espocrm splits
ADMIN_PASSWORD from ADMIN_NATIVE_PASSWORD because OIDC_ALLOW_ADMIN_USER
leaves both login paths open, the four permanently auth-less roles declare
their personas blocked instead of shipping a knowingly-red spec, and
web-app-mastodon gained a real test-seaweedfs.js because no honest
suppression existed for a role that does route Paperclip to S3.
web-app-magento moves pre-alpha -> alpha here: it now has a spec that runs,
which is what the lifecycle gate is about.
Six meta/services.yml files change only where adding a template falsified an
existing suppression -- gitlab and shopware both claimed the role 'ships no
templates/playwright.env.j2', which stopped being true in this commit.
Not resolved here, and the adversarial review says so plainly: the specs are
enabled, not proven. None has executed against a live stack. The review found
three classes of defect that land in follow-up commits rather than being
smuggled in silently -- persona blocks that are missing or should be tied to
sso.enabled, eleven suppressions naming a sys-svc-mail test suite that does
not exist, and eight claiming a CSP assertion that cannot fire because no
consumer role renders CSS_BASE_URL.
Co-Authored-By: Claude Opus 5 (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>
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>
Add the global TIMEOUT_FACTOR and a shared timeouts.js helper: resolveTimeout(base) scales by TIMEOUT_FACTOR and, over a .onion target, an extra multiplier, so Tor's per-request latency no longer trips hard-coded timeouts. Migrate every raw timeout in the specs (timeout:/timeout=/setTimeout/waitForTimeout) to resolveTimeout, stage the helper in all three staging paths, and add test_no_raw_timeouts to forbid raw numeric timeouts. Clearnet behaviour is unchanged (factor 1: resolveTimeout(N) === N).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HSTS (Strict-Transport-Security) is an HTTPS-only header: a browser ignores it
over plain http (RFC 6797) and OpenResty only emits it on the TLS vhost. On an
onion node TLS is off and every domain is served over http, so the inline
`expect(headers["strict-transport-security"]).toBeTruthy()` in 33 specs failed
with `Received: undefined`.
Route all 33 through the shared expectHstsWhenTls(headers, baseUrl, label)
helper (personas/utils/tls.js), which requires HSTS over https and asserts its
absence over http. On a TLS node the base URL is https, so the assertion is
identical to before; on an onion node the same spec now passes. HSTS logic
lives in one place (SPOT) instead of 34 inline copies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>