fix(7segment): rebuild per-element sim state when the digit count changes
Root cause of "el agente construye el reloj, dice que funciona, pero el display queda en blanco hasta recargar la página" — diagnosed by driving the live agent end-to-end and instrumenting the element: The 7-segment part simulator caches its state (segments, digitValues, digitEnabled) in a WeakMap keyed by the DOM element, sizing it from element.digits at FIRST access. The agent builds incrementally: it adds the display with the default digits=1 and only then sets digits=4 — so the cached state was born in single-digit mode. Every later attachEvents (compile bumps hexEpoch → re-attach with the finished wiring) kept consulting the stale state: it subscribed COM.1/COM.2 (which don't exist on a 4-digit part) instead of DIG1..DIG4, and because those resolvers DID attach, the all-digits-on fallback never kicked in either. Result: no digit ever enabled, no flush ever ran, values stayed a frozen 8-zero array. A page reload "fixed" it because the fresh element mounted with digits already 4. get7SegState now compares the cached digit count against the element's current value and rebuilds the state when they differ, so any re-attach after a digits change subscribes the right pins. Test: attach with digits=1 (COM subscribed), set digits=4, re-attach → DIG1..4 subscribed, and a segment+digit pulse actually lights values[0] in the 32-slot array. Verified live: the exact agent prompt that produced a permanently blank display now shows the multiplexed digits + blinking colon in-session, no reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fce1cdafdc
commit
8c1bbbc1a7
|
|
@ -557,6 +557,50 @@ describe('7segment — attachEvents', () => {
|
|||
expect(unsubMock).toHaveBeenCalledTimes(8);
|
||||
});
|
||||
|
||||
it('re-attach after digits changes 1→4 subscribes DIG pins (agent build order)', () => {
|
||||
// The agent adds the display with default digits=1, THEN sets digits=4,
|
||||
// then wires, compiles (hexEpoch bump → re-attach) and runs. The cached
|
||||
// per-element state must rebuild for the new digit count, or the
|
||||
// re-attach keeps subscribing COM.1/COM.2 and the display stays blank
|
||||
// until a page reload.
|
||||
const logic = PartSimulationRegistry.get('7segment')!;
|
||||
const el = makeElement({ values: new Array(8).fill(0), digits: 1 });
|
||||
const sim = makeSimulator();
|
||||
const subscribed: number[] = [];
|
||||
sim.pinManager.onPinChange.mockImplementation((pin: number) => {
|
||||
subscribed.push(pin);
|
||||
return () => {};
|
||||
});
|
||||
|
||||
// First attach while digits=1 (mid-build): subscribes COM pins only.
|
||||
const pins1 = pinMap({ 'COM.1': 30, 'COM.2': 31, DIG1: 10, DIG2: 11, DIG3: 12, DIG4: 13, A: 2 });
|
||||
const cleanup1 = logic.attachEvents!(el, sim as any, pins1);
|
||||
expect(subscribed).toContain(30); // COM.1 (single-digit mode)
|
||||
expect(subscribed).not.toContain(10); // DIG1 not subscribed yet
|
||||
cleanup1();
|
||||
|
||||
// Property set later: digits=4. Re-attach (compile/run does this).
|
||||
(el as any).digits = 4;
|
||||
subscribed.length = 0;
|
||||
const callbacks = new Map<number, (pin: number, state: boolean) => void>();
|
||||
sim.pinManager.onPinChange.mockImplementation(
|
||||
(pin: number, cb: (pin: number, state: boolean) => void) => {
|
||||
subscribed.push(pin);
|
||||
callbacks.set(pin, cb);
|
||||
return () => {};
|
||||
},
|
||||
);
|
||||
logic.attachEvents!(el, sim as any, pins1);
|
||||
expect(subscribed).toEqual(expect.arrayContaining([10, 11, 12, 13])); // DIG1..4
|
||||
|
||||
// And the display actually lights: segment A high + DIG1 enabled.
|
||||
callbacks.get(2)!(2, true); // segment A high
|
||||
callbacks.get(10)!(10, true); // DIG1 on → latches segments
|
||||
const values = (el as any).values as number[];
|
||||
expect(values.length).toBe(32); // 4 digits × 8 segments
|
||||
expect(values[0]).toBe(1); // digit 0, segment A lit
|
||||
});
|
||||
|
||||
it('CLN pin drives colon + colonValue (attachEvents and onPinStateChange)', () => {
|
||||
const logic = PartSimulationRegistry.get('7segment')!;
|
||||
|
||||
|
|
|
|||
|
|
@ -94,8 +94,20 @@ function getDigitsCount(element: HTMLElement): number {
|
|||
|
||||
function get7SegState(element: HTMLElement): SevenSegState {
|
||||
let s = sevenSegState.get(element);
|
||||
const digits = getDigitsCount(element);
|
||||
// Rebuild when the digit count CHANGED since the state was created. The
|
||||
// agent adds the display with the default digits=1 and only then sets
|
||||
// digits=4 — the stale 1-digit state made every later attachEvents
|
||||
// subscribe COM.1/COM.2 (which don't exist on a 4-digit part) instead of
|
||||
// DIG1..DIG4, and since those resolvers "attached", the all-digits-on
|
||||
// fallback never kicked in either: the display stayed permanently blank
|
||||
// in-session and only a page reload (fresh element, digits already 4)
|
||||
// fixed it.
|
||||
if (s && s.digits !== digits) {
|
||||
if (s.flushTimer !== null) clearTimeout(s.flushTimer);
|
||||
s = undefined;
|
||||
}
|
||||
if (!s) {
|
||||
const digits = getDigitsCount(element);
|
||||
s = {
|
||||
digits,
|
||||
segments: [0, 0, 0, 0, 0, 0, 0, 0],
|
||||
|
|
|
|||
Loading…
Reference in New Issue