velxio/frontend/src/__tests__/gallery-run-gate.audit.test.ts

117 lines
4.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { exampleProjects as examples } from '../data/examples';
import { buildInputFromStore } from '../simulation/spice/storeAdapter';
import { BOARD_PIN_GROUPS } from '../simulation/spice/boardPinGroups';
import { verifyCircuit } from '../simulation/verify/circuitVerifier';
import { stripBrandPrefix, isBoardComponentType } from '../utils/exampleToBuildNetlistInput';
import type { PinSourceState } from '../simulation/spice/types';
import type { BoardKind } from '../store/useSimulatorStore';
/**
* Gallery audit: which examples would the Run button REFUSE to start?
*
* EditorToolbar's `checkOrBlock` runs `verifyCircuitFromStore()` before every Run and
* returns false when the verifier reports errors — the compile never fires and the user
* gets a "run anyway?" modal. So a circuit error is not cosmetic: it is the difference
* between an example that runs and one that appears dead.
*
* This mattered because the defects hide until the circuit solves END TO END. c3-button
* wired its button to pins that do not exist, so the netlist never closed and nobody saw
* that its LED had no series resistor. Fixing the pin names surfaced a 506 mA LED that
* had been wrong since the example was written.
*
* Reproduces the Run gate exactly: same worst-case snapshot (every wired digital pin
* forced HIGH at the board's vcc, rails skipped), same buildInputFromStore, same
* verifyCircuit. Real ngspice, no browser.
*/
function boardKindOf(example: (typeof examples)[number]): BoardKind | null {
const bt = (example as { boardType?: string }).boardType;
return (bt as BoardKind) ?? null;
}
/** The board component id an example's wires refer to (legacy examples say 'arduino-uno'). */
function boardIdsOf(example: (typeof examples)[number]): Set<string> {
const declared = new Set(example.components?.map((c) => c.id) ?? []);
const refs = new Set<string>();
for (const w of example.wires ?? []) {
for (const ep of [w.start, w.end]) if (!declared.has(ep.componentId)) refs.add(ep.componentId);
}
return refs;
}
async function runGate(example: (typeof examples)[number]) {
const kind = boardKindOf(example);
const components = (example.components ?? [])
.filter((c) => !isBoardComponentType(c.type))
.map((c) => ({
id: c.id,
metadataId: stripBrandPrefix(c.type),
properties: c.properties ?? {},
}));
const wires = (example.wires ?? []).map((w) => ({
id: w.id,
start: { componentId: w.start.componentId, pinName: w.start.pinName },
end: { componentId: w.end.componentId, pinName: w.end.pinName },
color: '#666',
waypoints: [],
}));
const boards = kind
? [...boardIdsOf(example)].map((id) => {
const group = BOARD_PIN_GROUPS[kind] ?? BOARD_PIN_GROUPS.default;
const pinStates: Record<string, PinSourceState> = {};
const wired = new Set<string>();
for (const w of wires) {
if (w.start.componentId === id) wired.add(w.start.pinName);
if (w.end.componentId === id) wired.add(w.end.pinName);
}
for (const pinName of wired) {
if (group.gnd.includes(pinName)) continue;
if (group.vcc_pins.includes(pinName)) continue;
if (Number.isNaN(Number.parseInt(pinName, 10))) continue;
pinStates[pinName] = { type: 'digital', v: group.vcc };
}
return { id, boardKind: kind, pinStates };
})
: [];
const input = buildInputFromStore({ components, wires, boards });
return verifyCircuit(input);
}
describe('gallery: no example is blocked by its own circuit', () => {
const candidates = examples.filter(
(e) => (e.components?.length ?? 0) > 0 && (e.wires?.length ?? 0) > 0,
);
it('audits every wired example and reports the blockers', async () => {
const blocked: Array<{ id: string; board: string; codes: string[]; detail: string }> = [];
for (const ex of candidates) {
let res;
try {
res = await runGate(ex);
} catch {
continue; // unbuildable snapshot — the real app treats that as "don't block"
}
if (res.errors.length) {
blocked.push({
id: ex.id,
board: (ex as { boardType?: string }).boardType ?? '(sin placa)',
codes: [...new Set(res.errors.map((e) => e.code))],
detail: res.errors[0].message.slice(0, 110),
});
}
}
if (blocked.length) {
console.log(
`\n=== ${blocked.length} de ${candidates.length} ejemplos con el Run BLOQUEADO ===`,
);
for (const b of blocked)
console.log(` ${b.board.padEnd(20)} ${b.id.padEnd(36)} ${b.codes.join(',')} ${b.detail}`);
}
expect(blocked.map((b) => `${b.id} [${b.codes.join(',')}]`)).toEqual([]);
}, 600_000);
});