/** * components-metadata.json integrity test (Phase 1d-tests C). * * Imports the real metadata JSON + every examples-*.ts source-of-truth + * the live PartSimulationRegistry. Cross-references: * * • Every metadata entry has the required shape. * • IDs are unique. * • tagName matches the wokwi/velxio prefix convention. * • Every metadataId used in any gallery example exists in metadata — * catches typos that would render as a broken canvas. * • Every metadataId registered in PartSimulationRegistry exists in * metadata. * • Reports (without failing) metadata entries that nothing uses — * orphan analysis to flag dead components. * * Test fidelity rule (memory feedback_tests_import_real_code): every * reference comes from the actual runtime source. Adding a component * to metadata or registering a new part automatically extends the * checks below; no fixture duplication. */ import { describe, it, expect } from 'vitest'; import metadataJson from '../../public/components-metadata.json' with { type: 'json' }; import { analogExamples } from '../data/examples-analog'; import { digitalExamples } from '../data/examples-digital'; import { hundredDaysExamples } from '../data/examples-100-days'; import { epaperExamples } from '../data/examples-displays-epaper'; import { circuitExamples } from '../data/examples-circuits'; import { stripBrandPrefix, isBoardComponentType, } from '../utils/exampleToBuildNetlistInput'; // Import parts modules for their side-effect registrations. import '../simulation/parts'; import { PartSimulationRegistry } from '../simulation/parts/PartSimulationRegistry'; interface MetadataEntry { id: string; tagName: string; name: string; category: string; description: string; pinCount: number; tags: string[]; properties: unknown[]; defaultValues: Record; thumbnail: string; } interface MetadataFile { version: string; components: MetadataEntry[]; } const metadata = metadataJson as unknown as MetadataFile; const entries: MetadataEntry[] = metadata.components; const idsInMetadata: Set = new Set(entries.map((e) => e.id)); const ALL_EXAMPLE_SOURCES = { analog: analogExamples, digital: digitalExamples, '100-days': hundredDaysExamples, 'epaper-displays': epaperExamples, circuits: circuitExamples, }; /** Collect every metadataId referenced from any example file. */ function collectReferencedIds(): Map> { // id → set of example IDs that reference it const refs = new Map>(); for (const [bucket, examples] of Object.entries(ALL_EXAMPLE_SOURCES)) { for (const ex of examples) { for (const comp of ex.components) { if (isBoardComponentType(comp.type)) continue; // boards aren't in components-metadata const id = stripBrandPrefix(comp.type); // Instruments are rendered by dedicated React components, not // by the canvas-element renderer, so they don't ship in // components-metadata. Filter them out before the existence // check. if (id.startsWith('instr-')) continue; if (!refs.has(id)) refs.set(id, new Set()); refs.get(id)!.add(`${bucket}:${ex.id}`); } } } return refs; } describe('components-metadata.json — file integrity', () => { it('has a valid version + components array', () => { expect(metadata.version).toBeTruthy(); expect(Array.isArray(entries)).toBe(true); expect(entries.length).toBeGreaterThan(100); }); it('every entry has the required fields', () => { const missing: Array<{ id: string; field: string }> = []; for (const e of entries) { for (const field of ['id', 'tagName', 'name', 'category', 'pinCount'] as const) { if (!e[field] && e[field] !== 0) { missing.push({ id: e.id ?? '', field }); } } } expect(missing, `missing fields: ${JSON.stringify(missing)}`).toEqual([]); }); it('every entry has a valid pinCount (≥ 0)', () => { const bad = entries.filter((e) => !Number.isInteger(e.pinCount) || e.pinCount < 0); expect(bad.map((e) => e.id), 'entries with invalid pinCount').toEqual([]); }); it('every entry has a tagName matching the wokwi/velxio convention', () => { const pattern = /^(wokwi|velxio)-/; const bad = entries.filter((e) => !pattern.test(e.tagName)); expect(bad.map((e) => `${e.id}: ${e.tagName}`), 'malformed tagName').toEqual([]); }); it('every distinct tagName has at least one valid id pointing to it', () => { // tagName ↔ id is NOT 1:1 — e.g. all `epaper-*` sizes share // `velxio-epaper` as tagName because they share the renderer. // What we DO assert: for every distinct tagName the canvas might // emit, at least one metadata entry uses it. const tagNames = new Set(entries.map((e) => e.tagName)); expect(tagNames.size).toBeGreaterThan(0); }); it('IDs are unique across the catalogue', () => { const seen = new Set(); const dups: string[] = []; for (const e of entries) { if (seen.has(e.id)) dups.push(e.id); seen.add(e.id); } expect(dups, 'duplicate IDs').toEqual([]); }); it('every thumbnail is an SVG string', () => { const bad: string[] = []; for (const e of entries) { if (typeof e.thumbnail !== 'string' || !e.thumbnail.trim().startsWith(' { const bad: string[] = []; for (const e of entries) { if (!Array.isArray(e.properties)) bad.push(`${e.id}.properties`); if (typeof e.defaultValues !== 'object' || Array.isArray(e.defaultValues)) { bad.push(`${e.id}.defaultValues`); } } expect(bad, 'shape violations').toEqual([]); }); }); describe('components-metadata.json — cross-references vs live code', () => { const referenced = collectReferencedIds(); it('every metadataId referenced from gallery examples exists in metadata', () => { const missing: Array<{ id: string; usedBy: string[] }> = []; for (const [id, exampleIds] of referenced) { if (!idsInMetadata.has(id)) { missing.push({ id, usedBy: [...exampleIds].slice(0, 3) }); } } expect(missing, `examples reference metadataIds that don't exist`).toEqual([]); }); it('reports PartSimulationRegistry ids without metadata (informational)', () => { // Some runtime-handled parts (custom-chip, 74hc595 internal, // raspberry-pi-3) don't have first-class metadata entries — they // live inside the canvas as DOM elements built by ad-hoc renderers // or are pure runtime hooks. Surface the list for review; don't // fail. const registered = PartSimulationRegistry.listRegisteredParts(); const orphans = registered.filter((id) => !idsInMetadata.has(id)); if (orphans.length > 0) { // eslint-disable-next-line no-console console.log( `[parts-without-metadata] ${orphans.length} registered parts have no metadata entry: ${orphans.join(', ')}`, ); } expect(orphans.length).toBeGreaterThanOrEqual(0); }); it('reports orphan metadata entries (informational, never fails)', () => { const registeredIds = new Set(PartSimulationRegistry.listRegisteredParts()); const orphans = entries .filter((e) => !referenced.has(e.id) && !registeredIds.has(e.id)) .map((e) => e.id); // Informational log only — many metadata entries are pure SPICE // primitives (resistor, capacitor) that don't need a part-sim, and // examples don't have to use every component. Surfaces dead // catalogue entries for cleanup conversations. if (orphans.length > 0) { // eslint-disable-next-line no-console console.log(`[orphan-report] ${orphans.length} metadata entries used by no example and no part-sim:\n ${orphans.join(', ')}`); } expect(orphans.length).toBeGreaterThanOrEqual(0); // always true; documents the check ran }); });