Files
git.stella-ops.org/docs/qa/notify-compat-20260422/verify_with_token.mjs
master 99a5ae923a chore(qa): notify compat rebuild evidence + archive completed sprints
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>
2026-04-22 16:05:11 +03:00

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); });