/** * Build fixtures for the cert connectors from LIVE public verification pages (through the real router, * i.e. Firecrawl → Scrapfly exactly as production does). Every fixture is a real capture, trimmed by * the connector's own `trim`. * * pnpm tsx connectors/firecrawl/_g2-grading-lib/capture.ts = [...] * pnpm tsx connectors/firecrawl/_g2-grading-lib/capture.ts psa-cert charizard-psa10=69211238 not-found=99999999 * * Also usable as a live smoke: prints the normalised records of each capture. */ import { existsSync, readFileSync, readdirSync } from 'node:fs'; import path from 'node:path'; import { createCrawlContext, createRouter, ConnectorMetaSchema, DomainsFileSchema, DOMAINS_DIR, DOMAINS_PATH, setDomains } from '@rareindex/connectors'; import { saveFixture } from '@rareindex/connectors/testing'; import { childLogger } from '@rareindex/shared'; import type { CertLookupConnector } from './cert-base.js'; const [connectorId, ...pairs] = process.argv.slice(2); if (!connectorId || !pairs.length) { console.error('usage: capture.ts = ...'); process.exit(1); } try { process.loadEnvFile(path.resolve('.env')); } catch { /* .env optional */ } // Dev-time guard: while several groups edit connectors/domains.d in parallel, one invalid fragment must // not stop our captures — load domains.json + every fragment that validates, warn about the others. { const base = DomainsFileSchema.parse(JSON.parse(readFileSync(DOMAINS_PATH, 'utf8'))); if (existsSync(DOMAINS_DIR)) { for (const f of readdirSync(DOMAINS_DIR).filter((x) => x.endsWith('.json')).sort()) { const parsed = DomainsFileSchema.safeParse({ ...JSON.parse(readFileSync(path.join(DOMAINS_DIR, f), 'utf8')), version: base.version }); if (!parsed.success) { console.warn(` [capture] skipping invalid domains fragment ${f}: ${parsed.error.issues[0]?.path.join('.')} ${parsed.error.issues[0]?.message}`); continue; } for (const [host, patch] of Object.entries(parsed.data.domains)) base.domains[host] = { ...(base.domains[host] ?? {}), ...patch }; } } setDomains(base); } const dir = path.resolve('connectors/firecrawl', connectorId); const meta = ConnectorMetaSchema.parse(JSON.parse(readFileSync(path.join(dir, 'meta.json'), 'utf8'))); const mod = (await import(path.join(dir, 'index.ts'))) as { default: (m: typeof meta) => CertLookupConnector }; const connector = mod.default(meta); const router = createRouter({ firecrawlApiKey: process.env.FIRECRAWL_API_KEY, scrapflyApiKey: process.env.SCRAPFLY_API_KEY }); const ctx = createCrawlContext({ router, meta, options: { mode: 'probe', limit: pairs.length }, log: childLogger({ connector: connectorId, level: 'warn' }) }); for (const pair of pairs) { const eq = pair.indexOf('='); const name = pair.slice(0, eq); const seed = pair.slice(eq + 1); const cert = /^https?:/.test(seed) ? connector.certFromUrl(seed) : seed; if (!cert) { console.error(` [${connectorId}] ${name}: cannot derive a cert from ${seed}`); continue; } const raw = await connector.fetchCert(ctx, cert, { includeNotFound: true }); if (!raw) { console.error(` [${connectorId}] ${name}: no page captured (${JSON.stringify(ctx.anomalies.slice(-1))})`); continue; } const out = await connector.normalize({ ...raw, externalId: raw.externalId ?? null, fetchedAt: raw.fetchedAt ?? new Date() }); const payload = raw.payload as { status: string; url: string }; const kinds = [...new Set(out.map((r) => r.kind))]; saveFixture(connectorId, name, { raw: { url: raw.url, externalId: raw.externalId ?? null, kind: raw.kind, engine: raw.engine, fetchedAt: raw.fetchedAt ?? new Date(), payload: raw.payload }, // requiredFields must hold for every record kind (population_report has no `grade` block). expect: payload.status === 'not_found' ? { count: 0 } : { count: out.length, kinds, requiredFields: ['attributes.categorySlug', `attributes.identifiers.${connector.idKey}`], first: { 'grade.certificationNumber': connector.certIdentifier(cert) } }, note: payload.status === 'not_found' ? `Live capture (${raw.engine}) of ${payload.url}: the grader reports this certification number as not found; normalises to zero records.` : `Live capture (${raw.engine}) of the public verification page ${payload.url}, trimmed to the results fragment.`, }); console.log(` [${connectorId}] ${name}: ${payload.status} via ${raw.engine} → ${out.length} record(s) ${kinds.join(',')}`); for (const r of out) { if (r.kind === 'catalog_item') console.log(` ${r.rawTitle} | ${r.attributes.categorySlug} | grade=${r.grade.grader} ${r.grade.grade ?? '-'}${r.grade.qualifier ? ` (${r.grade.qualifier})` : ''} | ids=${JSON.stringify(r.attributes.identifiers)}`); if (r.kind === 'population_report') console.log(` population total=${r.total} byGrade=${JSON.stringify(r.byGrade)}`); } } console.log(`[${connectorId}] engineStats=${JSON.stringify(ctx.engineStats)} anomalies=${ctx.anomalies.length}`);