diff --git a/docs/implplan/SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md b/docs/implplan/SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md index 5a4d84d73..4254ed988 100644 --- a/docs/implplan/SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md +++ b/docs/implplan/SPRINT_20260309_002_FE_live_frontdoor_canonical_route_sweep.md @@ -70,6 +70,7 @@ Completion criteria: | 2026-03-09 | After the full image rebuild and the next web-only repair pass, reran the authenticated 111-route sweep. The live backlog moved to 24 failing routes, with the earlier title regressions and feeds-airgap issue cleared while new backend/runtime failures remained concentrated in analytics, JobEngine, integrations, policy governance, notifications, and trust authorization. | Developer | | 2026-03-10 | Full rebuild and redeploy completed cleanly, but the deeper live `ops/policy` action sweep stalled after authentication without writing a result file. This iteration is hardening the sweep itself with per-action watchdogs, progress persistence, and explicit failure semantics so the next scratch loops do not burn hours on a silent Playwright hang. | Developer | | 2026-03-10 | Completed the hardening pass on `live-ops-policy-action-sweep.mjs`: the script now persists progress while it runs, reports blocked actions with step-level snapshots, and exits non-zero on action/runtime failures. After the policy frontdoor fix, the same sweep completed cleanly on the rebuilt stack with zero runtime issues. | Developer | +| 2026-03-10 | Hardened `live-frontdoor-auth.mjs` so it waits for a real authority transition or established shell session before declaring authentication complete. This prevents false-positive sign-in clicks on rebuilt stacks where the login form appears asynchronously or the welcome page lingers after the CTA. | Developer | ## Decisions & Risks - Decision: keep this sprint focused on broad route-level live verification and action inventory, not on fixing specific route defects before the rebuilt stack is actually exercised. @@ -78,6 +79,7 @@ Completion criteria: - Decision: treat documented/canonical redirects as valid route outcomes in the live sweep (`/releases`, `/releases/promotion-queue`, `/ops/policy`, `/ops/policy/audit`, `/ops/platform-setup/trust-signing`, `/setup/topology`) because those aliases are intentional product behavior, not regressions. - Risk: many remaining failures are real frontdoor contract mismatches rather than simple UI copy/render issues, so the next iterations need backend/frontend contract inspection, not just surface-level error-banner suppression. - Decision: the deep live sweeps must be self-diagnosing. A hanging Playwright command is a harness defect because it blocks the problem-first loop from collecting the full issue set. +- Decision: authentication success in the live harness is defined by an established Stella Ops session or a completed authority redirect, not by a single successful CTA click on `/welcome`. ## Next Checkpoints - 2026-03-09: land the reusable live canonical route sweep script. diff --git a/src/Web/StellaOps.Web/scripts/live-frontdoor-auth.mjs b/src/Web/StellaOps.Web/scripts/live-frontdoor-auth.mjs index aef63862f..1439a23e8 100644 --- a/src/Web/StellaOps.Web/scripts/live-frontdoor-auth.mjs +++ b/src/Web/StellaOps.Web/scripts/live-frontdoor-auth.mjs @@ -59,6 +59,20 @@ async function waitForShell(page) { ]); } +async function waitForAuthTransition(page, usernameField, passwordField, timeoutMs = 10_000) { + await Promise.race([ + page.waitForURL((url) => url.toString().includes('/connect/authorize') || url.toString().includes('/auth/callback'), { + timeout: timeoutMs, + }).catch(() => {}), + usernameField.waitFor({ state: 'visible', timeout: timeoutMs }).catch(() => {}), + passwordField.waitFor({ state: 'visible', timeout: timeoutMs }).catch(() => {}), + page.waitForFunction(() => Boolean(sessionStorage.getItem('stellaops.auth.session.full')), null, { + timeout: timeoutMs, + }).catch(() => {}), + page.waitForTimeout(timeoutMs), + ]); +} + export async function authenticateFrontdoor(options = {}) { const baseUrl = options.baseUrl?.trim() || DEFAULT_BASE_URL; const username = options.username?.trim() || DEFAULT_USERNAME; @@ -134,9 +148,6 @@ export async function authenticateFrontdoor(options = {}) { 'button.cta', ]); - await clickIfVisible(signInTrigger); - await page.waitForTimeout(1_500); - const usernameField = createLocator(page, [ 'input[name="username"]', 'input[name="Username"]', @@ -149,6 +160,13 @@ export async function authenticateFrontdoor(options = {}) { 'input[type="password"]', ]); + const signInClicked = await clickIfVisible(signInTrigger); + if (signInClicked) { + await waitForAuthTransition(page, usernameField, passwordField); + } else { + await page.waitForTimeout(1_500); + } + const hasLoginForm = (await usernameField.count()) > 0 && (await passwordField.count()) > 0; if (page.url().includes('/connect/authorize') || hasLoginForm) { const filledUser = await fillIfVisible(usernameField, username); @@ -168,15 +186,31 @@ export async function authenticateFrontdoor(options = {}) { await submitButton.click({ timeout: 10_000 }); - await page.waitForURL( - (url) => !url.toString().includes('/connect/authorize') && !url.toString().includes('/auth/callback'), - { timeout: 30_000 }, - ).catch(() => {}); + await Promise.race([ + page.waitForURL( + (url) => !url.toString().includes('/connect/authorize') && !url.toString().includes('/auth/callback'), + { timeout: 30_000 }, + ).catch(() => {}), + page.waitForFunction(() => Boolean(sessionStorage.getItem('stellaops.auth.session.full')), null, { + timeout: 30_000, + }).catch(() => {}), + ]); } await waitForShell(page); await page.waitForTimeout(2_500); + const sessionStatus = await page.evaluate(() => ({ + hasFullSession: Boolean(sessionStorage.getItem('stellaops.auth.session.full')), + hasSessionInfo: Boolean(sessionStorage.getItem('stellaops.auth.session.info')), + })); + const signInStillVisible = await signInTrigger.isVisible().catch(() => false); + if (!sessionStatus.hasFullSession || (!page.url().includes('/connect/authorize') && signInStillVisible)) { + throw new Error( + `Frontdoor authentication did not establish a Stella Ops session. finalUrl=${page.url()} signInVisible=${signInStillVisible}`, + ); + } + await context.storageState({ path: statePath }); const report = {