From 5a79836fdc3ad7c76cfea2a260d12e3f84260b6b Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Wed, 29 Jul 2026 07:24:25 +0200 Subject: [PATCH] test(examples): ningun LED de la galeria puede colgar directo de un pin Guarda de regresion para lo que se acaba de arreglar. Recorre el grafo de cableado (no solo el vecino inmediato del LED, porque la resistencia protege igual desde el catodo o desde el otro lado de un transistor o un rele) y falla si algun LED llega a un pin sin limitar la corriente. Es el peor tipo de ejemplo roto: el sketch imprime "LED ON", el runtime quema el LED y no aparece ningun error, solo un LED oscuro. --- .../__tests__/examples-led-resistor.test.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 frontend/src/__tests__/examples-led-resistor.test.ts diff --git a/frontend/src/__tests__/examples-led-resistor.test.ts b/frontend/src/__tests__/examples-led-resistor.test.ts new file mode 100644 index 00000000..da67b1f5 --- /dev/null +++ b/frontend/src/__tests__/examples-led-resistor.test.ts @@ -0,0 +1,93 @@ +/** + * Every gallery LED must have a resistor in its path. + * + * The electrical model is honest: a LED straight across a 3.3 V or 5 V pin + * asks for an absurd current, the runtime burns it out and it stays dark + * for the rest of the run — while the sketch happily prints "LED ON". That + * is the worst kind of broken example, because nothing looks like an + * error. Twenty-four of them shipped that way (mostly Pico, and every RGB + * example, which needs one resistor PER CHANNEL) before this guard existed. + * + * The check walks the wiring graph rather than the LED's immediate + * neighbour: a resistor protects from the anode side, from the cathode + * side, and from the far side of a transistor or relay contact. + */ +import { describe, it, expect } from 'vitest'; +import { exampleProjects } from '../data/examples'; + +const LED_TYPES = new Set(['wokwi-led', 'wokwi-rgb-led']); +/** Anything that limits current well enough to save the LED. */ +const LIMITER_TYPES = [ + 'wokwi-resistor', + 'wokwi-resistor-us', + 'wokwi-potentiometer', + 'wokwi-slide-potentiometer', + 'wokwi-ntc-temperature-sensor', + 'wokwi-photoresistor-sensor', +]; + +interface Comp { + type?: string; + id?: string; +} +interface Wire { + start?: { componentId?: string }; + end?: { componentId?: string }; +} + +function isLimiter(type: string | undefined): boolean { + return !!type && LIMITER_TYPES.some((t) => type.startsWith(t)); +} + +/** LEDs with no current limiter anywhere on their side of the circuit. */ +function unprotectedLeds(example: { + components?: Comp[]; + wires?: Wire[]; +}): string[] { + const comps = new Map(); + for (const c of example.components ?? []) { + if (c?.id) comps.set(c.id, c.type ?? ''); + } + const leds = [...comps.entries()] + .filter(([, type]) => LED_TYPES.has(type)) + .map(([id]) => id); + if (!leds.length) return []; + + const adj = new Map(); + for (const w of example.wires ?? []) { + const a = w?.start?.componentId; + const b = w?.end?.componentId; + if (!a || !b) continue; + adj.set(a, [...(adj.get(a) ?? []), b]); + adj.set(b, [...(adj.get(b) ?? []), a]); + } + + return leds.filter((led) => { + const seen = new Set([led]); + const queue = [...(adj.get(led) ?? [])]; + while (queue.length) { + const node = queue.shift()!; + if (seen.has(node)) continue; + seen.add(node); + const type = comps.get(node); + if (isLimiter(type)) return false; // protected + // An unknown id is the board itself (a supply rail) and another LED + // is not protection either: neither continues the search. + if (type === undefined || LED_TYPES.has(type)) continue; + queue.push(...(adj.get(node) ?? [])); + } + return true; + }); +} + +describe('gallery examples — LED current limiting', () => { + it('no example drives a LED straight off a pin', () => { + const offenders: string[] = []; + for (const ex of exampleProjects) { + for (const led of unprotectedLeds(ex as never)) { + offenders.push(`${ex.id}: ${led}`); + } + } + expect(offenders, 'LEDs without a series resistor').toEqual([]); + }); +});