SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
20.2 KB · 339 lines javascript
Raw Blame History
1/**2 * D3 QA sweep (temporal · graph · research · changes · search · explore · claims · admin · SEO).3 * Widths 320 · 360 · 375 · 390 · 430 · 768 · 1366 · 1440 · 1920, dark + light — HTTP status, zero console errors, no horizontal4 * overflow, screenshot — plus assertions: graph draws ≥ 2 nodes, compiled chips on /search, backfill honesty line on the digest,5 * admin login → /admin/quality → /admin/entity-resolution (token never in HTML), OG image routes, sitemap shards.6 * Run: node qa/d3.mjs [BASE_URL] [API_URL] [ADMIN_TOKEN]   (defaults http://localhost:8343, http://127.0.0.1:8332, AIA_ADMIN_TOKEN from ../../.env)7 * QUICK=1 limits widths to 390 and 1440.8 */9import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';10import { existsSync, mkdirSync, readFileSync } from 'node:fs';1112const BASE = process.argv[2] ?? 'http://localhost:8343';13const API = process.argv[3] ?? 'http://127.0.0.1:8332';14function envToken() {15  for (const p of [new URL('../../../.env', import.meta.url).pathname, new URL('../.env', import.meta.url).pathname]) {16    if (!existsSync(p)) continue;17    const m = /^AIA_ADMIN_TOKEN=(.+)$/m.exec(readFileSync(p, 'utf8'));18    if (m) return m[1].trim().replace(/^["']|["']$/g, '');19  }20  return null;21}22const TOKEN = process.argv[4] ?? process.env.AIA_ADMIN_TOKEN ?? envToken();23const OUT = new URL('./screens/d3/', import.meta.url).pathname;24mkdirSync(OUT, { recursive: true });25const WIDTHS = process.env.QUICK === '1' ? [390, 1440] : [320, 360, 375, 390, 430, 768, 1366, 1440, 1920];26const THEMES = ['dark', 'light'];27const today = new Date().toISOString().slice(0, 10);28const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);2930const j = async (path, headers = {}) => {31  const r = await fetch(`${API}/api/v1${path}`, { headers: { accept: 'application/json', ...headers } });32  if (!r.ok) throw new Error(`API ${r.status} ${path}`);33  return r.json();34};3536// ---- discover slugs live (nothing hardcoded)37const trending = await j('/trending?kind=views&type=model&limit=1&days=7').catch(() => null);38const models = await j('/models?limit=1&sort=quality');39const modelSlug = trending?.items?.[0]?.slug ?? models.items[0].slug;40const papers = await j('/papers?limit=60&sort=updated');41const paperSlug = (papers.items.find((p) => (p.counts?.relations ?? 0) > 2) ?? papers.items[0]).slug;42const orgs = await j('/companies?limit=1&sort=models');43const orgSlug = orgs.items.find((o) => o.slug === 'anthropic')?.slug ?? orgs.items[0].slug;44const claims = await j(`/entities/${encodeURIComponent(modelSlug)}/claims?limit=1`);45const claimId = claims.items[0]?.id;46const researchers = await j('/explore/researcher?limit=1');47const researcherSlug = researchers.items[0]?.slug;48let snapshotId = null;49if (TOKEN) {50  try {51    const docs = await j('/admin/documents?limit=1', { 'x-aia-admin-token': TOKEN });52    const doc = await j(`/admin/documents/${docs.items[0].id}`, { 'x-aia-admin-token': TOKEN });53    snapshotId = Array.isArray(doc.snapshots) && doc.snapshots[0] ? doc.snapshots[0].id : null;54  } catch {55    snapshotId = null;56  }57}58console.log(`root model ${modelSlug} · paper ${paperSlug} · org ${orgSlug} · claim ${claimId} · researcher ${researcherSlug} · snapshot ${snapshotId ?? '—'} · token ${TOKEN ? 'yes' : 'NO'}`);5960const PAGES = [61  { path: '/graph', check: 'graph' },62  { path: `/graph?node=${orgSlug}&mode=company`, check: 'graph' },63  { path: `/graph/${modelSlug}?mode=lineage`, check: 'graph' },64  { path: '/timeline', check: 'timeline' },65  { path: '/timeline?include_backfill=1&year=2025', check: 'timeline' },66  { path: '/time-machine?date=2025-06-01', check: 'tm' },67  { path: `/diff?a=${weekAgo}&b=${today}`, check: 'diff' },68  { path: '/changes' },69  { path: `/changes/${today}`, check: 'digest' },70  { path: '/search?q=open+reasoning+models+over+30B+released+in+2026', check: 'search' },71  { path: '/explore', check: 'builder' },72  { path: '/papers' },73  { path: `/papers/${paperSlug}` },74  { path: '/companies' },75  { path: `/companies/${orgSlug}` },76  ...(researcherSlug ? [{ path: `/explore/researcher/${researcherSlug}` }] : []),77  ...(claimId ? [{ path: `/claims/${claimId}` }] : []),78  { path: '/sources' },79  { path: '/methodology' },80  { path: '/developers', check: 'builder-dev' },81  { path: '/graph/does-not-exist-xyz', expected: 404 },82  { path: '/claims/claim_does_not_exist', expected: 404 },83];84const LIGHT_SUBSET = new Set(['/graph', `/graph/${modelSlug}?mode=lineage`, '/timeline', '/time-machine?date=2025-06-01', `/diff?a=${weekAgo}&b=${today}`, `/changes/${today}`, '/search?q=open+reasoning+models+over+30B+released+in+2026', '/explore', `/papers/${paperSlug}`, `/companies/${orgSlug}`, `/claims/${claimId}`, '/sources', '/methodology', '/developers']);8586const filterErrors = (errors, expected) => errors.filter((e) => !/favicon|Failed to load resource: the server responded with a status of 404|the server responded with a status of 429/.test(e)).filter((e) => !(expected === 404 && /Encountered a script tag while rendering React component/.test(e)));8788const browser = await chromium.launch();89let failures = 0;90const slug = (p) => p.replace(/^\//, '').replace(/[^a-z0-9]+/gi, '_').slice(0, 60) || 'home';9192async function checks(page, kind) {93  const problems = [];94  if (kind === 'graph') {95    await page.waitForSelector('[data-graph-canvas] [data-node]', { timeout: 20000 }).catch(() => undefined);96    const n = await page.locator('[data-graph-canvas] [data-node]').count();97    if (n < 2) problems.push(`graph nodes=${n}`);98    if (!(await page.locator('[data-graph-count]').count())) problems.push('graph count line missing');99  }100  if (kind === 'search') {101    const n = await page.locator('[data-compiled-chip]').count();102    if (n < 1) problems.push(`compiled chips=${n}`);103    if (!(await page.locator('[data-open-builder]').count())) problems.push('builder link missing');104  }105  if (kind === 'digest') {106    const text = await page.evaluate(() => document.body.innerText);107    if (!/occurred/i.test(text)) problems.push('digest lacks occurred caption');108  }109  if (kind === 'tm') {110    if (!(await page.locator('[data-tm-banner]').count())) problems.push('time-machine banner missing');111    const text = await page.evaluate(() => document.body.innerText);112    if (!/reconstructed/i.test(text)) problems.push('time-machine lacks reconstructed note');113  }114  if (kind === 'diff') {115    const text = await page.evaluate(() => document.body.innerText);116    if (!/new models/i.test(text) || !/price changes/i.test(text)) problems.push('diff sections missing');117  }118  if (kind === 'timeline') {119    if (!(await page.locator('[data-timeline-workbench]').count()) && !(await page.evaluate(() => /No events/i.test(document.body.innerText)))) problems.push('timeline workbench missing');120  }121  if (kind === 'builder' && !(await page.locator('[data-query-builder]').count())) problems.push('query builder missing');122  if (kind === 'builder-dev' && !(await page.locator('[data-request-builder]').count())) problems.push('request builder missing');123  return problems;124}125126for (const theme of THEMES) {127  for (const width of WIDTHS) {128    const mobile = width < 768;129    const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: theme });130    await ctx.addInitScript((t) => localStorage.setItem('aia-theme', t), theme);131    const page = await ctx.newPage();132    for (const { path, check, expected: exp } of PAGES) {133      if (theme === 'light' && !LIGHT_SUBSET.has(path)) continue;134      if (theme === 'light' && ![390, 1440, 320, 1920].includes(width)) continue;135      const errors = [];136      page.on('pageerror', (e) => errors.push(String(e)));137      page.on('console', (m) => {138        if (m.type() === 'error') errors.push(m.text());139      });140      const t0 = Date.now();141      const res = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 90000 }).catch((e) => ({ status: () => `ERR ${e.message.slice(0, 40)}` }));142      await page.waitForTimeout(700);143      const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth).catch(() => -1);144      const expected = exp ?? 200;145      const status = res.status();146      const filtered = filterErrors(errors, expected);147      const problems = status === 200 && check ? await checks(page, check) : [];148      const ok = status === expected && overflow <= 0 && filtered.length === 0 && problems.length === 0;149      if (!ok) failures++;150      console.log(`${ok ? 'OK  ' : 'FAIL'} ${theme.padEnd(5)} ${String(width).padStart(4)} ${status} ${String(Date.now() - t0).padStart(5)}ms overflow=${overflow} errors=${filtered.length} ${path}${problems.length ? ' :: ' + problems.join('; ') : ''}${filtered.length ? ' :: ' + filtered[0].slice(0, 160) : ''}`);151      if ([320, 390, 1440, 1920].includes(width)) await page.screenshot({ path: `${OUT}${theme}-${width}-${slug(path)}.png`, fullPage: width >= 768 }).catch(() => undefined);152      page.removeAllListeners('pageerror');153      page.removeAllListeners('console');154    }155    await ctx.close();156  }157}158159// ---- flows160async function flow(name, width, fn) {161  const ctx = await browser.newContext({ viewport: { width, height: width < 768 ? 844 : 900 }, colorScheme: 'dark', isMobile: width < 768, hasTouch: width < 768 });162  await ctx.addInitScript(() => localStorage.setItem('aia-theme', 'dark'));163  const page = await ctx.newPage();164  const errors = [];165  page.on('pageerror', (e) => errors.push(String(e)));166  page.on('console', (m) => {167    if (m.type() === 'error') errors.push(m.text());168  });169  try {170    await fn(page);171    const filtered = filterErrors(errors, 200);172    if (filtered.length) throw new Error(`console: ${filtered[0].slice(0, 160)}`);173    console.log(`OK   flow ${name} @${width}`);174  } catch (e) {175    failures++;176    console.log(`FAIL flow ${name} @${width} :: ${e.message.slice(0, 200)}`);177    await page.screenshot({ path: `${OUT}flow-${name}-${width}-fail.png`, fullPage: true }).catch(() => undefined);178  }179  await ctx.close();180}181182for (const width of [390, 1440]) {183  // Graph: select a node → inspector shows it; expand loads more nodes (or reports already expanded).184  await flow('graph-inspect-expand', width, async (page) => {185    await page.goto(`${BASE}/graph/${modelSlug}?mode=lineage`, { waitUntil: 'networkidle' });186    await page.waitForSelector('[data-graph-canvas] [data-node]', { timeout: 20000 });187    const before = await page.locator('[data-graph-canvas] [data-node]').count();188    const nodes = page.locator('[data-graph-canvas] [data-node]');189    const target = nodes.nth(Math.min(1, before - 1));190    await target.dispatchEvent('pointerdown', { pointerId: 1, clientX: 0, clientY: 0, isPrimary: true, button: 0 });191    await target.dispatchEvent('pointerup', { pointerId: 1, clientX: 0, clientY: 0, isPrimary: true, button: 0 });192    // Fallback: keyboard selection is always available.193    await target.focus();194    await page.keyboard.press('Enter');195    await page.waitForTimeout(400);196    const inspectors = await page.locator('[data-graph-inspector]').count();197    if (inspectors < 1) throw new Error('inspector not rendered after selection');198    const expand = page.locator('[data-graph-expand]:not([disabled]):visible').first();199    if (await expand.count()) {200      await expand.click();201      await page.waitForTimeout(2500);202      const after = await page.locator('[data-graph-canvas] [data-node]').count();203      if (after < before) throw new Error(`nodes shrank ${before} → ${after}`);204    }205    await page.screenshot({ path: `${OUT}flow-graph-${width}.png` });206  });207208  // Graph: switch mode via the rail (desktop) → count line updates without navigation error.209  if (width >= 1024)210    await flow('graph-mode-switch', width, async (page) => {211      await page.goto(`${BASE}/graph?node=${orgSlug}&mode=company`, { waitUntil: 'networkidle' });212      await page.waitForSelector('[data-graph-canvas] [data-node]', { timeout: 20000 });213      await page.locator('[data-graph-mode="provider"]').click();214      await page.waitForTimeout(2500);215      if (!/mode=provider/.test(page.url())) throw new Error(`url not updated: ${page.url()}`);216      const text = await page.locator('[data-graph-count]').innerText();217      if (!/nodes/.test(text)) throw new Error('count line missing after mode switch');218    });219220  // Search: remove a compiled chip → re-query without its words.221  await flow('search-chip-remove', width, async (page) => {222    await page.goto(`${BASE}/search?q=open+reasoning+models+over+30B+released+in+2026`, { waitUntil: 'networkidle' });223    const chips = await page.locator('[data-compiled-chip]').count();224    if (chips < 2) throw new Error(`chips=${chips}`);225    const remove = page.locator('[data-compiled-chip] a[aria-label^="Remove"]').first();226    const label = await remove.getAttribute('aria-label');227    const before = page.url();228    await Promise.all([page.waitForURL((u) => u.toString() !== before), remove.click()]);229    await page.waitForLoadState('networkidle');230    const after = await page.locator('[data-compiled-chip]').count();231    if (after >= chips) throw new Error(`chips not reduced after "${label}": ${chips} → ${after}`);232  });233234  // Explore builder: set a filter, run → /models URL with the param; save a query → appears in the list.235  await flow('explore-builder', width, async (page) => {236    await page.goto(`${BASE}/explore`, { waitUntil: 'networkidle' });237    await page.locator('[data-query-builder] select').nth(2).selectOption({ index: 3 }); // Parameters ≥ 7B238    await page.waitForTimeout(300);239    const target = await page.locator('[data-target-url]').innerText();240    if (!/\/models\?.*min_params=/.test(target)) throw new Error(`target url ${target}`);241    if (!/type=model/.test(page.url())) throw new Error(`share url not mirrored: ${page.url()}`);242    await page.locator('[data-save-query]').click();243    await page.waitForTimeout(300);244    if ((await page.locator('[data-saved-queries] li').count()) < 1) throw new Error('saved query not listed');245    await Promise.all([page.waitForURL(/\/models\?/), page.locator('[data-run-query]').click()]);246  });247248  // Time machine: presets navigate and the banner says reconstructed / observed.249  await flow('time-machine-presets', width, async (page) => {250    await page.goto(`${BASE}/time-machine`, { waitUntil: 'networkidle' });251    await Promise.all([page.waitForURL(/date=2024-01-01/), page.locator('a[href*="date=2024-01-01"]').first().click()]);252    await page.waitForLoadState('networkidle');253    const text = await page.locator('[data-tm-banner]').innerText();254    if (!/reconstructed|observed/i.test(text)) throw new Error('banner missing basis');255    await Promise.all([page.waitForURL(/scope=benchmarks/), page.locator('a[href*="scope=benchmarks"]').first().click()]);256  });257258  // Changes feed: load more via cursor.259  await flow('changes-load-more', width, async (page) => {260    await page.goto(`${BASE}/changes`, { waitUntil: 'networkidle' });261    const before = await page.locator('[data-event-id]').count();262    const btn = page.locator('[data-load-more]');263    if (!(await btn.count())) return; // fewer than 50 events: nothing to page264    await btn.click();265    await page.waitForTimeout(2500);266    const after = await page.locator('[data-event-id]').count();267    if (after <= before) throw new Error(`load more did not append (${before} → ${after})`);268  });269270  // Developers: request builder → Try → status line.271  await flow('developers-try', width, async (page) => {272    await page.goto(`${BASE}/developers`, { waitUntil: 'networkidle' });273    await page.locator('[data-route-picker]').selectOption('stats');274    await page.locator('[data-try]').click();275    await page.waitForSelector('[data-try-result] pre', { timeout: 20000 });276    const text = await page.locator('[data-try-result]').innerText();277    if (!/HTTP 200/.test(text)) throw new Error(`try result: ${text.slice(0, 80)}`);278  });279280  // Admin: login → quality → entity-resolution → anomalies → audit → extractions; token never in HTML.281  if (TOKEN)282    await flow('admin-workbenches', width, async (page) => {283      await page.goto(`${BASE}/admin`, { waitUntil: 'networkidle' });284      if ((await page.content()).includes(TOKEN)) throw new Error('token leaked before login');285      await page.locator('input[type="password"]').first().fill(TOKEN);286      await Promise.all([page.waitForURL(/\/admin\/(overview|connectors)/, { timeout: 30000 }), page.locator('form button[type="submit"]').first().click()]);287      const pages = ['/admin/quality', '/admin/entity-resolution', '/admin/anomalies', '/admin/quarantine', '/admin/audit', '/admin/runs', ...(snapshotId ? [`/admin/extractions/${snapshotId}`] : [])];288      for (const p of pages) {289        const r = await page.goto(`${BASE}${p}`, { waitUntil: 'networkidle', timeout: 60000 });290        if (r.status() !== 200) throw new Error(`${p} → ${r.status()}`);291        const html = await page.content();292        if (html.includes(TOKEN)) throw new Error(`token leaked in ${p}`);293        const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);294        if (overflow > 0) throw new Error(`${p} overflow ${overflow}`);295        if (p === '/admin/quality' && !(await page.locator('[data-quality-tiles] li').count())) throw new Error('quality tiles missing');296        if (p === '/admin/entity-resolution' && !(await page.locator('[data-resolution-pair]').count()) && !/No candidate pairs/.test(await page.evaluate(() => document.body.innerText))) throw new Error('resolution pairs missing');297        if (p === '/admin/runs' && !(await page.locator('[data-rollback]').count())) throw new Error('rollback buttons missing');298        if (p.startsWith('/admin/extractions') && !(await page.locator('[data-extraction-text]').count())) throw new Error('extraction text missing');299        await page.screenshot({ path: `${OUT}admin-${width}-${slug(p)}.png`, fullPage: width >= 768 });300      }301      // Sources shows operator columns when the cookie exists (cookie path is /admin, so /sources must not see it → public view).302      await page.goto(`${BASE}/sources`, { waitUntil: 'networkidle' });303      if ((await page.content()).includes(TOKEN)) throw new Error('token leaked in /sources');304      // Rollback button opens a confirmation field, disabled until the id is typed.305      await page.goto(`${BASE}/admin/runs`, { waitUntil: 'networkidle' });306      const rb = page.locator('[data-rollback]:not([disabled])').first();307      if (await rb.count()) {308        await rb.click();309        const confirm = page.locator('[data-rollback-form] button[type="submit"]');310        if (!(await confirm.isDisabled())) throw new Error('rollback confirm enabled before typing the id');311      }312    });313}314315// ---- OG images and sitemap (plain HTTP)316for (const p of ['/graph/og', `/graph/og?node=${modelSlug}&mode=lineage`, '/time-machine/og?date=2025-06-01', `/diff/og?a=${weekAgo}&b=${today}`, `/changes/${today}/opengraph-image`, `/companies/${orgSlug}/opengraph-image`, `/papers/${paperSlug}/opengraph-image`]) {317  const r = await fetch(BASE + p);318  const ok = r.status === 200 && /image\/png/.test(r.headers.get('content-type') ?? '');319  if (!ok) failures++;320  console.log(`${ok ? 'OK  ' : 'FAIL'} og ${r.status} ${r.headers.get('content-type')} ${p}`);321}322{323  const idx = await (await fetch(`${BASE}/sitemap.xml`)).text();324  const st = await (await fetch(`${BASE}/sitemap/static.xml`)).text();325  const okIdx = /model_family-0\.xml/.test(idx) && /license-0\.xml/.test(idx) && /artifact-0\.xml/.test(idx) && /changes\.xml/.test(idx);326  const okSt = ['/time-machine', '/graph', '/frontier', '/pulse', '/families', '/licenses', '/agents', '/calculator', '/run-locally', '/find-a-model', '/open'].every((r) => st.includes(`<loc>`) && st.includes(r));327  if (!okIdx || !okSt) failures++;328  console.log(`${okIdx && okSt ? 'OK  ' : 'FAIL'} sitemap index(model_family/license/artifact/changes)=${okIdx} static(new routes)=${okSt}`);329  const fam = await fetch(`${BASE}/sitemap/model_family-0.xml`);330  const famBody = await fam.text();331  const okFam = fam.status === 200 && /\/families\//.test(famBody);332  if (!okFam) failures++;333  console.log(`${okFam ? 'OK  ' : 'FAIL'} sitemap model_family-0 ${fam.status}`);334}335336await browser.close();337console.log(failures ? `\n${failures} failure(s)` : '\nAll D3 checks passed');338process.exit(failures ? 1 : 0);339