Commit Graph
26 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 303c47fe2a fix(test-e2e-playwright): retry transient onion transport errors for every API verb, not only GET
0a52433dcb gave the standalone request fixture a Tor-resilient GET,
because the bundled socks client caps the CONNECT at 30s and a cold
onion circuit fails a request regardless of its timeout. POSTs and calls
made through context.request still went out once, and a circuit Tor
rejected ("Socks5 proxy rejected connection") was not recognised as
transient at all.

apiFetchOnion now takes the verb from opts.method and calls
request.fetch, so it serves both APIRequestContexts; apiGetOnion becomes
its GET shorthand. The rejection joins the transient patterns. Retries
stay onion-only and transport-only: clearnet keeps a single attempt and
HTTP or other errors re-throw at once.

The Mattermost LiteLLM spec routes its login, users/me, users/username
and channels/direct calls through it. The /posts call that sends the
prompt stays a single attempt, because a replayed post would message the
bot twice. The Hermes administrator spec reads /v1/models through
apiGetOnion.

env.test.js pins a POST surviving a rejected circuit, the single
clearnet attempt, no retry of a non-transport error over Tor, and the
GET shorthand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 12:05:02 +02:00
kevinveenbirkenbachandClaude Opus 5 347904c04a test(mattermost): keep polling for the bot reply through a transport error
firstBotReply already treats an HTTP error as "no reply yet" and returns an
empty string, but a transport failure throws out of context.request.get, and
expect.poll does not catch what its callback throws, so the attempt ends
there instead of at the poll's deadline. Over tor that is routine. Job
102705652088 of run 34421944328 lost three retries to it, each thrown at
test-litellm-agent.js:54 from inside the poll (:142):

    TimeoutError: apiRequestContext.get: Timeout 30000ms exceeded.   after 6.2 of 15 minutes
    Error: apiRequestContext.get: Socks5 proxy rejected connection - Failure   twice

A failed request now counts like a failed response: the poll asks again two
seconds later and only its deadline decides. A reply is still required, so a
transport that never recovers still fails the test, with the poll's message
rather than the last transport error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 11:08:26 +02:00
kevinveenbirkenbachandClaude Opus 5 8b9733b425 fix(mattermost): let the DM page load once the team join has landed
The biber DM spec opened /main/messages/@administrator as its first navigation after SSO, so a single page load both joined the open team and opened the DM. The Agents plugin fetches team-scoped data during that load and errors with "Unable to find an existing account matching your username for this team", after which the typed message stays in the composer and Enter sends nothing.

The spec was flaky rather than broken: the same job recorded it both green and red, and the DM channel still held a message from an earlier successful attempt, so the URL does join the team. Reload it once, so the plugin initialises against a membership that already exists.

Evidence from the run artifacts: the trace records two pageError entries from the mattermost-ai bundle during that navigation, and the failure snapshot shows the message still sitting in the textbox. The causal step from the plugin error to the lost keypress is inferred, not proven, so a single green run will not confirm this fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 23:50:20 +02:00
kevinveenbirkenbachandClaude Opus 5 7d8e7e87ff test(mcp): prove a switched-off surface is actually gone
Every role-local MCP spec gates itself on the service flag and skips when the surface is off, so nothing ever checked that switching it off removes it. A surface still serving after the flag went false was indistinguishable from one that was never deployed: both read as a green skip. The gating helper had no inverse at all, so there was no way to assert what a disabled service must not do.

A shared assertion now probes the endpoint with both a GET and a POST while the flag is false and fails on any answer that shows something listening. It counts 401 and 403 as listening, which is the point: a refused credential still means the surface is there. The check is transport-agnostic because the roles differ in their enabled-state probes, over SSE, Streamable HTTP or plain JSON-RPC, while an endpoint that is gone answers none of them. Each role passes its endpoint URL as a thunk, since some resolve it only once a test is running.

The coverage lint requires the registration, and derives the role set from meta/mcp.yml rather than from a filename convention, which is how the tenth role surfaced: its spec is not named like the other nine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 00:40:15 +02:00
kevinveenbirkenbachandClaude Opus 5 45a138d1a6 fix(mattermost): stop the session cookie defeating header auth in the specs
Mattermost classifies a request by the session cookie it carries, not by the
Authorization header, and enforces CSRF on non-GET requests for a
cookie-authenticated session.

test-litellm-agent.js took its token out of the MMAUTHTOKEN cookie on the
OIDC path, so every later context.request sent cookie and bearer together.
GET /users/me and the bot lookup returned 200 while POST /channels/direct
came back 401, and the spec died before it could prompt anything. Clear the
cookies once the token is in hand so only header auth remains.

mattermostLogout only navigated to /logout with waitUntil "commit" and
returned immediately, so the session survived and the spec still landed on
/main/channels/town-square afterwards. Perform a real server-side logout
first, passing the X-CSRF-Token from the MMCSRF cookie, then wait for the
session cookie to disappear. The wait removes the race only; the assertion
stays in the spec.

test-sso-login.js was already failing before any of this, so this repairs an
existing weakness rather than one introduced here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 12:11:50 +02:00
kevinveenbirkenbachandClaude Opus 5 7162b7bf4c Merge fork/feature/svc-net-tor into feature/ai-agent-employees
834 commits from the Tor onion-service branch against 299 of ours, from merge
base 7b646bdece: 127 conflicted files, 3506 files changed.

The two sides are largely orthogonal. Theirs adds svc-net-tor and a `tor` service
across the role tree, plus two sweeping refactors: shipped scripts sorted by
language (files/ruby/, files/python/, files/shell/, scripts/lint/*.sh) and the
unit suite moved under tests/unit/python/. Ours is requirement 036, the native AI
gateway, plus the MCP role work. Most conflicts were both sides appending to the
same service map, resolved as unions.

Resolutions that were not unions:

Credentials moved namespace. Theirs renamed meta/schema.yml to meta/secrets.yml
and re-addressed credentials as secrets.credentials.<name>. Git carried our
litellm_api_key blocks through the rename, but every consumer we added still read
'credentials.<name>', which no longer resolves. All 45 call sites are migrated.

plugins/filter/container_volumes.py is deleted by theirs and relocated to
plugins/lookup/container_volumes.py, where it carries the same tmpfs defect our
f85996b43 had just fixed: a hard-coded {"size": ...} that silently drops a per
mount `mode`. The fix is re-applied in the relocated file, as that commit's merge
note anticipated.

compose.yml keeps only INFINITO_PLAYWRIGHT_REPORTS_BASE_DIR. Theirs also forwards
INFINITO_PLAYWRIGHT_STAGE_BASE_DIR; that line is deliberately dropped, per the
analysis recorded in 45804e9ad -- STAGE has a drift-proof role-vars fallback,
while forwarding it promotes a setdefault-pinnable .env value above that SPOT.

tasks/stages/01_constructor.yml takes their include, ./tasks/utils/setup/group.yml,
because their refactor removed tasks/groups/*-roles.yml, leaving our include
target nonexistent. Our task name is kept, since svc-virt is in the loop.

Makefile swarm-zombie is a union: their validate plus teardown, then our
lab_subnet cleanup, which their side dropped while leaving lab_subnet.sh as the
only orphaned reference in the file.

01_bootstrap.sh keeps our deterministic --ip pinning, which distinguishes an
already-attached node from a real failure, over their `|| true`.

Their language-sorted layout orphaned references our side still held: zammad's
two scripts/apply/*.rb, jenkins' java-with-ca-trust.sh, and the notify topics
their handlers renamed to hyphenated form. All corrected.

roles/sys-svc-compose/files/shell/swarm/stack_ready.sh gains our latched-update
block: paused, rollback_paused, rollback_started and rollback_completed exit 2
rather than polling a state that cannot resolve without another deploy.

config_from_run is unioned to (title, logged, jobs); the production caller in
trigger/__main__.py now passes jobs, without which our suite suppression would
have been inert.

READMEs are regenerated rather than hand-merged: 15 LiteLLM rows and 91 Tor rows
now derive from the merged metadata, which is itself evidence the meta merge is
coherent.

Verified: 5161 unit tests pass. The single failure, test_query.py TestSortSpec,
also fails on fork/feature/svc-net-tor at b51d2f9919, measured in a baseline
worktree -- so this merge introduces no new unit-test failure. Two modules are
excluded from that run because `baudolo`, a dependency their branch introduces,
is not installed on this host.
Not verified: no deploy has run against the merged tree, and make quality has not
been run end to end here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:32:30 +02:00
kevinveenbirkenbachandClaude Opus 5 19407bad1f feat(mattermost): make the Agents gateway binding converge and prove itself
The previous contract could not fail. Its guard and its closing assertion both
keyed on apiURL -- the one field the merge itself writes -- so once any run had
set the URL, every later run skipped the merge on a false_condition and still
reported all assertions passed. The live row shows what that hides: an apiKey of
`sk-` followed by 48 literal `a` characters, which the gateway answers 401 to,
and defaultModel `llama3:latest` while the round's gateway serves only
`smollm2:135m`. The merge task is `skipping` in every deploy log on this host.

The read now compares the whole desired service and bot with jsonb equality and
answers `converged` or `stale`, so a drifted key or model is drift the guard can
see. jsonb equality is key-order independent, which keeps the check from
depending on how Jinja happened to serialise the dict. Both SQL templates render
from the same two role vars, so read and write cannot disagree about what
correct means. The `jsonb_typeof(...) = 'array'` guards stay: before the first
write those keys exist as JSON null, coalesce does not fire on JSON null, and
jsonb_array_elements then raises "cannot extract elements from a scalar".

A row edit cannot create the bot account, which is why the Playwright spec 404s
on user `agent`. In mattermost-plugin-agents v2.5.1 the config lives in an
in-memory atomic pointer; bots.EnsureBots() is the only path that calls
pluginAPI.Bot.Create, and it runs from OnActivate and from the config update
listener. Writing straight into agents_confighistory reaches neither. The role's
only reload lever was `notify: container recreate`, which never fired -- the
container is unchanged with RestartCount 0 while the config row was written
inside it. The plugin is now cycled explicitly through mmctl, and the enable step
moves into tasks/utils/agents/enable.yml so the MCP path and the AI path share
one definition.

The closing gate is deliberately not another read of what we wrote: it polls the
bots-to-users join until the agent account exists. That table is written by the
plugin and never by this role, so it cannot pass by echoing the deploy's own
work. The reload fires on stale config OR a missing bot, which covers the state
this stack is actually in, where the config matched but no account existed.

Verified: the stale row, the absent bot account and the always-skipping merge are
all measured on the live stack; the EnsureBots path was read from upstream v2.5.1
source rather than inferred; every changed task file parses.
Not verified: none of this has executed yet. Specifically open are whether
`MATTERMOST_LITELLM_SERVICE | to_json` renders nested Jinja inside a dict var,
whether the converged/stale SQL is valid against live Postgres, and whether a
disable/enable cycle triggers EnsureBots on this version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 12:56:57 +02:00
kevinveenbirkenbachandClaude Opus 5 575a5a87ac refactor(layout): sort shipped scripts by language and split the unit suite
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).

This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.

Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:

  * 7 `src:` values built from a role variable holding a bare filename
    (svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
    The variable doubles as the destination name inside the container, so the
    language segment belongs at the call site, not in the variable.
  * `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
    through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
    18 roles ship exactly that and have no template fallback: the play would
    have died on each.
  * `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
    interface now reads `files/javascript/javascript.js`.
  * `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
    path through `path_absolute(... 'files', ...)`.
  * 4 `ansible.builtin.script:` invocations naming a bare command
    (pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
  * `dev-python/files/shell/install.sh` pointed at a sibling role's script.

Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:31:09 +02:00
kevinveenbirkenbachandClaude Opus 5 e0eda30fbe style(e2e): drop four more timeout arguments isVisible discards
Same class as 642055a20, missed by the filter that selected it: files were grouped by whether they CONTAIN a waitFor state call rather than by whether the diff ADDS one, so four whose change is a pure deletion were sorted into the waiting-semantics bundle instead. Their diffs alter nothing but the discarded argument, and every added line here is a bare isVisible().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:59:15 +02:00
kevinveenbirkenbachandClaude Opus 5 642055a20a style(e2e): drop the timeout argument isVisible discards
Playwright declares the timeout option on locator.isVisible() and isHidden() as deprecated and ignored: both return immediately and never wait. Passing resolveTimeout(10_000) to one therefore changes nothing at runtime and only tells the reader a wait happened that never did. That reading is what let 7538b8a0b conclude two genuine call sites were fine, and what hid the false negative b97cf8888 had to fix.

Sixty call sites across fifty-eight role-local specs lose the argument and nothing else. Every added line in this commit is a bare isVisible(); a grep for added lines that are not one returns zero, so a behaviour change cannot have entered here. Files whose diff also alters waiting semantics are deliberately not part of this commit.

A repository lint added in b97cf8888 keeps the construct out. It matches isVisible and isHidden only, because isChecked, isDisabled, isEditable and isEnabled take a real timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:49:18 +02:00
kevinveenbirkenbachandClaude Opus 5 9a3bd21ea3 fix(web-app-mattermost): decode the quoted MCP endpoint path
The Playwright env file renders every value through dotenv_quote, so the
raw read carried the literal quotes into the URL and the parser took the
host up to the first slash. The guest spec asked mattermost.infinito.example"
and failed DNS on every attempt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:47:32 +02:00
kevinveenbirkenbach 8358e423a4 feat(mattermost): proxy MCP through adapter 2026-08-12 02:09:11 +02:00
kevinveenbirkenbachandClaude Opus 5 3f900c7600 feat(mattermost): enable the Agents MCP server through the database
Mattermost keeps the plugin's MCP server switch in the database rather
than in a config file, so the role reads the plugin state, flips the
server on, and reads back the personal access token through SQL, with
reading and writing statements kept in separate directories.

Take over the token the server issued instead of generating one: the
value only exists once Mattermost has minted it, so it is persisted
through sys-token-store rather than declared as a vault credential.

Calls execute as the account the token belongs to, so the surface is
bounded by that account's permissions rather than by an administrator
role.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 22:19:29 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6f9142027d fix(e2e): skip seaweedfs filer check on onion targets
When an app rides onion, Playwright routes through the node Tor SOCKS
proxy, which cannot resolve the clearnet-internal SeaweedFS filer UI
domain (net::ERR_SOCKS_CONNECTION_FAILED). Skip the filer storage check
on onion targets in all consumer specs — same pattern already proven in
the nextcloud spec. Clearnet runs are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:34:59 +02:00
kevinveenbirkenbachandClaude Opus 4.8 07c6ad8e7c test(e2e): scale every nav click + timeout literal for onion targets
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.

Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).

Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:35:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4c91df5de7 test(e2e): forbid raw page.goto, migrate all specs to Tor-resilient gotoOnion
New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:59:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 35dee215c8 test(playwright): route all timeouts through resolveTimeout for onion-compat
Add the global TIMEOUT_FACTOR and a shared timeouts.js helper: resolveTimeout(base) scales by TIMEOUT_FACTOR and, over a .onion target, an extra multiplier, so Tor's per-request latency no longer trips hard-coded timeouts. Migrate every raw timeout in the specs (timeout:/timeout=/setTimeout/waitForTimeout) to resolveTimeout, stage the helper in all three staging paths, and add test_no_raw_timeouts to forbid raw numeric timeouts. Clearnet behaviour is unchanged (factor 1: resolveTimeout(N) === N).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:38:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 20bdd12219 test(web-app): seaweedfs object-store consumer e2e tests for 7 apps + gitea/opentalk exemptions
Add files/playwright/test-seaweedfs.js (+ SEAWEEDFS_* playwright.env vars, spec require wiring, and removal of the playwright-service-flag nocheck above the seaweedfs service) for baserow, bookwyrm, mattermost, openwebui, peertube, penpot, pixelfed. Each test reuses the role's real login flow and triggers an upload that writes to the app's S3 bucket (file-field, avatar, chat attachment, composer photo, video), verified via the shared runSeaweedfsStorageCheck (bucket grew through the Filer UI).

openwebui: also export dismissAllOpenModals from _shared.js (used by the new test) and match the uploaded file by basename (not the full filename) to avoid the extension-split false-negative.

Exempt gitea and opentalk via meta/services.yml nocheck markers (seaweedfs-playwright + playwright-service-flag): gitea has no automatable UI login in this stack (oauth2-proxy + OIDC account-link / native login both fail; personas blocked); opentalk only writes S3 for recordings/whiteboard exports, which need the recorder service + a live multi-party meeting.

Builds on 9b6c6415f (shared runSeaweedfsStorageCheck helper + consumer lint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 20:17:35 +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 f46f64899a fix(mattermost): collapse SSO start loop into single-shot navigation
`startMattermostSsoFlow` polled `/login` in a `while (true)` loop with
`waitUntil: "domcontentloaded"` and a 10s `isVisible` window for
`#input_loginId`. Under CI load Mattermost v11+'s React bundle takes
15-30s to parse and mount the login form. Each loop iteration's
`page.goto` aborted the previous in-flight hydration, so the SPA
restarted from scratch on every retry and the form mounted only when
the iteration's 10s window happened to align with a hydration that
slipped through.

Run 26451797037 job 77878640850 (web-app-mattermost ubuntu) timed out
at 300s in this state. The Playwright trace shows biber's loop ran 80+
iterations over 156s before `#input_loginId` finally became visible;
admin's loop then had only 132s of budget left, never broke out, and
the test hit the outer `testTimeout: 300_000`. Both contexts'
`error-context.md` page snapshots confirmed the form was present at
the moment of timeout, ruling out an actual Mattermost-side failure.

Replace the loop with a single navigate sequence:
  * `waitUntil: "load"` waits for the full `load` event, giving the
    React bundle time to parse and run instead of returning at
    `domcontentloaded` before any of it has executed.
  * One explicit `page.locator(...).waitFor({ visible, timeout: 60_000 })`
    spans the observed 15-30s hydration tail with comfortable headroom.
  * The wrapping `for (attempt = 0; attempt < 2; ...)` adds exactly one
    retry on transient navigation failures (network blip, mid-deploy
    container restart) without reintroducing the abort-self race the
    bare loop had — only one in-flight hydration at a time.

The PASS 1 (sync) path that already succeeded in the failing run keeps
working because the new control flow is strictly simpler than the
loop. The failing PASS 2 (async) path now has a deterministic upper
bound: at most two 60s navigations + two 60s `waitFor`s per call site
= 240s ceiling per `startMattermostSsoFlow`, comfortably below the
test-level 300s timeout even with biber + admin both invoking it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:24:18 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9fddd8fc97 fix(web-app-mattermost/playwright): pre-stamp landing-page-seen flag
Mattermost v11.7.1's `Root.tsx` redirects every fresh browser context
that lacks `localStorage.__landingPageSeen__` to `/landing#/` (the
"open in app vs view in browser" splash) — verified against upstream
source webapp/channels/src/components/root/root.tsx:146 and the key
name in webapp/channels/src/utils/constants.tsx:808 at tag v11.7.1.
The redirect is purely client-side (React Router `history.push`), so
localStorage stamped via Playwright's `addInitScript` before the very
first navigation suppresses it.

The bounce broke job 77797115305 on run 26428083128 (fedora distro):
`test-biber-dm-administrator` opens two fresh `browser.newContext()`s
that go through `startMattermostSsoFlow`. The first SSO test passes
(default `page` fixture inherits no relevant state either, but the
admin user's first provisioning seemingly papers over the missing
flag for that one context). The DM test's second context bounces to
`/landing#/` indefinitely, the existing 120s deadline + outer 300s
test budget expire across all three retries, and the role-runner
reports `Playwright failed for roles: ['web-app-mattermost']`.

Adds a single `page.addInitScript` in `startMattermostSsoFlow` that
writes `localStorage.__landingPageSeen__ = "true"` on every document
load. The existing bounce-loop stays as a safety net for the case
where Mattermost ever moves the flag — the fix is purely additive.

Verification:
- Flag name `__landingPageSeen__`: `StoragePrefixes.LANDING_PAGE_SEEN`
  in upstream webapp/channels/src/utils/constants.tsx:808.
- Redirect gate: `BrowserStore.hasSeenLandingPage()` short-circuits
  the redirect at root.tsx:146 — `localStorage.getItem(...)` on the
  same key.
- Playwright contract: `addInitScript` evaluates on every page load
  before any page script (per Playwright docs).
- Confidence: ~95%. The remaining 5% covers (a) `appDownloadLink`
  unset (redirect never fires — irrelevant) and (b) Playwright
  ordering edge cases (standard pattern, low risk).

The clean architectural follow-up (Playwright `storageState` reuse
to skip the second SSO round-trip entirely) lives elsewhere; this
commit is the focused unblocker.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 13:25:14 +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 a7c520d97f refactor(web-app-mattermost): split playwright.spec.js into per-test files
The single spec mixed an SSO login test, a biber-DM-administrator test, and the three persona shims; each rebuilt its own env-var bindings and helpers. Splits the surface into thin per-test modules around a `_shared.js` that owns the env asserts, the popup/channel-view/logout helpers, and the SSO bootstrap.

`startMattermostSsoFlow` now polls `/login` for up to 120 s instead of waiting 30 s once, so the v1 bulk reinstall no longer flakes when the Mattermost SPA takes longer than 30 s to mount `#input_loginId` under concurrent deploy load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:24:31 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1f9559a335 fix(playwright): gate bespoke SSO/LDAP tests on service flags (odoo, fider, mattermost, peertube, kix)
Bespoke `test(...)` blocks that drive a real Keycloak SSO round-trip
(odoo, fider, mattermost, peertube) had no `test.skip(\!oidcEnabled, ...)`
guard. In matrix variants that render `OIDC_SERVICE_ENABLED=false`, the
Playwright locator for the SSO button never resolves; each test waits
300s, retries 2x, and 2-3 such tests in series blow past the 1200s
Ansible task-wrapper timeout — Bundle α odoo variant 1 reproduced this.

Pattern parity with `roles/web-app-moodle/files/playwright/playwright.spec.js`:
import `service-gating`, derive `<flag>Enabled` once, prefix each
affected test with `test.skip(\!<flag>Enabled, "...")`.

- odoo (verified empirically against running variant-1 stack via
  `make run`: 4 skipped, 1 passed, 2.2s — was 1200s timeout)
- fider, mattermost, peertube: same pattern, 2 bespoke tests each
- kix: gates on `oauth2Enabled` + `ldapEnabled` instead — the role's
  variant matrix has no `oidc` flag, the flow is
  KIX → OAuth2-proxy → Keycloak → KIX-LDAP, both can be independently
  disabled per variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:30:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2fdf02cfcf refactor(playwright): consolidate prometheus checks into one SPOT
- Matcher in web-app-prometheus's parametrized scrape test now also
  reads `labels.app`. Native scrape jobs (set `app="web-app-<id>"` via
  each role's `templates/prometheus.yml.j2` fragment) now match alongside
  blackbox-healthz targets. A single iteration asserts up=1 for both
  surfaces per consumer, so per-app native tests become redundant.
- Scenario I (`/metricz`) iterates `prometheusTargetRoles` and asserts
  each consumer's `app="<role_id>"` label is present in the
  lua-resty-prometheus output. Subsumes the per-app metricz-label spot
  checks.
- Remove four redundant per-app prometheus tests from web-app-gitea and
  web-app-mattermost playwright specs (metricz-label + native-scrape
  up=1), drop the orphaned `PROMETHEUS_BASE_URL=` declarations from
  their env.j2 files, and clean up `OIDC_ISSUER_URL=` in gitea's env.j2
  (only used by the removed scenarios).
- Drop the dead `const prometheusBaseUrl` declarations plus their
  eslint-disable shut-up comments from web-app-fider, web-app-odoo, and
  web-app-taiga playwright specs, along with the matching
  `PROMETHEUS_BASE_URL=` blocks in those roles' env.j2.
- Correct the persona-required-envs lint: only `MATOMO_BASE_URL` is
  actually consumed (by `personas/utils/csp.js`). `DASHBOARD_BASE_URL`
  and `PROMETHEUS_BASE_URL` were aspirationally required by a lint
  whose docstring described persona-collapse behaviour that no persona
  implements. Update `_REQUIRED_URLS` and the docstring, and drop the
  unused `\!dashboardBaseUrl` token from `_ALLOWED_SKIP_GUARDS` in
  test_strict_mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:02:22 +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