Both shared flows gate performKeycloakLogin on 'openid-connect/auth' (personas/biber.js:85, personas/admin.js:64). SuiteCRM's SSO is SAML - d15716643 moved it off the oauth2-proxy onto nbgrp/onelogin-saml-bundle - so its auth URL is /realms/<realm>/login-actions/authenticate for client <base>/saml/metadata. The gate never opens, performKeycloakLogin never runs, and the personas fail at 'did NOT reach an authenticated surface'.
The trace of job 95883075126 carries no fill() at all: goto, clearCookies x2, waitForSelector x5, click x2, waitForFunction x2, fetch. Nothing was ever typed. Keycloak's 24 LOGIN_ERROR entries are the consequence - requests to login-actions/authenticate without credentials, which is also why they carry no username attribute.
The block alone would drop coverage, so saml.spec.js takes it over where the session already exists: assertCspInjections on the authenticated surface, inAppLogout, session-status must report active:false, then assertUnauthenticatedLanding. That is what runBiberFlow asserted, minus runRoleInteraction, which SuiteCRM never passed. The SAML round trip itself stays green in the two existing tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role's only login coverage was the shared biber flow, and that flow can
only drive OIDC: personas/biber.js authenticates solely when the URL contains
`openid-connect/auth`. SAML redirects to /protocol/saml and the ldap variant
uses SuiteCRM's own form, so neither path could be exercised — across every
persona helper there are 15 hits for `openid-connect` and none for `saml`.
Both specs assert through /session-status rather than by hunting a logout
control in the DOM. That is what the whole SSO change is about — whether
SuiteCRM owns a session or only the edge does — and it does not race an
asynchronously rendering Angular surface, which is exactly where the 2026-08-08
run tore.
Three timing decisions are load-bearing, each from a defect found while
reviewing the first draft:
- The ACS response is awaited explicitly instead of polling the URL.
performKeycloakLogin only checks that page.url() CONTAINS the canonical
domain, and php-saml puts the SP's own URL in RelayState, so Keycloak's login
URL already contains it: a URL wait returns before the assertion is posted.
- The `POST /login` response is awaited explicitly in the ldap spec. SuiteCRM 8
logs in by XHR with no document navigation, so waitForLoadState resolves while
the request is still in flight.
- /session-status is requested exactly once, after the login completed.
SecurityController's unauthenticated branch calls $session->invalidate(),
so a probe fired mid-login destroys the session it reports on. The first draft
did all three wrongly and would have gone red against a WORKING deployment.
The ldap spec clicks #login-button because the login form declares no
(ngSubmit); Enter only worked through HTML implicit submission.
Verified in the production Playwright image against a protocol stub that
replays the choreography (fake IdP with a Keycloak-shaped form, auto-posted
assertion, XHR login): saml.spec.js 2 passed, ldap.spec.js 2 passed. The
counter-runs matter more — against an app that does not redirect to Keycloak the
SAML spec fails with its own message, and against an app with no login form the
ldap spec times out on #login-button, so neither is vacuously green. Also
verified: collection of all 9 tests, the three service-gate combinations, and a
missing credential failing loudly instead of skipping.
playwright.env.j2's persona rationale said biber has no account when SSO is off.
He does — LDAP_AUTO_CREATE mints one on first bind. He is blocked because the
shared flow cannot drive that login, which is what ldap.spec.js now covers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>