Commit Graph
242 Commits
Author SHA1 Message Date
kevinveenbirkenbach bae2050d59 fix(test-e2e-playwright): gate the whole run behind IS_STACK_HOST
02_run_one.yml asserts that the self-signed CA cert exists at
/etc/infinito.nexus/ca/root-ca.crt and bails out hard when it does
not, but the cert is only laid down on the host that owns the stack.
Under swarm-mode delegation the role plays on every host so workers
hit the CA-presence guard and the matrix job fails before any
Playwright spec runs. Affected jobs in CI run 27006276488:
web-app-bridgy-fed, web-app-fider, web-app-fusiondirectory,
web-app-openwebui, web-app-postmarks, web-app-yourls.

Move every task from main.yml (dirs, setup include, run loop,
failure aggregation) except the leading debug into a new
tasks/01a_orchestrate.yml, then include it from main.yml only when
IS_STACK_HOST is true. Workers print the role list and stop; the
stack-host runs Playwright as before. compose mode keeps
IS_STACK_HOST true so single-host runs are unchanged.
2026-06-05 13:19:28 +02:00
kevinveenbirkenbach 85c1ed1a6e Merge branch 'main' into feature/web-app-zammad
# Conflicts:
#	CHANGELOG.md
#	packaging/arch/PKGBUILD
#	packaging/debian/changelog
#	packaging/fedora/infinito-nexus.spec
#	pyproject.toml
#	roles/sys-svc-compose/README.md
#	scripts/docker/entry.sh
#	scripts/github/update/image_versions.sh
#	scripts/github/update/repository_refs.sh
2026-05-29 11:18:28 +02:00
kevinveenbirkenbachandClaude Opus 4.7 8ab37347b1 feat(test-e2e-playwright): add host.docker.internal:host-gateway to sidecar runs
Gives Playwright sidecars a stable name for the docker host so role specs
can reach services whose port is bound on the host (e.g. Mailu's SMTP
submission on 465/587 in the Zammad email-channel test) without
attaching to that service's docker network.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:53:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 35137a83a3 fix(playwright): satisfy eslint preserve-caught-error + no-useless-assignment
Five eslint errors blocking make lint-javascript:

* personas/guest.js, web-app-opencloud/playwright.spec.js,
  web-app-opentalk/playwright.spec.js — re-thrown wrapped errors now
  carry { cause: err } so the symptom keeps a link to the root cause.
* personas/utils/csp.js — drop let blockedHost = "" initial value;
  the catch arm returns early, so the empty string was never read.
* web-app-wordpress/_shared.js — drop let match = null initial value;
  both branches assign before the readers fire.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:53:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f19a0b246b fix(playwright): satisfy eslint preserve-caught-error + no-useless-assignment
Five eslint errors blocking `make lint-javascript`:

* `personas/guest.js`, `web-app-opencloud/playwright.spec.js`,
  `web-app-opentalk/playwright.spec.js` — re-thrown wrapped errors now
  carry `{ cause: err }` so the symptom keeps a link to the root cause.
* `personas/utils/csp.js` — drop `let blockedHost = ""` initial value;
  the catch arm returns early, so the empty string was never read.
* `web-app-wordpress/_shared.js` — drop `let match = null` initial value;
  both branches assign before the readers fire.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 18:54:44 +02:00
kevinveenbirkenbach 21d81ccdc2 Merge branch 'main' into feature/req-21-sso-flavor-migration 2026-05-27 18:24:00 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fc2ac7597a refactor(lookup): rename status -> deployment, add runtime-aware deployed
`status` was too vague for what the lookup returns (apps in current
play). Rename to `deployment` and add two new fields:

* `runtime` — RUNTIME variable value
* `deployed` — runtime-aware authoritative set: equals `running`
  (whitelist or groups) for ephemeral CI runtimes (act, github),
  falls back to `groups` for persistent runtimes (host, dev). This
  matches reality: in CI the container is fresh so only whitelisted
  apps exist; in production a partial deploy still leaves prior
  apps live, so groups is the right set.

Switch the two webserver/csp healthchecks to `lookup('deployment').deployed`
so they only probe domains for apps that actually exist in the current
deploy. In production behaviour is unchanged.

Update remaining consumers (cli/build/include, web-app-keycloak,
test-e2e-playwright) for the rename. Existing `status` semantics
(`whitelist`, `running`, `groups`, `all`) preserved under the new
name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 18:52:43 +02:00
kevinveenbirkenbachandClaude Opus 4.7 24d4ab8e4a refactor(sso)\!: collapse oauth2 + oidc into unified services.sso block (req 21)
Implements docs/requirements/021-sso-flavor-migration.md atomically:

* Schema: every role's meta/services.yml replaces the parallel
  `oauth2:` / `oidc:` top-level blocks with a single `sso:` block
  carrying `enabled`, `shared`, and a `flavor` discriminator
  (`oidc` | `oauth2` | `saml`; default `oidc`). Flavor-specific
  sub-keys live under `sso.<flavor>.*` (`sso.oauth2.{origin,acl,
  allowed_groups}`, `sso.oidc.plugin`).
* Provider role: roles/web-app-oauth2-proxy/ is deleted in its
  entirety; its 5 sidecar templates move under
  roles/web-app-keycloak/templates/sso_proxy/, and the per-consumer
  rendering task lives at roles/web-app-keycloak/tasks/sso_proxy.yml.
  web-app-keycloak now declares `provides: sso`.
* Runtime: new resolver utils/roles/applications/services/sso.py
  centralises the SSO predicates (`is_enabled`, `is_proxy_gated`,
  `is_oidc_native`) and the oauth2 sub-values
  (`oauth2_origin_{host,port}`, `oauth2_acl`, `oauth2_allowed_groups`).
  Three lookup plugins consume the resolver: `sso` (single-app
  predicate access), `sso_proxy_consumers` (enumerates oauth2-flavored
  consumers), `sso_oidc_plugin` (renamed from `oidc_flavor`, Nextcloud
  plugin selector). sys-stk-backend, sys-svc-compose, sys-svc-proxy
  and plugins/filter/compose_volumes.py all gate on
  `lookup('sso', ..., 'is_proxy_gated')` instead of manually combining
  `enabled` and `flavor`.
* Identifier renames repo-wide: `services.<entity>.ports.local.oauth2`
  → `ports.local.sso`; `PORT_BANDS.local.oauth2` → `PORT_BANDS.local.sso`;
  `OAUTH2_PROXY_*` env/Jinja vars → `SSO_PROXY_*`;
  `oauth2_proxy_cookie_secret` → `sso_proxy_cookie_secret`;
  `OAUTH2_SERVICE_ENABLED` + `OIDC_SERVICE_ENABLED` collapse into
  `SSO_SERVICE_ENABLED`; Playwright gating helpers
  (isServiceEnabled/skipUnlessServiceEnabled/requireService/safeIsEnabled/
  safeSkipUnlessEnabled) move from `oidc`/`oauth2` to `sso`.
* Dual-block resolution: per req's Background-Table, web-app-bookwyrm
  is `flavor: oauth2`; web-app-gitea and web-app-friendica keep
  `flavor: oauth2` with their existing `oauth2.acl.blacklist`
  (/user/login) and `oauth2.acl.whitelist` (federation + discovery
  endpoints) — preserving the operative oauth2-proxy gating.
* Tests: the two mutual-exclusion guards
  (tests/integration/iam/oauth2_oidc/test_mutual_exclusive.py and
  test_acl_mutual_exclusive.py) are deleted per Decision #10;
  test_oauth2_contract.py is renamed and rewritten to
  test_sso_contract.py, asserting that `flavor: oauth2` roles carry
  `oauth2.origin.{host,port}` plus `ports.local.sso`; test_sso.py
  (web-app integration) now requires the unified `sso:` block with an
  explicit flavor or a `# nocheck: sso` opt-out; new
  tests/lint/repository/no_legacy_sso_paths/ guards the source tree
  against re-introducing the legacy strings.
* Docs: req 21 itself has every AC checked off, and reqs 006, 008,
  009, 013, 015, 016, 019, 020 plus 10+ role READMEs and
  docs/contributing/actions/testing/suppression.md are updated to the
  unified SSO naming.

Empirically validated end-to-end against the validation app set
(web-app-keycloak, web-app-nextcloud, web-app-bookwyrm, web-app-gitea,
web-app-friendica, web-app-dashboard) in matrix variants 0 and 1; v2
also passes the 4 target roles' Playwright after a documented
pre-existing KC_HOSTNAME-drift workaround (separate concern, not
caused by this refactor).

BREAKING CHANGE: every consumer of `services.oauth2.*`,
`services.oidc.*`, `OAUTH2_PROXY_*`, `OAUTH2_SERVICE_ENABLED`,
`OIDC_SERVICE_ENABLED`, `oauth2_proxy_cookie_secret`,
`oauth2_proxy_application_id`, `ports.local.oauth2`, the
`web-app-oauth2-proxy` role, or the legacy gating helper service names
(`oidc`/`oauth2`) must migrate to the unified `sso` shape documented
in docs/requirements/021-sso-flavor-migration.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 12:59:59 +02:00
kevinveenbirkenbachandClaude Opus 4.7 053fedcbeb refactor(required_by-lint): SPOT helpers + yaml-aware mailu test
- Move role_is_invokable / role_has_required_by / role_has_nocheck out
  of plugins/filter (where they were flagged as test-only filters) into
  utils/roles/required_by_coverage.py. All three use load_yaml_any,
  read_text, and ROLE_FILE_META_SERVICES from the canonical SPOTs.
- Refactor the verifier (cli.meta.roles.services.called) the same way:
  iterate roles/*/ directly, look up meta/services.yml via the constant,
  parse with load_yaml_any, and tolerate a missing roles_dir (unit tests
  point at a fake /repo).
- Make the mailu-dependency integration test yaml-aware for meta/
  services.yml: walk the parsed tree and skip values inside any
  `required_by` subtree. `required_by` is inverse coverage metadata for
  the deploy verifier, not a "consumer of mailu" reference, so previous
  regex-on-text matching produced false positives for cert-deploy.
- sys-ctl-mtn-cert-renew is gated by `lookup('tls', application_id,
  'mode') == 'letsencrypt'`, which the verifier can't express via
  role-id/category match — mark nocheck with the reason inline.
- Add README.md for cli/meta/roles/services (CLI-category convention).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 04:23:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4bd23d5f1b chore(roles): mark non-invokable roles with nocheck:required-by-coverage (Bundle D)
Adds `# nocheck: required-by-coverage — TODO: declare proper required_by`
markers to meta/services.yml of 74 non-invokable system roles (dev-*, pkgmgr*,
sys-aur*, sys-front-inj-*, sys-front-tls-*, sys-svc-{cdn,certbot,certs,
cln-domains,dns,journalctl,mail*,proxy,rdbms,ssh,sshd}, sys-timer*, sys-stk-*,
user-*, …).

This is the passthrough layer that lets the upcoming `required_by` lint
(Bundle C) hard-fail any *new* non-invokable role without an explicit coverage
declaration, while existing roles get a documented TODO grace period instead
of an immediate CI break.

No runtime effect — comments only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 04:23:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 38588af4eb style(test-e2e-playwright): wrap long Jinja line in vars/main.yml
Autoformat side-effect: TEST_E2E_PLAYWRIGHT_IMAGE exceeded the YAML
line-length budget. The rendered value is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:38:13 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5196283f6c refactor(test-e2e-playwright): bake @playwright/test into a shared node_modules
Per-role Playwright runs no longer call `npm install` against
registry.npmjs.org on every invocation. A new `01_setup.yml` runs once
per host/version, populates `_shared/node_modules/` inside the Playwright
container, and stamps the installed version in `.installed-version`.
Each `02_run_one.yml` mounts that tree read-only at `/e2e/node_modules`
and the container command becomes `npx --no-install playwright test`.
This eliminates the npmjs.org TLS-handshake flake that took out the
keycloak playwright in a recent variant-2 update, saves ~4 s per role,
and gives version bumps a single rebuild point.

Three retry layers cover transient registry failures: Ansible (3× with
5 s delay), npm fetch (5× with 10-60 s backoff), and image pull (3×).

Stage-base / shared-dir / stamp / node_modules paths live as SPOT
constants in `vars/main.yml` so call sites never reconstruct them.

Renames `tasks/run_one.yml` → `tasks/02_run_one.yml` (and adds
`01_setup.yml`) to make the include order visible by filename; updates
cross-references in `AGENTS.md`, the two `docs/contributing/...` pages,
`dump_disk_diagnostics.sh`, and the lint test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:23:24 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b3c519b914 refactor(images): route role image refs through lookup('config', ...) so the GHCR mirror takes effect
`lookup('image', '<role>', '<svc>', 'ref')` only consulted
`images_overrides.*` plus role defaults, which the mirror pipeline
never populates — the pipeline writes into
`applications.<role>.services.<svc>.{image,version}`. As a result,
`sys-svc-container`'s `BUSYBOX_IMAGE` / `NODE_IMAGE`,
`sys-ctl-hlth-csp`'s `HEALTH_CSP_IMAGE`, and `test-e2e-playwright`'s
`TEST_E2E_PLAYWRIGHT_IMAGE` always resolved to the raw upstream ref
and hit Docker Hub directly. In CI this surfaced as
`toomanyrequests: ...unauthenticated pull rate limit` on
`busybox:1.37`.

Switches every productive `lookup('image', ...)` to
`lookup('config', '<role>', 'services.<svc>.image|version')` — the
same path templates like `web-app-oauth2-proxy` already use, which
resolves through the merged `applications.*` map and so transparently
picks up the GHCR mirror override written by the inventory creator.

Removes the now-unused `plugins/lookup/image.py` plugin and its unit
test (zero remaining consumers across roles, scripts, and tests).
Rewrites the image-origin / image-mirror / dockerfile docs to teach
the `lookup('config', ...)` pattern, dropping references to the
deleted plugin and the dead `images_overrides.*` variable. Doc edits
follow the `documentation.md` rules (no em dashes, one sentence per
line, no historical phrasing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:53:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 91fc422f19 feat(make): add compose-playwright target + schema-aware help
Adds `make compose-playwright role=<role> [pw="..."] [keep=true]` as the
canonical inner-loop entry point for rerunning a role-local Playwright
spec against the live stack — replaces the raw
`scripts/tests/e2e/rerun-spec.sh` invocation across docs.

Defines a small comment-block schema for Makefile targets (description
line on top, followed by `Usage:` / `Example:` / `Note:` / `Param <name>:`
markers). Lint enforces it; `make help` shows the description per target
in the short list and `make help target=<name>` prints the full block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:53:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 79faaf21e7 refactor(ansible): move INFINITO_* env lookups out of group_vars/roles into dev inventory
\`lookup('env', 'INFINITO_*')\` calls in group_vars / roles couple
deployment-agnostic Ansible code to default.env, which only exists in
dev/CI. Production deploys never source .env, so the lookups silently
fall back to their group_vars defaults and the inventory's intent gets
lost on the way (see the IP4 host_vars-overlay incident fixed in
8980b00b1).

Migrated keys (group_vars/roles -> inventory):
- DOMAIN_PRIMARY            (00_general.yml)
- BUILDKIT_ENABLED          (00_general.yml)
- CA_CONTAINER_ENABLED      (02_tls.yml)
- defaults_networks.internet.{ip4,ip6,dns}  (08_networks.yml)
- TEST_E2E_PLAYWRIGHT_KEEP  (roles/test-e2e-playwright/defaults/main.yml)

group_vars and the role default now carry literal fallback values;
the dev/CI inventory at \`inventories/development/default.yml\` pulls
the live values from .env via straight \`lookup('env', 'INFINITO_*')\`
calls without any \`| default(...)\` filter, because load.sh guarantees
the keys are set whenever .env exists.

The convention is enforced by the new lint test
\`tests/lint/filesystem/env/test_no_ansible_env_lookups.py\`, which
scans group_vars/, tasks/, roles/, and playbook.yml for any
\`lookup('env', 'INFINITO_*')\` against a default.env key and fails
with a pointer to the dev inventory as the injection site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:57:13 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a77a570409 refactor(env): consolidate INFINITO_* namespace and centralize path/image SPOTs
Closes a stack of env-pipeline regressions left over from the recent
INFINITO_* namespace refactor by:

* DOMAIN_PRIMARY now reads INFINITO_DOMAIN strictly (was: legacy DOMAIN
  env var → defaulted to infinito.localhost, which never resolved
  because CoreDNS serves the INFINITO_DOMAIN value, infinito.example).
* group_vars/all/08_networks.yml IPs (ip4/ip6/dns) now read
  INFINITO_IP4 / INFINITO_IP6 / INFINITO_DNS_IP.
* PLAYWRIGHT_KEEP_ALL renamed to INFINITO_PLAYWRIGHT_KEEP end-to-end
  (env-builder, role default, run_one.yml docker exec, playwright.config.js,
  lint allowlist, docs anchor, CI workflow vars).
* PKGMGR_IMAGE renamed to INFINITO_PARENT_IMAGE; Dockerfile ARG drops
  the inline default so env/default.env is the only SPOT.
* INFINITO_SRC_DIR centralized: Dockerfile ARG (no default) + ENV;
  compose forwards it as build-arg, volume mount path, and container env;
  every Python CLI + shell helper + CI workflow now reads it strictly
  (os.environ[...] / "${INFINITO_SRC_DIR:?...}") instead of the literal
  /opt/src/infinito.
* New entries in env/default.env: INFINITO_IP6=::1, INFINITO_PARENT_IMAGE,
  INFINITO_PLAYWRIGHT_KEEP=false. All three propagated through
  utils/env/handlers/passthrough.STATIC_KEYS and (where consumed in the
  container) through compose.yml's environment block.

Validated end-to-end with `make test` (2360 tests across lint, lint-*,
test-lint, test-external, test-unit, test-integration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 02:30:34 +02:00
kevinveenbirkenbach 162fe71634 fix(test-e2e-playwright,web-app-mailu): replace inline persona skip-sniffing with the PERSONA_*_BLOCKED contract
Commit 1f6b5f894 fixed the mailu V1 guest 60-s timeout by adding inline
auto-detection skips to the shared persona runners (`\!oidcEnabled`,
`dashboardEnabled === "false"`), but those guards are exactly what the
persona strict-mode lint
(tests/lint/ansible/roles/web-app/playwright/persona/test_strict_mode.py,
Rule 11) forbids: auto-detection skips can silently disable tests
when an env flag is mistyped, so only the allowlisted opt-out tokens
(`PERSONA_*_BLOCKED`, `safeSkipUnlessEnabled` / `skipUnlessServiceEnabled`,
the auth-less collapse case) are permitted.

Re-fix in the lint-compliant shape:

- Drop the four inline `test.skip(...)` blocks from
  personas/{admin,biber,guest}.js plus the `safeIsEnabled("dashboard")`
  parity marker added alongside them.
- Add the missing `PERSONA_GUEST_BLOCKED` opt-out check to
  personas/guest.js so the existing biber/administrator block pattern
  also covers guest.
- Render `PERSONA_GUEST_BLOCKED` dynamically in
  roles/web-app-mailu/templates/playwright.env.j2 — true exactly when
  `services.dashboard.enabled` is false. Drops the now-unreferenced
  `DASHBOARD_SERVICE_ENABLED=` line.

Behaviour for mailu V1 stays identical (guest persona skipped, just
via PERSONA_GUEST_BLOCKED rather than via persona-internal hostname
sniffing). Verified against the live stack with rerun-spec.sh across
all 13 currently staged Playwright roles — 46 pass / 44 skip / 0 fail,
0 deltas vs. the prior fresh-purged V2 deploy outcomes.

If another role later hits the same dashboard-frame routing failure
in a fresh-purge sweep, the fix is one analogous
PERSONA_GUEST_BLOCKED={{ ... }} line in that role's
templates/playwright.env.j2.
2026-05-19 18:06:50 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1f6b5f8942 fix(test-e2e-playwright): gate shared personas on dashboard + oidc service flags
CI run 26089179898 job 76714878217 (Deploy web-app-bluesky, variant 1)
failed at the mailu Playwright `guest: public-landing → auth chain →
never authenticated` test with 60s timeout × 3 retries. Root cause is
two layered defects:

1. Between Round 1 and Round 2 of the matrix deploy the fresh-purge
   sweep removes the web-app-dashboard vhost and container (mandate-
   fresh-purged on first contact with a new VARIANT, see commit
   3f12fbe83). Round 2 in variant 1 deploys only matomo/mailu/keycloak/
   bluesky; the dashboard.infinito.example vhost never comes back.

2. mailu's templates/playwright.env.j2 pins APP_BASE_URL to
   `lookup('tls', 'web-app-dashboard', 'url.base')` unconditionally —
   it has to, because the shared persona helpers route through the
   dashboard main-frame Account menu. In variant 1 (dashboard disabled
   for mailu) that URL is dangling: nginx's default_server falls onto
   the bluesky-pds backend, which serves a text/plain ASCII banner.
   readCspString's `<meta>` fallback in personas/utils/csp.js has no
   explicit getAttribute timeout and waits on the test-level 60s.

The shared persona itself has no way to learn that APP_BASE_URL routes
through a dashboard frame whose service is disabled — biber + admin
sidestepped this only because mailu already blocks them via
PERSONA_BIBER_BLOCKED / PERSONA_ADMINISTRATOR_BLOCKED for an unrelated
silent-SSO escape. guest had no equivalent opt-out and timed out.

Decided by service flag, not by sniffing the response, so the skip
fires before any network I/O:

- roles/test-e2e-playwright/files/personas/guest.js: skip when
  APP_BASE_URL's host starts with "dashboard." AND
  DASHBOARD_SERVICE_ENABLED=false.
- roles/test-e2e-playwright/files/personas/biber.js +
  personas/admin.js: same dashboard-frame skip + a new OIDC-required
  skip (these personas drive the Keycloak round-trip; without OIDC
  they cannot reach an authenticated surface). Skips placed
  immediately before the dependent operation, not at the top, so the
  reason matches the operation that would actually fail. parity-marker
  safeIsEnabled("dashboard") added to the existing oauth2/logout/
  matomo block so the env-gate parity guard recognises the new
  consumption.
- roles/web-app-mailu/templates/playwright.env.j2: emit
  DASHBOARD_SERVICE_ENABLED in the existing OIDC/LOGOUT/MATOMO_SERVICE
  _ENABLED block. The other 14 dashboard-frame roles will pick up
  this declaration when they hit the same variant-disabled failure.

biber + admin remain unconditional-blocked for mailu via the existing
PERSONA_*_BLOCKED flags; the new skips inside them are forward-looking
for the day the silent-SSO escape lifts and those personas are
un-blocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:00:38 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f24b2d903b style(playwright,csp): strip explanatory comments per minimal-comments rule
Removes prose block comments, JSDoc headers, and multi-line nocheck
rationale paragraphs added in the previous two commits — keeps only
the bare nocheck markers (which carry actual exception semantics
the lint test enforces) and the unchanged function/test bodies. No
behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 01:09:02 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b6a41941ec test(test-e2e-playwright): assert injected assets load + CSP allows them across css/logout/matomo
Adds the shared `assertInjectedAssetLoadsWithoutCspBlock(page, opts)`
helper in personas/utils/csp.js. It installs the CSP violation
observer, navigates the consumer URL with `waitUntil: networkidle`,
and asserts both:

  1. the browser observed a successful 2xx/3xx response for the
     requested resourceType(s) from one of the injector hosts, AND
  2. no `securitypolicyviolation` event named one of those hosts as
     its `blockedURI`.

Closes the loophole where the markup-only checks would pass even
when the consumer's CSP `style-src` / `script-src` did not whitelist
the injector host — the `<link>` / `<script>` was in the HTML but
the browser silently dropped the load.

Wires the helper into the three consumer-injection provider specs:

- web-svc-css: replaces the HTML-regex link check with the helper
  (`stylesheet` resource type, css.* + cdn.* hosts).
- web-svc-logout: extends the existing `logout.js`-in-HTML check with
  the helper (`script` resource type, cdn.* host). Adds CDN_BASE_URL
  to the env template since logout.js is served from the CDN.
- web-app-matomo: extends the per-consumer tracker test with the
  helper (`script` resource type, matomo canonical host) before the
  existing `_paq` / `matomo.php` markup tokens, so the matomo.js
  async fetch after DOMContentLoaded is verified end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 01:09:02 +02:00
kevinveenbirkenbachandClaude Opus 4.7 16cab61397 fix(image-lookup): require explicit role_id to prevent wrong-role resolution
The `image` lookup inferred `role_id` from `role_name` when only
`service_name` was passed. Inference broke lazy re-evaluation: a default
like `HEALTH_CSP_IMAGE: "{{ lookup('image', 'csp-checker', 'ref') }}"`
defined in sys-ctl-hlth-csp got re-evaluated inside sys-service's
systemctl.service.j2 template (via `system_service_tpl_exec_start`),
where `role_name='sys-service'`. The lookup then searched
`roles/sys-service/meta/services.yml` for `csp-checker` and aborted with
`no image mapping found for role 'sys-service' service 'csp-checker'`.

CI run 25976825584 surfaced this on six apps (web-app-{bridgy-fed,chess,
mig,hugo,mini-qr,postmarks}) whose deploy reached sys-svc-webserver-core
outside the service-loader preload context, which is what actually
triggers the sys-ctl-hlth-csp -> sys-service include chain.

Make `role_id` mandatory in the plugin signature, remove `_infer_role_id`,
and update the three callsites that relied on inference
(sys-ctl-hlth-csp, sys-svc-container, test-e2e-playwright) to pass the
owning role id explicitly. A migration guard catches the removed
`(service_name, want)` form and emits a clear error instead of the
confusing "missing services.yml" file-not-found message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:11:30 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0aa34b903e feat(playwright): persist artifacts per variant+pass, add Time column
- Matrix-deploy orchestrator now pushes `VARIANT_INDEX` (0-based round
  id) alongside the existing `ASYNC_ENABLED` extra var for both PASS 1
  (sync) and PASS 2 (async) runs. Test-e2e-playwright's archive step
  consumes both to derive a per-(variant, pass) destination so a
  subsequent round cannot overwrite the previous one's reports.
- New `roles/test-e2e-playwright/files/archive_playwright_reports.sh`
  moves the playwright-junit.xml / playwright-report/ / test-results/
  outputs into `variant-<N>/<sync|async>/` under the role's reports
  dir, idempotently (re-running the same tuple replaces rather than
  fails). `run_one.yml` invokes the script via `ansible.builtin.script`
  so the YAML stays a single 7-line task.
- `VARIANT_INDEX: 0` lives in `defaults/main.yml` to keep the variable
  defined for dev / non-matrix invocations. Role defaults are not
  considered SPOT, so the `| default(0)` belt-and-suspenders inside
  the script invocation stays valid against the spot-default lint.
- Summary script now renders an 8-column table: Time | App | Variant |
  Pass | Test | Status | Duration | Message. Per-test ISO timestamps
  are derived from the JUnit `<testsuite timestamp>` attribute plus the
  cumulative duration of preceding cases; file mtime is the fallback
  when the attribute is absent. App / Variant / Pass are extracted
  from the artefact path layout produced by the archive script.
- Matrix-deploy unit tests refreshed to expect the new
  `extra_ansible_vars` shape; summary unit tests gain coverage for
  `_to_iso_z`, `_parse_iso_timestamp`, the four supported path
  layouts, and the file-mtime fallback (55 tests total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:30:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 181c46d742 feat(ci): render Playwright JUnit reports as workflow step summary
- New `utils/github/playwright_summary.py` parses every
  `playwright-junit.xml` under a given artefact directory and prints a
  markdown table to stdout with one row per test case: App, Test,
  Status (🟢 passed / 🔴 failed / 🔵 skipped), Duration (sub-second
  precision), Message. Sub-second formatting keeps useful per-test
  signal that integer rounding would erase. Edge cases (missing dir,
  no junit files, all-unparseable) emit a friendly placeholder note
  instead of crashing. Covered by 26 unit tests in
  `tests/unit/utils/github/test_playwright_summary.py`.
- `test-deploy-server.yml` runs the script with `if: always()` after
  the deploy step and pipes its output into `$GITHUB_STEP_SUMMARY`, so
  every matrix job's run page surfaces a per-app test table without
  needing to download artefacts.
- Playwright config adds the `github` reporter so failing tests also
  emit inline `::error file=...,line=...::`-annotations directly on the
  workflow run page and in PR diffs.
- New lint `tests/lint/filesystem/test_plugin_paths_files.py` enforces
  that files under the Ansible plugin source directories declared in
  `ansible.cfg` (`action_plugins`, `filter_plugins`, `lookup_plugins`,
  `library`, `module_utils`) are Python source files (README.md
  exempt). Paths are read live from `ansible.cfg` so a future config
  edit automatically extends the lint's coverage. Counterpart to the
  existing `test_scripts_folder_files.py` lint that enforces
  `scripts/` contains only `.sh`.

Known limitation (pre-existing pipeline behaviour, not introduced by
this change): when a role runs through multiple matrix variants, the
container path `/var/lib/infinito/logs/test-e2e-playwright/<app_id>/`
has no variant component, so each variant overwrites the previous
variant's reports. The summary therefore reflects only the LAST
variant per role. A future change to namespace the reports per
variant (or copy them after each variant pass) will make the table
cover every variant without touching the script or workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:30:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 83466868e9 feat(update): support mcr.microsoft.com in docker version checker
- Add MCR_REGISTRY constant, is_mcr / mcr_repo / fetch_mcr_tags helpers
  mirroring the Docker Hub and GHCR pattern. Tags are fetched via the
  Docker Registry V2 API with RFC 5988 Link-header pagination (anonymous
  access; MCR is a public read mirror). Wire MCR into both the external
  test_image_versions warning surface and the find_outdated_updates
  orchestrator in utils.update.docker.
- Bump mcr.microsoft.com/playwright in test-e2e-playwright from
  v1.59.1-noble to v1.60.0-noble — the update was surfaced by the new
  MCR detection path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:02:23 +02:00
kevinveenbirkenbachandClaude Opus 4.7 48cc98c7cc fix(status,playwright): tokenise CSV inputs + tolerate stringified list repr
- plugins/lookup/status.py: `list(str)` on `APPLICATIONS_WHITELIST` (CSV from `-e`) split it into chars, breaking every consumer of `.whitelist` / `.running` (web-app-roles when-clause skipped all apps, keycloak per-app LDAP mappers never created).
- test-e2e-playwright/discover_playwright_roles: tolerate Python list-repr strings from non-native-Jinja2 stringification of `"{{ lookup('status').groups }}"` — otherwise `TEST_E2E_PLAYWRIGHT_APPS` resolves to `[]` and no spec runs.
- Unit tests cover both regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:24:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1f665b8bcc chore(test-e2e-playwright): trim role description; widen ONLY_ROLES to all host groups
- meta/main.yml: cut the wordy auto-discovery description to a one-liner ("Runs end-to-end Playwright tests against deployed applications.").
- defaults/main.yml: `TEST_E2E_PLAYWRIGHT_ONLY_ROLES` now reads `lookup('status').groups` (the full host-deployed set) instead of `lookup('status').running` (the CLI `--id`-narrowed subset). Playwright now exercises every dashboard-discoverable app on the host regardless of the deploy's `--id` scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:24:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c2c532c8e6 refactor(playwright): centralize dashboard-tile mechanics in web-app-dashboard SPOT; move specs into files/playwright/ subdir
Architecture
============
- New SPOT helper `roles/web-app-dashboard/files/playwright/dashboard-card-flow.js` (`runDashboardCardScenario(page, context, target)`): per `roles_with_service('dashboard')` target, asserts tile visible → click loads in `#main iframe` (outer URL gets `?iframe=` + iframe src matches canonical_domain) → header "Tab" button pops the embed into a fresh browser tab.
- `web-app-dashboard/files/playwright/playwright.spec.js` parametrised loop calls the helper for every `DASHBOARD_TARGET_ROLES_JSON` entry — the single owner of dashboard-tile mechanics across the whole codebase.

Spec relocation
===============
- 71 `roles/<role>/files/playwright.spec.js` → `roles/<role>/files/playwright/playwright.spec.js` (`git mv`, history preserved). odoo + pixelfed show as D+A in `git status` because of substantial content rewrites; coverage retained.
- New SPOT constant `ROLE_FILE_PLAYWRIGHT_SPEC` in `utils/roles/mapping.py`, registered as optional on `ROLE_TYPE_APPLICATION` (disallowed elsewhere, mirrors `ROLE_FILE_META_VARIANTS` pattern).
- `roles/test-e2e-playwright/tasks/run_one.yml`: staging task replaced with `with_fileglob` over `files/playwright/*.js` — roles can ship companion helpers (like `dashboard-card-flow.js`) next to the spec without further wiring.

Spec migrations (no more dashboard-iframe path in app specs)
============================================================
13 specs rewritten so the persona/SSO journey starts at the app URL directly, never through the dashboard tile or iframe:
* fider, mailu, mattermost, nextcloud, odoo, taiga: iframe-wrap removed (no more `frameLocator("#main iframe")`)
* bigbluebutton, discourse, kix, matrix, openwebui, peertube: dashboard goto preamble removed
* pixelfed: ~270 LOC of iframe-aware helpers/login/logout collapsed to direct-URL flow

Title cleanup
=============
- 64 specs: `"<persona>: dashboard → app → universal logout"` / `"administrator: dashboard → prometheus → app → universal logout"` → `"<persona>: app → universal logout"`.
- 13 specs: `"<persona>: dashboard to <app> ..."` → `"<persona>: <app> ..."`.

Dead-code cleanup
=================
- `dashboardBaseUrl` consts + beforeEach assertions removed from 11 specs (no functional uses; personas helper handles env validation).
- `DASHBOARD_BASE_URL=` env-template lines removed from fider, mattermost, odoo, taiga, pixelfed, kix.
- `DASHBOARD_SERVICE_ENABLED=` env-template lines removed (pixelfed retained for the still-gated spec).
- Stale "loads inside the iframe" / "FrameLocator parameter" comments cleaned in fider, odoo, prometheus, nextcloud.

Tooling
=======
- 5 lint Python files + 1 integration test now use `from utils.roles.mapping import ROLE_FILE_PLAYWRIGHT_SPEC`.
- `eslint.config.js` glob pattern + `roles/test-e2e-playwright/meta/main.yml` description + `roles/test-e2e-playwright/README.md` updated to new path.
- 25 markdown/docstring references in `docs/`, `roles/**/README.md`, `roles/**/TODO.md`, persona-helper docstrings updated to new path.
- `docs/contributing/artefact/files/role/playwright.specs.js.md`: file-placement section now documents the `files/playwright/` convention + `ROLE_FILE_PLAYWRIGHT_SPEC` SPOT constant; dashboard SPOT delegation explicit at 3 places.

Net diff: 124 files, +1142/-1564 (−422 LOC).
Integration suite: 183/183 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:24:55 +02:00
kevinveenbirkenbachandClaude Opus 4.7 944e782d84 refactor(applications): introduce status lookup + rbac_app_ids filter; rename allowed_applications → APPLICATIONS_WHITELIST
- plugins/lookup/status.py: new lookup returning {whitelist, running, groups, all}, cached per (whitelist, groups). Consumers replace direct group_vars references.
- web-app-keycloak/filter_plugins/rbac_per_app.py: add `rbac_app_ids` (intersect input list with invokable apps; sorted, deduped).
- web-app-keycloak/tasks/update/05c_per_app_mappers.yml: drop two set_fact tasks; consume KEYCLOAK_RBAC_{ALL,RUN}_APP_IDS as constants from vars/main.yml. Split orphan-purge scope (all host-deployed) from mapper-sync scope (current run).
- Rename Ansible-side `allowed_applications` → `APPLICATIONS_WHITELIST` (group_vars/all default, deploy CLI `-e` injection, generated include `when:` clause, debug task, test asserts). Python kwarg name unchanged (internal contract).
- test-e2e-playwright: `TEST_E2E_PLAYWRIGHT_ONLY_ROLES` consumes `lookup('status').running`; `TEST_E2E_PLAYWRIGHT_APPS` moved from set_fact to vars/main.yml constant.
- 01_constructor debug: collapse to single `status:` block over the lookup result.
- web-app-{opencloud,opentalk}/vars: drop dead `group_names | default([])` + fix latent `| lower`-on-list precedence bug.
- New lint `test_no_jinja_default_on_ansible_magic_var.py`: forbid `| default(...)` on Ansible magic vars (group_names, inventory_hostname, …); meta-scan + 7 fixture positive cases.
- Unit tests for `rbac_app_ids` (10) and `status` lookup (8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:39:52 +02:00
kevinveenbirkenbachandClaude Opus 4.7 16bdbf03d7 docs: fix repo-wide drift against documentation.md style rules
Three classes of drift cleaned up across 85 .md files:

- Em-dashes: replaced ` — ` with the contextually correct separator
  (colon for `term: explanation`, period to split clauses across
  sentences/source lines, parentheses for asides, semicolon for
  tight-linked continuations). Two intentional exceptions remain
  in `docs/contributing/documentation.md` (the rule literal) and
  inside a fenced ```sh block in `docs/contributing/actions/debugging/ci.md`.
- SPOT self-labels: rewrote "This is the SPOT for X" to describe
  the subject directly ("This directory holds X" / "This file
  describes X"). Cross-references to other artefacts' canonical
  role remain unchanged.
- Heading-emoji position: moved leading-emoji headings (`# 🧩 Title`)
  to trailing position (`# Title 🧩`) per the headline rule that
  emojis MUST come after the title text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d65d9b6765 fix(test-e2e-playwright): cap Playwright sidecar runtime + block generic personas on Friendica
Two changes that together stop one badly-behaved role from stalling
the whole deploy:

1. `roles/test-e2e-playwright/tasks/run_one.yml` — pass `timeout:
   {{ TEST_E2E_PLAYWRIGHT_RUN_TIMEOUT }}` to the `command` task. New
   default `1200` (20 min) lives in `defaults/main.yml`. Without an
   outer Ansible-side cap, a single hung test could burn 5 min ×
   3 retries × N specs before either `--max-failures` or the
   deploy supervisor noticed; friendica's run today sat at 27 min
   before the operator killed it manually.

2. `roles/test-e2e-playwright/defaults/main.yml` — append
   `--max-failures=5` to the default Playwright command. Once five
   tests have failed, the sidecar stops instead of chewing through
   the rest of the retry budget for tests that are obviously broken.

3. `roles/web-app-friendica/templates/playwright.env.j2` — declare
   `PERSONA_BIBER_BLOCKED=true` and `PERSONA_ADMINISTRATOR_BLOCKED=true`.
   Friendica authenticates against its own local DB / LDAP via the
   `ldapauth` addon, not OIDC. The generic `runBiberFlow` /
   `runAdminFlow` helpers drive an OAuth2-proxy + Keycloak round-trip
   that Friendica cannot reconcile, so the generic persona tests
   wedge on the Keycloak sign-in form. The bespoke admin/biber tests
   above own the actual login flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbach e22d772574 fix(test-e2e-playwright/personas): fail fast on guest goto error + cap CSP fetch
The guest scenario's `page.goto` previously swallowed every navigation
error via `.catch(() => null)`. When the role's public surface was
unreachable, the subsequent CSP probe in `readCspString` ran another
HTTP request without an explicit timeout and fell back to Playwright's
30s default. Stacked on top of the stalled goto this consumed the 60s
test-level limit and surfaced as `Test timeout of 60000ms exceeded`,
masking the real cause (backend unhealthy).

Re-throw with a precise message when goto rejects so the CI artifact
points at the unreachable URL inside the 30s navigation timeout, and
cap `page.request.get` inside `readCspString` at 10s so a single
stalled CSP fetch can no longer add 30s to the test budget.
2026-05-14 17:12:56 +02:00
kevinveenbirkenbach 92cc9a49df test(lint): forbid | default(...) on resolvable group_vars/all SPOT paths
Adds tests/lint/ansible/test_no_jinja_default_on_spot_path.py: scans every
Jinja `{{ ... | default(...) }}` block under roles/, tasks/, group_vars/,
identifies the actual default target via a paren-aware left-walk from the
matched `|`, and fails when the target's dotted path resolves cleanly
against any group_vars/all/*.yml dict tree. Role vars/defaults are
intentionally out of scope (Ansible loads them only when the owning role
is in the play, so cross-role defaulting is a legitimate caller guard,
not SPOT decoupling). Per-line opt-out: `# nocheck: spot-default`.

Fixes the 21 violations the test surfaces:
- group_vars/all/00_general.yml: drop `| default('')` on CAPTCHA self-checks
- group_vars/all/02_tls.yml: drop `| default(...)` on DOMAIN_PRIMARY in CA subject
- roles/web-app-{listmonk,keycloak,espocrm}/meta/schema.yml: drop defaults on CAPTCHA.*
- roles/web-app-suitecrm/templates/env.j2: map empty SECURITY -> 'none' via `or`,
  not via the default filter (SPOT value `""` is always set)
- roles/web-app-opentalk/tasks/main.yml: drop default on MODE_ASSERT (always bool)
- roles/user-workstation/tasks/01_core.yml: drop default on MASK_CREDENTIALS_IN_LOGS
- roles/test-e2e-playwright/tasks/run_one.yml: drop defaults on TLS_MODE + CA_TRUST.*

Also corrects the dotted-path bugs in roles/web-app-odoo/vars/main.yml that
referenced non-existent LDAP keys (`LDAP.SERVER.REACH_DOMAIN`,
`LDAP.USERS.SERVICE_ACCOUNT.*`, `LDAP.ATTRIBUTES.ID`, `LDAP.SERVER.SSL`) to
the canonical paths actually defined in group_vars/all/12_ldap.yml.
2026-05-14 17:12:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4481976fb9 fix(playwright): two-pass OIDC Login link + PERSONA_BLOCKED for nextcloud/opencloud/minio
Two-pass Login link resolution in `personas/utils/keycloak.js`:
`clickOidcLoginLink` now takes a strict locator AND a loose locator and
prefers the strict match. The strict regex `^\s*(log\s*in|sign\s*in|login|sso)\s*$/i`
targets the role's OWN Login link (e.g. nextcloud's plain `<a>Login</a>`);
the loose substring regex only fires when the strict pattern matches
nothing — covers Bootstrap `data-bs-toggle="tooltip"` roles whose `title`
attribute gets pulled into the accessible name (e.g. dashboard's
"Login to Infinito.Nexus on infinito.example"). Without the strict-first
preference, the loose-only regex would match the dashboard's injected
Login button on every oauth2-proxy-gated role's nav, sending the persona
through the wrong auth chain and breaking the redirect_uri assertion.

PERSONA_BLOCKED for three roles whose auth chains can't be driven by the
generic persona helper, with bespoke tests already owning persona
coverage:

- web-app-nextcloud: `/login` reverse-proxies Keycloak's form on the
  nextcloud domain (no `/openid-connect/auth` URL marker). Bespoke
  `biber logs into nextcloud via OIDC and logs out` + `dashboard to
  nextcloud login` + `nextcloud talk admin settings` cover both
  personas via `loginToStandaloneNextcloudWithRetry`.
- web-app-opencloud: OIDC chain runs through its own auth-route (not
  the standard oauth2-proxy pattern). Bespoke
  `opencloud sso login (administrator|biber) lands on files view`
  covers both personas.
- web-app-minio: STS via OIDC, biber has no minio account. Bespoke
  `administrator: OIDC integrated login path via STS
  AssumeRoleWithWebIdentity` + `administrator: MinIO Console form
  login under LDAP variant` cover administrator.

Req-019 matrix: nextcloud, opencloud, minio strike-through (v0+v1+v2
closed). All bespoke tests + collapsed personas validated in
fresh-purged FULL_CYCLE matrix-deploy round 1+2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 94a80db92a fix(web-app-dashboard): close req-019 — silent-check-sso postMessage + PKCE-safe persona Login + variant=1 gates
Root causes diagnosed inside the running container per
docs/agents/action/iteration/playwright.md, fixed in repo, validated
via inner-loop rerun-spec.sh, and finally re-deployed via FULL matrix
variant=0 (all dashboard tests green) and reuse-kept variant=1
(persona/asset/login-swap tests collapse cleanly via env + test.skip
gates, 2 passed + 5 skipped, failed=0).

- templates/nginx/sso.html.conf.j2: serve the standard Keycloak
  silent-check-sso body — `parent.postMessage(location.href,
  location.origin)`. Without that script, the hidden iframe never
  reports its URL back to the parent and `keycloak.init()` hangs
  forever; the role's OIDC adapter then never wires its click
  interceptor and the persona helper races into the static `href`
  without PKCE state.
- templates/javascript/oidc.js.j2: set `window.__oidcLoginReady = true`
  after `wireLoginLogoutClicks()` so persona helpers can detect that
  the JS-wrapped click handler is in place BEFORE clicking. The flag
  name SPOT is `OIDC_LOGIN_READY_FLAG` in
  `roles/test-e2e-playwright/files/personas/utils/keycloak.js`.

- personas/utils/keycloak.js: new `clickOidcLoginLink(page, loginLink)`
  helper — single source of truth for the OIDC-link click ritual
  (waitFor visible → waitFor `__oidcLoginReady` → click → waitForURL
  `/openid-connect/auth/`). Replaces the inline duplicated block in
  biber.js + admin.js, and removes the static-href direct-nav fallback
  that broke PKCE-enforced clients silently.
- personas/biber.js + admin.js: delegate to `clickOidcLoginLink` and
  drop the `^...$` regex anchors on the Login link locator. Bootstrap
  `data-bs-toggle="tooltip"` moves the link's `title` into
  `data-bs-original-title` and the accessibility tree pulls THAT into
  the link's accessible name (e.g. "Login to Infinito.Nexus on
  infinito.example") — the anchored regex matched zero links. The
  `\s*` between `log` and `in` keeps the substring pattern from
  matching `Logout`/`logoff`.

- templates/playwright.env.j2: render
  `PERSONA_{BIBER,ADMINISTRATOR}_BLOCKED` from
  `services.oidc.enabled`. Variant=1 disables every shared service —
  with OIDC off there is no Login link to click, so the persona
  scenarios MUST collapse to a clean skip instead of failing loudly.
- files/playwright.spec.js: add `test.skip(\!cdnEnabled || \!logoutEnabled, ...)`
  on the asset-loading test and `test.skip(\!oidcEnabled, ...)` on the
  Login → Account swap test. The bespoke tile-reachability loop
  already skips cleanly when `DASHBOARD_TARGET_ROLES_JSON` is empty,
  so no further gate needed there.

- TODO.md: removed. The autonomy escape clause is satisfied.
- docs/requirements/019-playwright-meta-services-parity.md: strike
  through `web-app-dashboard` row (v0+v1 closed) and document the
  silent-check-sso / `__oidcLoginReady` / Bootstrap tooltip
  contracts inline in the row's notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:10:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 76bfd9e665 fix(persona/inAppLogout): wait for Account dropdown trigger, not just direct logout
`waitForAnyLogoutCandidate` previously only polled for logout-shaped
elements (link/button/menuitem matching `log out|sign out|abmelden`),
which are hidden inside the Account dropdown for any role whose
logout sits behind a user menu. With a 10s ceiling that timed out
before the dashboard's async post-login chain (CDN-loaded keycloak-js
+ token exchange) flipped the Account button visible, so the persona
helpers `biber: dashboard → app → universal logout` and
`administrator: dashboard → prometheus → app → universal logout`
failed even though the dashboard's own bespoke OIDC test (which
polls 60s for the Account button) passed.

Change: also poll for the menu triggers `inAppLogout` already knows
how to open (Account/Profile/user-menu buttons + Bootstrap
`data-bs-toggle="dropdown"` and friends), and raise the default
timeout to 30s. Returning true the moment a trigger surfaces lets the
downstream menu-open loop click it and read logout from inside the
opened dropdown. Per-test Playwright timeout is 300s, well above the
new wait ceiling.

The change is behavior-preserving for already-passing roles:
direct-logout roles still return on the first poll cycle within
milliseconds; menu-behind-logout roles that worked before now exit
the wait earlier; the soft-fail outcome for genuinely unreachable
logouts is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:10:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2c0d5fb9fc fix(playwright/personas): treat Keycloak round-trip as auth proof + wait for injected logout
* 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>
2026-05-11 03:48:53 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9763388229 fix(playwright/personas): harden auth chain + logout discovery
* 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>
2026-05-11 03:48:52 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b1b73bc1df refactor(lint): tighten inline-script lint to 11 lines + extract overflows
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>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 576fad84ff refactor(images): move role images from defaults/main.yml to meta/services.yml
Consolidates image declarations under a single schema: every role's
`<svc>.{image,version}` lives in `meta/services.yml` alongside compose
service entries. The `image` lookup now reads from disk per role,
`iter_role_images` collapses to a single source, and the mirror chain
(resolver + apply_mirror_overrides) drops the dead `images:` branch.

`test-e2e-playwright` resolves `TEST_E2E_PLAYWRIGHT_IMAGE` directly via
`lookup('image', 'playwright', 'ref')` in vars/main.yml; the runtime
set_fact + override-empty-string dance is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 14700e0658 feat(test-e2e-playwright): add PLAYWRIGHT_KEEP_ALL artefact-retention toggle
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>
2026-05-10 22:06:31 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f6f4236ffd fix(playwright/personas): wire PERSONA_*_BLOCKED + handle oauth2-proxy auth chain
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>
2026-05-10 18:53:39 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0e651e4a22 chore(lint): tighten ESLint rules and clean up flagged offenders
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>
2026-05-10 16:25:23 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c55ad69c41 refactor: drop requirement-file references from code, add doc rule + lint
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>
2026-05-10 16:25:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 eb7344cc04 refactor(playwright): share DOM + regex utility helpers
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>
2026-05-10 16:25:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 370f381d1c refactor(playwright): share Keycloak login-form helper
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>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 7018b3c991 refactor(playwright): share CSP header/meta/violation helpers
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>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 122c82910b refactor(playwright): share decodeDotenvQuotedValue + normalizeBaseUrl
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>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 db2a2d9638 refactor(playwright): move cross-service tests to provider specs
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>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fa39d5522b fix(test-e2e-playwright/personas): gate dashboard via safeIsEnabled
`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>
2026-05-10 14:55:04 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a35636051e feat(playwright/personas): early-skip + URL fallback + drop bogus dashboard gate
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>
2026-05-10 12:06:50 +02:00