Harden live frontdoor authentication harness
This commit is contained in:
@@ -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-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 | 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 | 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
|
## 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.
|
- 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.
|
- 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.
|
- 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: 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
|
## Next Checkpoints
|
||||||
- 2026-03-09: land the reusable live canonical route sweep script.
|
- 2026-03-09: land the reusable live canonical route sweep script.
|
||||||
|
|||||||
@@ -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 = {}) {
|
export async function authenticateFrontdoor(options = {}) {
|
||||||
const baseUrl = options.baseUrl?.trim() || DEFAULT_BASE_URL;
|
const baseUrl = options.baseUrl?.trim() || DEFAULT_BASE_URL;
|
||||||
const username = options.username?.trim() || DEFAULT_USERNAME;
|
const username = options.username?.trim() || DEFAULT_USERNAME;
|
||||||
@@ -134,9 +148,6 @@ export async function authenticateFrontdoor(options = {}) {
|
|||||||
'button.cta',
|
'button.cta',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await clickIfVisible(signInTrigger);
|
|
||||||
await page.waitForTimeout(1_500);
|
|
||||||
|
|
||||||
const usernameField = createLocator(page, [
|
const usernameField = createLocator(page, [
|
||||||
'input[name="username"]',
|
'input[name="username"]',
|
||||||
'input[name="Username"]',
|
'input[name="Username"]',
|
||||||
@@ -149,6 +160,13 @@ export async function authenticateFrontdoor(options = {}) {
|
|||||||
'input[type="password"]',
|
'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;
|
const hasLoginForm = (await usernameField.count()) > 0 && (await passwordField.count()) > 0;
|
||||||
if (page.url().includes('/connect/authorize') || hasLoginForm) {
|
if (page.url().includes('/connect/authorize') || hasLoginForm) {
|
||||||
const filledUser = await fillIfVisible(usernameField, username);
|
const filledUser = await fillIfVisible(usernameField, username);
|
||||||
@@ -168,15 +186,31 @@ export async function authenticateFrontdoor(options = {}) {
|
|||||||
|
|
||||||
await submitButton.click({ timeout: 10_000 });
|
await submitButton.click({ timeout: 10_000 });
|
||||||
|
|
||||||
await page.waitForURL(
|
await Promise.race([
|
||||||
(url) => !url.toString().includes('/connect/authorize') && !url.toString().includes('/auth/callback'),
|
page.waitForURL(
|
||||||
{ timeout: 30_000 },
|
(url) => !url.toString().includes('/connect/authorize') && !url.toString().includes('/auth/callback'),
|
||||||
).catch(() => {});
|
{ timeout: 30_000 },
|
||||||
|
).catch(() => {}),
|
||||||
|
page.waitForFunction(() => Boolean(sessionStorage.getItem('stellaops.auth.session.full')), null, {
|
||||||
|
timeout: 30_000,
|
||||||
|
}).catch(() => {}),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitForShell(page);
|
await waitForShell(page);
|
||||||
await page.waitForTimeout(2_500);
|
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 });
|
await context.storageState({ path: statePath });
|
||||||
|
|
||||||
const report = {
|
const report = {
|
||||||
|
|||||||
Reference in New Issue
Block a user