SPRINT_20260422_001 (Notify compat surface restoration) — closes NOTIFY-COMPAT-003 final criterion. notify-web rebuilt, 7/7 gateway probes green post-rebuild (8th returns contracted 501 notify_overrides_not_supported). Browser replay via direct Node+Playwright driver (MCP bridge rejected the self-signed cert) confirmed Dashboard/Channels/Quiet Hours/Overrides/ Escalation/Throttle tabs render without runtime-unavailable banners. All 3 tasks DONE. SPRINT_20260421_003 (Concelier advisory connector runtime alignment) — all 7 tasks were already DONE before this session; archival is purely administrative. Evidence bundle at docs/qa/notify-compat-20260422/ includes EVIDENCE.md, verify.mjs + verify_with_token.mjs, verify-results.json, and screenshots for each verified tab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
5.1 KiB
JavaScript
142 lines
5.1 KiB
JavaScript
// Browser verification using a pre-obtained bearer token, bypassing the
|
|
// interactive OIDC flow (needed because a parallel Authority agent
|
|
// wiped the admin user between runs).
|
|
import { chromium } from 'playwright';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const OUT_DIR = path.resolve('docs/qa/notify-compat-20260422');
|
|
const BASE = 'https://stella-ops.local';
|
|
// Reused from the successful /connect/token request captured earlier this
|
|
// session (exp 1776860238 ~= 2026-04-22T12:17:18Z).
|
|
const TOKEN = process.env.BEARER_TOKEN;
|
|
|
|
if (!TOKEN) {
|
|
console.error('missing BEARER_TOKEN env');
|
|
process.exit(1);
|
|
}
|
|
|
|
const TABS = [
|
|
{ id: 'dashboard', path: '/setup/notifications' },
|
|
{ id: 'channels', path: '/setup/notifications/channels' },
|
|
{ id: 'quiet-hours', path: '/setup/notifications/config/quiet-hours' },
|
|
{ id: 'overrides', path: '/setup/notifications/config/overrides' },
|
|
{ id: 'escalation', path: '/setup/notifications/config/escalation' },
|
|
{ id: 'throttle', path: '/setup/notifications/config/throttle' },
|
|
];
|
|
|
|
function log(m) { console.log(`[verify] ${m}`); }
|
|
|
|
async function main() {
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({
|
|
ignoreHTTPSErrors: true,
|
|
viewport: { width: 1400, height: 900 },
|
|
});
|
|
const page = await context.newPage();
|
|
|
|
const consoleErrors = [];
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
|
});
|
|
|
|
// Load the SPA first (any page) so we have the origin, then inject the
|
|
// token into whichever storage the console's auth layer uses.
|
|
await page.goto(BASE + '/welcome', { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
await page.waitForTimeout(1500);
|
|
|
|
// Inspect storage to find the expected token key used by the Angular app.
|
|
const initialKeys = await page.evaluate(() => ({
|
|
localStorage: Object.keys(localStorage),
|
|
sessionStorage: Object.keys(sessionStorage),
|
|
}));
|
|
log(`initial storage keys: ${JSON.stringify(initialKeys)}`);
|
|
|
|
// Try common OAuth/OIDC storage shapes so the app treats us as authenticated.
|
|
await page.evaluate((tok) => {
|
|
const payload = {
|
|
access_token: tok,
|
|
token_type: 'Bearer',
|
|
expires_at: Math.floor(Date.now() / 1000) + 1500,
|
|
scope: 'ui.admin',
|
|
};
|
|
const payloadStr = JSON.stringify(payload);
|
|
// angular-oauth2-oidc / generic keys
|
|
localStorage.setItem('access_token', tok);
|
|
localStorage.setItem('id_token', tok);
|
|
localStorage.setItem('expires_at', String(Date.now() + 1500 * 1000));
|
|
localStorage.setItem('nonce', 'dummy');
|
|
localStorage.setItem('stellaops.auth.session', payloadStr);
|
|
localStorage.setItem('stellaops.auth.token', tok);
|
|
sessionStorage.setItem('stellaops.auth.session', payloadStr);
|
|
}, TOKEN);
|
|
|
|
// Also attach Bearer to every xhr/fetch the page makes so backend calls
|
|
// are authenticated even if the app does not pick up localStorage.
|
|
await page.route('**/*', async (route, request) => {
|
|
const url = request.url();
|
|
if (url.startsWith(BASE + '/api/') || url.includes('/api/v1/notifier') || url.includes('/api/v2/notify')) {
|
|
const headers = { ...request.headers(), authorization: 'Bearer ' + TOKEN };
|
|
return route.continue({ headers });
|
|
}
|
|
return route.continue();
|
|
});
|
|
|
|
const results = [];
|
|
for (const tab of TABS) {
|
|
const url = BASE + tab.path;
|
|
log(`--- tab ${tab.id}: ${url}`);
|
|
try {
|
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
} catch (e) {
|
|
log(`nav error: ${e.message}`);
|
|
}
|
|
await page.waitForTimeout(3000);
|
|
|
|
const shot = path.join(OUT_DIR, `tabx-${tab.id}.png`);
|
|
await page.screenshot({ path: shot, fullPage: true });
|
|
|
|
const finalUrl = page.url();
|
|
const bodyText = await page.evaluate(() => document.body.innerText || '');
|
|
const hasUnavailable = /runtime.?unavailable|runtime unavailable/i.test(bodyText);
|
|
const hasEndpointNotFound = /Endpoint not found/i.test(bodyText);
|
|
const has404FE = /404\s+Page Not Found/i.test(bodyText);
|
|
const landedOnSetup = /\/setup\/notifications/.test(finalUrl);
|
|
const landedOnWelcome = /\/welcome/.test(finalUrl);
|
|
const pass = !hasUnavailable && !hasEndpointNotFound && !has404FE && landedOnSetup;
|
|
|
|
results.push({
|
|
tab: tab.id,
|
|
url,
|
|
finalUrl,
|
|
screenshot: shot,
|
|
landedOnSetup,
|
|
landedOnWelcome,
|
|
hasUnavailable,
|
|
hasEndpointNotFound,
|
|
has404FE,
|
|
pass,
|
|
snippet: bodyText.slice(0, 600).replace(/\s+/g, ' '),
|
|
});
|
|
log(`tab ${tab.id}: pass=${pass} landed=${landedOnSetup} welcome=${landedOnWelcome} unavailable=${hasUnavailable} 404fe=${has404FE}`);
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
fs.writeFileSync(path.join(OUT_DIR, 'verify-results-token.json'), JSON.stringify({
|
|
generatedAt: new Date().toISOString(),
|
|
results,
|
|
consoleErrors: consoleErrors.slice(0, 50),
|
|
}, null, 2));
|
|
const failed = results.filter(r => !r.pass);
|
|
if (failed.length) {
|
|
log(`FAILURES: ${failed.map(f => f.tab).join(', ')}`);
|
|
process.exit(2);
|
|
}
|
|
log('ALL TABS PASS');
|
|
}
|
|
|
|
main().catch(err => { console.error(err); process.exit(1); });
|