* Set a `keycloakRoundTripCompleted` flag once `performKeycloakLogin`
has returned and the page is on the role's canonical hostname.
oauth2-proxy-gated roles whose upstream UI ships no Account/Logout
menu (Prometheus, status pages, raw upstream surfaces) now satisfy
the persona auth-marker without a probe-able in-app control. The
visible-marker poll still runs for roles that DO surface one.
* Add `[data-injected-logout]` as the first logout candidate so the
shared `inAppLogout` helper picks up the per-role JS-injected
fallback button (e.g. the Prometheus navbar patch) before falling
through to the regex-driven candidates.
* Replace the implicit "snapshot isVisible" gate at the top of
`inAppLogout` with a `waitForAnyLogoutCandidate` poll that pings the
candidate set for up to 10 s. Required because the universal /
per-role logout helpers inject their button asynchronously after
DOMContentLoaded plus a MutationObserver settle; the previous
synchronous check failed before the button could mount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Wait for the role's Login link with `Locator.waitFor` instead of the
snapshot-only `isVisible` — `isVisible({timeout})` only governs locator
resolution and never polls for visibility, so an OIDC-driven UI that
injects the Login link asynchronously (e.g. dashboard's `oidc.js`
fetched at runtime from the CDN) was treated as logged-out and the
helper returned in milliseconds without ever clicking the link.
* Once the link is visible, prefer a direct `page.goto(href)` to the
embedded openid-connect/auth URL when the link exposes one; fall back
to a real click plus `page.waitForURL(/openid-connect\/auth/)` so a
JS-handler-intercepted click (keycloak adapter, single-page-app shell)
still drives the test through Keycloak.
* Restrict the post-login URL fallback to nested frames. The main
frame can park on the role's canonical domain without an authenticated
session (the role's auth chain failed silently), so accepting the main
frame URL as proof of authentication produced false positives. The
iframe fallback still covers the legitimate "app surface rendered via
embed" case.
* Broaden the auth marker so role=link account/profile/user-menu
controls and `aria-label='account'`, `aria-haspopup`, Bootstrap
`data-bs-toggle='dropdown'`, and a `href*='logout|end_session'` link
count as the post-login surface signal. Roles that hide the auth
marker behind a closed dropdown still surface a probe-able anchor.
* Rebuild `inAppLogout` to iterate every visible menu trigger (Bootstrap
dropdowns, `.user-menu-toggle`, `data-testid='user'`, ARIA-popup
controls), not just the first match. After each click the helper
polls for logout/menuitem/link candidates inside the dropdown,
closes the menu again before trying the next trigger, and falls back
to a top-level `a[href*='logout']` so static-only logout links work
even when no dropdown wraps them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops `MAX_LINES` from 12 to 11 in `test_inline_script_block_size.py`,
catching the five blocks that were sitting exactly on the previous
threshold. Pure-bash blocks move to dedicated `.sh` files invoked via
`ansible.builtin.script:` or `run: bash …`; the keycloak heredoc with a
templated JSON payload splits into a `set_fact` plus a folded
`printf | kcadm … -f -` one-liner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forwards `PLAYWRIGHT_KEEP_ALL` from host env / GitHub Actions repo var
through `TEST_E2E_PLAYWRIGHT_KEEP_ALL` (inventory-overridable) into the
container and flips trace / screenshot / video to `on` for every test
when truthy.
SPOT for the propagation chain lives in
`docs/contributing/actions/testing/playwright.md#artefact-retention-`;
CI Configuration and the agent iteration loops link to it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five fixes against the matomo deploy CI failure mode:
* The persona helpers ([admin.js], [biber.js]) now perform the
Keycloak login on either auth shape — oauth2-proxy gate (the goto
is intercepted and the page lands directly on
`openid-connect/auth`) AND in-app OIDC plugin (Login link → click
→ redirect). Previously the Keycloak step was gated on
`oidcEnabled`, which left oauth2-proxy-only roles like prometheus
with no auth path. The login-link regex also tolerates leading /
trailing whitespace (`/^\s*(log\s*in|...)\s*$/i`), unblocking the
dashboard's `" Login"` link whose accessible name carries an
icon-induced leading space.
* `roles/web-app-keycloak/templates/playwright.env.j2`: declare
`PERSONA_BIBER_BLOCKED=true` and `PERSONA_ADMINISTRATOR_BLOCKED=true`.
The persona stubs in the keycloak spec exist purely to satisfy the
persona-naming lint; Keycloak IS the auth provider so neither
persona has an in-role app surface. The bespoke
master-realm / normal-realm tests already cover the contract.
* `roles/web-app-matomo/templates/playwright.env.j2`: same — matomo's
local login form is not OIDC / oauth2-proxy, so the generic helper
has no useful flow. The bespoke "matomo administrator" test covers
the admin journey.
* `roles/web-app-prometheus/templates/playwright.env.j2`: only
`PERSONA_BIBER_BLOCKED=true`. Prometheus is admin-only via
oauth2-proxy with `allowed_groups:
web-app-prometheus-administrator`; biber's session is denied at the
proxy and biber has no in-app surface to drive a logout from.
Administrator goes through the standard oauth2-proxy → Keycloak
chain. Also rendered the missing `PROMETHEUS_BASE_URL`,
`CANONICAL_DOMAIN`, and pointed `APP_BASE_URL` at prometheus itself
so the persona helper drives the role under test (not the dashboard).
* `roles/web-app-prometheus/files/playwright.spec.js`: read
`process.env.CANONICAL_DOMAIN` directly so the env-keys-used lint
recognises the new env entry as consumed.
The 019 matrix note for prometheus is updated to describe the
biber-only opt-out and the admin's oauth2-proxy chain.
Out of scope: the matomo CSP biber-deny test still fails because
`roles/web-app-matomo/meta/services.yml` has `oauth2.enabled: false`
(TODO at line 12 — wire matomo's oauth2-proxy gate). That is a
deploy-config change, not a test fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote four cheap rules in `eslint.config.js`:
* `no-unused-vars`: warn → error (with `argsIgnorePattern: "^_"`)
* `no-var`: error
* `eqeqeq`: error (always)
* `playwright/no-wait-for-navigation`: off → error
* `linterOptions.reportUnusedDisableDirectives`: off → warn
Add a repo-root CommonJS block so `eslint.config.js` itself sees Node
globals (`require`, `module`).
Fix every flagged offender:
* 138 stale persona-helper imports across 70 spec files
(`isServiceEnabled`, `skipUnlessServiceEnabled`, `runAdminFlow`,
... — leftovers from the SPOT consolidation).
* 4 unused catch bindings → modern `catch {}` syntax.
* 2 unused `dashboardBaseUrl` declarations (minio, moodle).
* 1 unused `label` arg → renamed to `_label` to opt out per the
argsIgnorePattern.
* 1 intentionally-kept legacy helper in web-app-wordpress's spec
receives a per-line `eslint-disable-next-line` with reason; the
preceding docstring already explained why it stays.
* 1 `page.waitForNavigation` in web-app-fediwall's Mastodon OIDC flow
receives a per-line disable with reason — the navigation target
depends on the runtime OIDC issuer; `waitForURL` would need a
runtime-built pattern.
* 10 stale `// eslint-disable-next-line no-console` directives now
surfaced by `reportUnusedDisableDirectives`; removed.
ESLint after this pass: 0 errors, 154 warnings (148 pre-existing
`playwright/expect-expect` plus 6 `playwright/no-force-option` that
were already inherited from `playwright/flat/recommended`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`,
`requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from
non-Markdown source files. Comments anchored to requirement numbers
rot when requirements are renumbered or rewritten and conflate
present-state behaviour (which the comment should describe) with the
audit trail of how the code got there (which belongs in commit
messages, PR descriptions, and the requirement file itself).
* Document the rule in `docs/contributing/documentation.md` under
"Comments". Code MUST describe the *why* in present-state language;
contract enforcement lives in tests, not folklore.
* Clean 254 source files across `roles/`, `tests/`, `plugins/`,
`cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root
`eslint.config.js`. Pure comment / docstring / log-message edits;
no behavioural change.
* Add `tests/lint/repository/test_no_req_references_in_code.py` to
enforce the rule going forward across `.js`, `.py`, `.yml`,
`.yaml`, `.j2`, and `.sh` files. The legacy
`test_no_stale_requirement_refs.py` is exempt as the only file
whose purpose is to detect these very tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five role specs (dashboard, nextcloud, odoo, pixelfed, taiga)
duplicated four small utility helpers across one-cluster-each
inline copies:
- isVisible(locator) — `locator.first().isVisible()`
with the strict-mode / detached
error swallowed.
- waitForFrameUrl(iframe, ...) — poll the embedded iframe URL
until it contains a matcher.
- findFirstVisibleCandidate(list) — pick the first visible
candidate from a label/locator
list and return the resolved
single-element locator.
- escapeRegex(value) — escape regex metacharacters
so a raw string can be used as
a `new RegExp(...)` source.
Changes
-------
- New `roles/test-e2e-playwright/files/personas/utils/dom.js` collects
all four helpers (canonical 1-cluster bodies, byte-equivalent to the
former inline copies).
- `personas/utils/index.js` re-exports the new module so callers reach
the helpers via `require("./personas")`.
- The five specs strip their inline definitions and amend the
destructuring with whichever helpers their body still references.
Drift handling
--------------
None. All four helpers were 1-cluster across the call sites; this is
mechanical substitution.
Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
`tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
baseline).
- 0 inline `function isVisible|waitForFrameUrl|findFirstVisibleCandidate|escapeRegex`
definitions remain in any role spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fourteen role specs (bigbluebutton, discourse, fider, gitea, mailu,
matrix, mattermost, odoo, openwebui, peertube, postmarks, prometheus,
yourls, libretranslate) each carried an inline `performOidcLogin`
that drove the Keycloak login form. The bodies fell into four
clusters split between the role-based selector strategy
(`getByRole`) and the legacy input-name strategy
(`input[name='username']`); the existing shared
`performKeycloakLogin` already covered both via `.or()` chains but
also asserted post-login navigation, so it could not be reused
verbatim.
Changes
-------
- `personas/utils/keycloak.js` gains `performKeycloakLoginForm
(target, username, password)` — form-fill + submit only, accepts
`Page` OR `FrameLocator` so iframe-embedded Keycloak pages work
without branching at the call site. The selectors are the union
of all four prior cluster shapes (role-based getter chained with
`.or(input-name fallback)`, sign-in matched against
`/sign in|login|log in/i` plus `input#kc-login` family). The
existing `performKeycloakLogin` and
`performKeycloakLoginExpectingDenial` now delegate to the new
helper, so their public API is unchanged.
- The 14 specs strip their inline `performOidcLogin` and amend
`require("./personas")` with `performKeycloakLoginForm`. Every
call site and comment reference is renamed in lockstep.
Drift handling
--------------
- Cluster 1 (5 specs) expected the accessible name `"Password"`
exactly; the canonical regex `/^password$/i` is case-insensitive
and matches every prior name plus lowercase variants — strictly
more permissive.
- Cluster 1's sign-in button matched `/sign in/i`; the canonical
matches `/sign in|login|log in/i` plus the legacy `input#kc-login`
family — strictly broader.
- Cluster 1 awaited `usernameField.press("Tab")` without a `.catch`;
the canonical wraps the press in `.catch(() => {})` so an
unexpected Tab failure no longer aborts the flow.
- Cluster 2 / 3 used `expect(usernameField).toBeVisible({ timeout })`
for the visibility wait; the canonical uses
`usernameField.waitFor({ state: "visible", timeout })`, the
Locator-method counterpart of the same wait — functionally
equivalent.
Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
`tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
baseline).
- 0 inline `performOidcLogin` definitions remain in any role spec,
and 0 stale references to the legacy name in any context (call
sites and comments are renamed in the same pass).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nine role specs (bigbluebutton, dashboard, discourse, keycloak,
matomo, matrix, moodle, openwebui, wordpress) each carried inline
copies of the same six CSP helpers and the `EXPECTED_CSP_DIRECTIVES`
constant. The bodies were semantically identical; only message
wording, brace style, and one captured-field-set differed.
Changes
-------
- Extends `roles/test-e2e-playwright/files/personas/utils/csp.js`
(already home to `assertCspInjections`) with
`installCspViolationObserver`, `readCspViolations`,
`parseCspHeader`, `assertCspResponseHeader`,
`assertCspMetaParity`, `expectNoCspViolations`, and the
`EXPECTED_CSP_DIRECTIVES` constant. The helpers are re-exported
through `personas/utils/index.js` so specs that
`require("./personas")` get them via the existing aggregator.
- Strips the inline definitions from the nine role specs and amends
each spec's `require("./personas")` destructuring with whichever
helpers the body still references.
Drift handling
--------------
- moodle's reduced violation-record shape (only `violatedDirective`
+ `blockedURI`) is replaced by the canonical 5-field record.
Moodle's downstream consumer asserts `.toEqual([])` on the array
only, so the extra fields are diagnostic-only — no behaviour
change.
- dashboard's 2-arg `expectNoCspViolations(page, label)` call site
is updated to the canonical 3-arg form
`expectNoCspViolations(page, null, label)`. The shared helper
guards `if (diagnostics && Array.isArray(diagnostics.cspRelated))`
before checking the diagnostics arm, so passing `null` keeps the
exact pre-migration behaviour for that call site.
- bigbluebutton and matrix had subtly different failure-message text
in `assertCspResponseHeader` and `expectNoCspViolations`. The
canonical wording is more descriptive; passing tests are
unaffected, failing tests get clearer diagnostics.
Doc
---
`docs/contributing/artefact/files/role/playwright.specs.js.md` is
extended in two places: the invariants section names the full CSP
test surface and pins the "import from `./personas`, no inline copy"
rule, and the per-service catalogue's CSP / injectors row lists the
helpers by name.
Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
`tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
baseline).
- `markdownlint-cli2`: 0 errors on the touched doc.
- 0 inline definitions of any extracted helper / constant remain in
any role spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both helpers were inlined verbatim in 71 of the role-local
`playwright.spec.js` files (52 with the compact form, 19 with the
verbose form, plus one parameter-name variant). All bodies were
semantically identical.
Changes
-------
- New `roles/test-e2e-playwright/files/personas/utils/dotenv.js` holds
the canonical pair (`decodeDotenvQuotedValue` and `normalizeBaseUrl`),
re-exported through `personas/utils/index.js` so specs that
`require("./personas")` get them via the existing aggregator.
- Every role spec drops its inline copies and pulls the helpers from
`./personas` instead. Specs that only reference one of the two
carry only that one in the destructuring (no unused-import noise).
Doc
---
`docs/contributing/artefact/files/role/playwright.specs.js.md` no
longer carries the "once a third spec needs them, MUST be promoted"
directive: the helpers live at the shared path and inline copies are
forbidden.
Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
`tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
baseline; no new unused-imports introduced).
- 0 inline `function decodeDotenvQuotedValue|normalizeBaseUrl`
definitions remain in any role spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cross-service surface assertions for dashboard, prometheus and matomo
no longer run in every consumer role's persona. They are owned by the
provider's own spec, parameterised once over a `<NAME>_TARGET_ROLES_JSON`
manifest rendered into the provider's `templates/playwright.env.j2` via
the new `lookup('roles_with_service', '<svc>')` Ansible lookup.
Changes
-------
- New `plugins/lookup/roles_with_service.py` enumerates roles that
declare `services.<svc>.{enabled, shared}` truthy in the merged
applications config, returning `[{id, canonical_domain}, …]`.
- `roles/web-app-{dashboard,prometheus,matomo}/files/playwright.spec.js`
parse `<NAME>_TARGET_ROLES_JSON` and parameterise the per-consumer
assertion (dashboard tile reachability, prometheus `up=1`, matomo
SitesManager registration). The matomo spec gains an explicit
biber-denial test (request body validated against admin DOM markers
per the body-marker rule); the prometheus admin-reach test drops
the dashboard-iframe path in favour of direct goto.
- Persona helpers `roles/test-e2e-playwright/files/personas/{admin,
biber,guest}.js` enter at `${APP_BASE_URL}/` directly. The
`clickRoleTileFromDashboard` step and the `assertPrometheus*` /
`assertMatomo*` cross-service probes are gone; `utils/{dashboard,
prometheus,matomo}.js` are deleted. `utils/csp.js` switches to
literal `process.env.<KEY>` reads so the env-keys-used parity guard
recognises matomo's CSP injector consumption.
- 42 consumer-role `templates/playwright.env.j2` templates drop
`DASHBOARD_BASE_URL`, `DASHBOARD_SERVICE_ENABLED`, `PROMETHEUS_BASE_URL`,
`PROMETHEUS_SERVICE_ENABLED`. `MATOMO_BASE_URL` and
`MATOMO_SERVICE_ENABLED` stay because the per-role CSP injection
helper still gates on matomo's tracker host.
- `tests/integration/roles/test_playwright_env_services_match.py`
globally exempts `dashboard` and `prometheus` from the
services.yml-to-env parity rule.
- `tests/unit/plugins/lookup/test_roles_with_service.py` adds 14
unit tests for the lookup, including an explicit-contract test
that pins the over-inclusive behaviour on raw Jinja strings (the
lookup expects a templar-rendered payload).
- Docs: `docs/requirements/019-playwright-meta-services-parity.md`
rewords Rules 1, 3, 12 and adds Rule 13 describing the SPOT shift.
`docs/contributing/artefact/files/role/playwright.specs.js.md`
reflects the new persona shape and per-service catalogue, and is
cleaned per `docs/contributing/documentation.md` (no SPOT self-
labels, no req-references, em dashes replaced with complete
sentences, multi-clause lines split onto separate lines, broken
anchor `#two-personas-fixed-flow-` corrected).
Verification
------------
- 418 passed / 1 skipped / 0 failed across `tests/lint/`,
`tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors.
- `markdownlint-cli2`: 0 errors on all touched docs.
- `make autoformat`: clean (FURB171 single-item-set inequality fixed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`tests/integration/roles/test_playwright_spec_env_gates.py` failed
across 35 roles: every spec that imports `runBiberFlow` /
`runAdminFlow` ships `DASHBOARD_SERVICE_ENABLED=` in its env template,
but the persona helpers consume the dashboard purely via
`process.env.DASHBOARD_BASE_URL` without ever passing the literal
`"dashboard"` to `safeIsEnabled` / `requireService` / friends. The
integration test grep'd for the string-literal call and (correctly)
saw zero matches, so flagged the flag as declared-but-never-gated.
Adds `safeIsEnabled("dashboard");` as a bare-statement gate to
admin.js and biber.js, parallel to the existing `safeIsEnabled
("oauth2"); safeIsEnabled("logout");` lines that already pin those
two flags to the persona surface. The integration test walks every
file under `roles/test-e2e-playwright/files/personas/` once any
persona runner is referenced in the spec, so a single mention covers
all 35 roles in one shot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coupled changes to `runBiberFlow` / `runAdminFlow`:
1. Move `PERSONA_BIBER_BLOCKED` / `PERSONA_ADMINISTRATOR_BLOCKED`
to position 0 of the flow. A role that opts out now short-circuits
the entire dashboard → app → logout journey instead of walking
through the auth chain and tripping the in-app logout step where
a late opt-out check could not catch it.
2. Add a URL-based fallback to the auth-surface detection: when the
selector-based marker (`getByRole("button", { name: /^(log\s*out…)/i
})`) finds no logout / user-menu control on any frame, accept any
nested frame whose URL has settled on the role's canonical domain
(and is NOT on Keycloak / oauth2-proxy denial / about:blank) as
evidence that the persona reached the authenticated surface.
Iframe-wrapped role surfaces (dashboard tile + role app inside)
and SPAs whose logout sits behind icon-only avatar dropdowns then
count as authenticated.
3. Drop `safeSkipUnlessEnabled("dashboard")` from the top of both
flows. The flag is rendered as `<NAME>_SERVICE_ENABLED=` from the
*provider* perspective in `meta/services.yml` (matomo's services
declare `dashboard.enabled: <web-app-dashboard in group_names>`,
meaning "is the dashboard role part of *this* role's stack").
When matomo deploys solo, the flag rendered as `false` even
though the dashboard URL was reachable, and every persona test in
the matrix run silently `-`-skipped. The auth-less collapse check
immediately below (`if (\!dashboardBaseUrl || \!canonicalDomain)
skip`) is the correct gate — a role that legitimately cannot
start the journey has no `DASHBOARD_BASE_URL` rendered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Persona helpers learn three new behaviours:
* `clickRoleTileFromDashboard` and `clickSiblingTileFromDashboard`
open every visible Bootstrap dropdown / accordion trigger
(`[data-bs-toggle='dropdown']`, `[aria-expanded='false']`, ...) on
the dashboard before searching for the role tile, so roles that
live inside a collapsed category (e.g. keycloak under "Identity")
are reachable without coupling personas to the dashboard menu shape.
* `assertPrometheusForbiddenForBiber` and
`assertMatomoForbiddenForBiber` switch from fragile iframe DOM
inspection to a direct authoritative probe via
`page.request.get(<role-url>, { maxRedirects: 0 })`. They peek at
the FIRST hop only: 401 / 403 or a redirect into the auth chain
(`/oauth2/start|sign_in|callback`, `openid-connect/auth`) means
denial; any other status is examined further. A first-hop 200 is
acceptable ONLY when the body carries role-specific markers
(`prometheus_build_info` / `<title>Prometheus</title>` for
prometheus; matomo's login-form markers / `piwik|matomo` for
matomo). A 200 with a non-matching body is treated as a
misconfigured proxy or a denial-as-200 surface and fails loudly,
matching req 019 Rule 12.
* `web-app-keycloak`'s spec drops the generic persona-flow runners
(`runGuestFlow` / `runBiberFlow` / `runAdminFlow`) under the
auth-provider exemption: the bespoke "master-realm super
administrator", "normal-realm administrator", and "normal-realm
biber logs in through account interface" tests already cover the
three persona contracts via the realm account UI. The matching env
flags are marked `# nocheck: playwright-service-gate` /
`# nocheck: persona-required-envs` with auth-provider rationales.
Drift matrix in `019-playwright-meta-services-parity.md` strikes
through `web-app-matomo`, `web-app-prometheus`, `web-app-keycloak`,
`web-app-akaunting`, `web-app-baserow`, `web-svc-cdn`, and
`web-svc-simpleicons` after their FULL_CYCLE deploys produced green
Playwright runs (with the well-known kcadm.sh + sys-svc-mail
infrastructure failures documented in `web-app-mailu/TODO.md` per
the autonomy escape clause).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The persona-flow runners now fail loudly when biber / administrator
cannot reach an authenticated surface on the role under test. The
previous runtime auto-detection silently `test.skip(...)`'d, hiding
real regressions (broken OIDC mapping, removed logout button,
misconfigured oauth2-proxy, drifted UI selectors) behind a green
deploy.
Roles where the persona legitimately has no access (admin-only roles,
auth-provider roles) MUST declare an explicit env opt-out
`PERSONA_BIBER_BLOCKED=true` / `PERSONA_ADMINISTRATOR_BLOCKED=true`
in `templates/playwright.env.j2` with a documented rationale; without
the flag the helper fails the test with a diagnostic naming the
last-seen URL.
A new lint guard (`test_persona_strict_mode.py`) hard-fails any
future `test.skip(...)` in the persona helpers that is not guarded
by an allowed env opt-out / auth-less-collapse / service-gate
condition, and any `status === 200` branch in the prometheus / matomo
deny-check helpers that does not validate role-specific body markers.
Req 019 Rule 11 (strict-skip contract) and Rule 12 (200-body validation)
plus a matching "Strict failure on un-executable persona cases"
section in `playwright.specs.js.md` document the contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces roles/test-e2e-playwright/files/personas/ — runGuestFlow,
runBiberFlow, runAdminFlow plus 10 utils (csp, dashboard, env, keycloak,
landing, logout, matomo, prometheus, interaction, index) — and wires
both the Ansible runner (run_one.yml) and the local rerun helper
(scripts/tests/e2e/rerun-spec.sh) to stage the tree alongside
service-gating.js so specs can `require("./personas")`.
First batch of role specs migrated to consume the shared library
(dashboard, keycloak, mailu, matomo, prometheus, web-svc-cdn,
web-svc-simpleicons) — each gets the spec.js persona scenarios plus
the matching meta/services.yml + playwright.env.j2 entries; matomo is
marked role-closed in req 019. dashboard/keycloak/mailu TODO.md
updated; docs/requirements/019 reflects matomo closure.
Reason: req 019 Rule 3 mandates that every role's guest/biber/
administrator flow runs the same canonical steps; centralising them
in one shared library keeps per-role drift from re-introducing the
stub-shaped bodies the new lint forbids.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes 67 live references to docs/requirements/<NNN>-*.md files across
43 contributor docs, role READMEs, code comments, task files, and tests.
Requirement docs are initial-creation-only artefacts; live references
freeze a snapshot that rots once the per-feature SPOT (a contributor
doc, a code module, a lint test) takes over the live behaviour.
In the same pass, fixes documentation.md violations on the touched
files: drops em-dashes used as thought breaks, removes "SPOT for ..."
self-labels, strips historical phrasing ("the legacy", "previously",
"used to", "is no longer used", "matching the historical"), and
reflows soft-wrapped sentences so each sentence stays on one logical
line.
The `test_inventory_vars_file_spot.py` whitelist for the literal
inventory path drops the docs/requirements/003 entry and instead
skips docs/requirements/ entirely; future requirement docs that
mention the literal path are then ignored automatically.
Verified by tests/lint/repository/test_no_stale_requirement_refs.py
(green tree-wide).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements requirements 005, 006, and 007.
005: per-application Keycloak `group-ldap-mapper`s anchor each app's
role groups under `/roles/<application_id>/<role_name>` (and, for
tenant-aware apps, `/roles/<application_id>/<tenant>/<role_name>`).
The flat-CN LDAP layout keeps each CN globally unique so Keycloak's
group-tree resolver never collides; per-app mappers use the LDAP
`description` as the visible Keycloak group name to keep the path
free of redundant prefixes. Migration purges legacy pre-005 entries
and forces a Keycloak `logout-all` so stale tokens drop pre-005
group paths.
006: per-service `<SERVICE>_SERVICE_ENABLED` env flags and the
`service-gating.js` helper let role specs `skipUnlessServiceEnabled`
cleanly when a shared service is missing or `SERVICES_DISABLED` is
set; `MODE_CI` markers (`INFINITO_MAKE_DEPLOY`, `GITHUB_ACTIONS`,
`ACT`) propagate through the deploy CLI into the test stage.
007: WordPress->Discourse round-trip Playwright scenario gated on the
`web-app-discourse` deployment.
WordPress Playwright runs 5/5 green on the single-site flow; the
multisite + Discourse scenarios are correctly skipped when their
service prerequisites are absent. The `rbac_group_path` lookup is the
single sanctioned producer for OIDC group paths and is wired into
every consumer (yourls, lam, minio, openwebui, pgadmin, phpldapadmin,
phpmyadmin, prometheus, xwiki).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add image_version filter plugin to extract bare semver from image tag
- Derive TEST_E2E_PLAYWRIGHT_VERSION via image_version filter
- Convert files/package.json to templates/package.json.j2 using the
derived version so image tag and devDependency stay in sync
- Use lookup('image', 'playwright', 'ref') in run_one.yml instead of
building the image ref manually from package.json
- Add unit tests for image_version filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Transient issues (DNS timeout, slow CI runner) no longer cause immediate
failure — each test gets up to 2 retries before being marked as failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Increase global test timeout from 60s to 300s; the OIDC login flow
alone can consume the full 60s budget, causing timeouts in subsequent
wait steps (waitForVisibleCandidate at line 171 in the CI logs)
- Replace non-existent NC 30+ selectors verified against the actual
nextcloud:production-fpm-alpine image:
- remove [aria-controls="header-menu-user-menu"] (not in NC 30+)
- remove settingsMenuButton role "Settings menu" (not confirmed)
- replace #content-vue / #appmenu with #header-start__appmenu
(layout.user.php uses <nav id="header-start__appmenu">)
- remove #logout (no such ID in layout.user.php)
- Dismiss the firstrunwizard modal before clicking the user menu:
firstrunwizard-activate.mjs opens a NcModal after DOMContentLoaded
on first login; its .modal-mask intercepts all clicks and would
cause the user-menu click to fail silently on first login
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>