velxio/frontend/src/__tests__/esp32-breadboard-display.te...

80 lines
3.4 KiB
TypeScript
Raw Normal View History

fix(trace): recognise runtime boards and same-hole junctions in pin tracing An ESP32 clock built by the agent stayed dark while QEMU was verifiably emitting hundreds of GPIO edges per second (437/pin measured on the live websocket). Reload did not help — this was not the seating race. Two independent tracing bugs, reproduced from the real project circuit (fixture included) and each sufficient to kill the display: Boards added at runtime were invisible -------------------------------------- isBoardComponent matches static id prefixes ('arduino-uno', ...), which only covers the default board. Every board added at runtime gets a minted UUID id — the agent's add_board always does — so traceDetailed treated the board endpoint as an unknown component and resolved null, and SimulatorCanvas's direct-wire subscription path skipped it entirely. Every Uno project happened to work because they reuse the default board whose instance id IS the literal 'arduino-uno'. Both sites now consult the live boards list first, keeping isBoardComponent as the legacy-id fallback. Strip walking missed wires stacked on one hole ---------------------------------------------- The breadboard group walk continued the trace from every OTHER wired hole of the strip, excluding the arrival hole by name. But two wires may legitimately share one hole — the agent bridges strips straight into the seat hole (8 of this circuit's 9 bridges land exactly on a resistor's own hole), which is electrically identical to using a free hole of the strip. The name exclusion made those junctions dead ends. Exclusion is now by incoming WIRE id, so same-hole connections resolve; the depth bound already prevents ping-ponging between two wires of one net. With both fixes the exact saved circuit resolves every display pin to its GPIO (A..DP -> 32,33,25,26,27,14,12,13; DIG1..4 -> 15,2,4,5; COM -> GND) and the live project now shows 12:00 on the real QEMU simulation. traceDetailed is exported for the regression test, which drives the real store with the real circuit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 23:46:23 +07:00
// @vitest-environment jsdom
/**
* Reproduction of the reported "ESP32 clock through a breadboard is dead"
* bug, using the exact circuit the agent built (fixture exported from the
* real project). QEMU was proven to emit every GPIO edge (437 events/pin on
* the live site) — the break is in the frontend resolution/subscription.
*
* These tests interrogate each stage separately so a failure names the
* broken stage rather than just "display dead".
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { useSimulatorStore } from '../store/useSimulatorStore';
import { traceDetailed } from '../components/DynamicComponent';
import circuit from './fixtures/esp32-clock-circuit.json';
const BOARD = circuit.boardId;
function loadCircuit() {
const s = useSimulatorStore.getState();
s.setComponents(circuit.components.map((c) => ({ ...c, x: 0, y: 0 })) as never);
s.setWires(circuit.wires.map((w) => ({
...w, waypoints: [], color: '#000',
start: { ...w.start, x: 0, y: 0 }, end: { ...w.end, x: 0, y: 0 },
})) as never);
}
describe('ESP32 + breadboard-seated 7segment (real agent circuit)', () => {
beforeAll(() => {
const s = useSimulatorStore.getState();
s.addBoard('esp32' as never, 0, 0, BOARD);
loadCircuit();
});
it('traceDetailed resolves every segment pin to its GPIO through the breadboard', () => {
const state = useSimulatorStore.getState();
const expected: Record<string, number> = {
A: 32, B: 33, C: 25, D: 26, E: 27, F: 14, G: 12, DP: 13,
DIG1: 15, DIG2: 2, DIG3: 4, DIG4: 5,
};
for (const [pin, gpio] of Object.entries(expected)) {
const r = traceDetailed(state as never, '7segment_1', pin, 0);
expect(r.arduinoPin, `7segment.${pin} should reach GPIO ${gpio}`).toBe(gpio);
}
});
it('COM resolves to ground (-1) through the rail', () => {
const state = useSimulatorStore.getState();
const r = traceDetailed(state as never, '7segment_1', 'COM', 0);
expect(r.arduinoPin).toBe(-1);
});
it('the esp32 board exposes a simulator with a live pinManager', () => {
// DynamicComponent's attach effect reads store.simulator; the 7segment's
// attachEvents bails out entirely when simulator.pinManager is missing,
// which strands the display on the breadboard-blind onPinStateChange
// path (single-hop isBoardComponent — never true through a breadboard).
const s = useSimulatorStore.getState();
s.setActiveBoardId(BOARD);
const after = useSimulatorStore.getState();
expect(after.simulator, 'store.simulator for the esp32 board').toBeTruthy();
const pm = (after.simulator as { pinManager?: unknown } | null)?.pinManager;
expect(pm, 'simulator.pinManager (attachEvents bails without it)').toBeTruthy();
});
it('a GPIO edge from the bridge reaches a resolver subscribed via the pinManager', () => {
const s = useSimulatorStore.getState();
const pm = (s.simulator as { pinManager?: {
onPinChange: (pin: number, cb: (pin: number, state: boolean) => void) => () => void;
triggerPinChange: (pin: number, state: boolean, source?: string) => void;
} } | null)?.pinManager;
expect(pm).toBeTruthy();
let seen: boolean | null = null;
const un = pm!.onPinChange(32, (_p, st) => { seen = st; });
// What Esp32Bridge.onPinChange does on a gpio_change frame:
pm!.triggerPinChange(32, true, 'mcu');
un();
expect(seen, 'edge on GPIO 32 must reach subscribers').toBe(true);
});
});