velxio/frontend/src/__tests__/mixed-mode-scheduler.test.ts

106 lines
3.8 KiB
TypeScript

/**
* Phase 1b continued — Step 1 tests for MixedModeScheduler.
*
* Exercises the subscriber routing and voltage cache in isolation from
* the SPICE engine. The engine is never booted in these tests; we
* drive `publishVoltage` directly so the routing logic can be locked
* down before the real `alter + tran + readVec` loop lands.
*
* Coverage:
* - publishVoltage fires every matching subscriber and only those
* - getCurrentVoltage returns the last published value per pin
* - unsubscribe removes the callback cleanly
* - reset (via __resetMixedModeScheduler) clears state between tests
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import {
getMixedModeScheduler,
__resetMixedModeScheduler,
} from '../simulation/spice/MixedModeScheduler';
afterEach(() => {
__resetMixedModeScheduler();
});
describe('MixedModeScheduler — voltage cache', () => {
it('returns null until something is published', () => {
const sched = getMixedModeScheduler();
expect(sched.getCurrentVoltage('q1', 'C')).toBeNull();
});
it('returns the last published voltage per (component, pin)', () => {
const sched = getMixedModeScheduler();
sched.publishVoltage('q1', 'C', 4.5);
sched.publishVoltage('q1', 'B', 1.2);
sched.publishVoltage('q2', 'C', 0.3);
expect(sched.getCurrentVoltage('q1', 'C')).toBe(4.5);
expect(sched.getCurrentVoltage('q1', 'B')).toBe(1.2);
expect(sched.getCurrentVoltage('q2', 'C')).toBe(0.3);
sched.publishVoltage('q1', 'C', 2.7); // overwrite
expect(sched.getCurrentVoltage('q1', 'C')).toBe(2.7);
});
});
describe('MixedModeScheduler — subscribe / publish routing', () => {
it('fires the matching subscriber with the published voltage', () => {
const sched = getMixedModeScheduler();
const cb = vi.fn();
sched.subscribe('q1', 'C', cb);
sched.publishVoltage('q1', 'C', 4.7);
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith('UNKNOWN', 4.7);
});
it('does NOT fire subscribers watching a different pin', () => {
const sched = getMixedModeScheduler();
const cbMatching = vi.fn();
const cbOtherPin = vi.fn();
const cbOtherComp = vi.fn();
sched.subscribe('q1', 'C', cbMatching);
sched.subscribe('q1', 'B', cbOtherPin);
sched.subscribe('q2', 'C', cbOtherComp);
sched.publishVoltage('q1', 'C', 4.7);
expect(cbMatching).toHaveBeenCalledTimes(1);
expect(cbOtherPin).not.toHaveBeenCalled();
expect(cbOtherComp).not.toHaveBeenCalled();
});
it('supports multiple subscribers on the same pin (fan-out)', () => {
const sched = getMixedModeScheduler();
const cbA = vi.fn();
const cbB = vi.fn();
sched.subscribe('q1', 'C', cbA);
sched.subscribe('q1', 'C', cbB);
sched.publishVoltage('q1', 'C', 4.7);
expect(cbA).toHaveBeenCalledWith('UNKNOWN', 4.7);
expect(cbB).toHaveBeenCalledWith('UNKNOWN', 4.7);
});
it('unsubscribe handle detaches the callback', () => {
const sched = getMixedModeScheduler();
const cb = vi.fn();
const cancel = sched.subscribe('q1', 'C', cb);
sched.publishVoltage('q1', 'C', 4.7);
expect(cb).toHaveBeenCalledTimes(1);
cancel();
sched.publishVoltage('q1', 'C', 0.3);
expect(cb).toHaveBeenCalledTimes(1); // not called again
});
it('reset clears subscribers and voltage cache', () => {
const sched = getMixedModeScheduler();
const cb = vi.fn();
sched.subscribe('q1', 'C', cb);
sched.publishVoltage('q1', 'C', 4.7);
__resetMixedModeScheduler();
const sched2 = getMixedModeScheduler();
expect(sched2).not.toBe(sched);
expect(sched2.getCurrentVoltage('q1', 'C')).toBeNull();
sched2.publishVoltage('q1', 'C', 0.5);
// The old subscriber attached to the disposed scheduler must NOT
// fire from the new scheduler instance.
expect(cb).toHaveBeenCalledTimes(1);
});
});