velxio/frontend/src/__tests__/chipbus-netkey.test.ts

154 lines
5.8 KiB
TypeScript

/**
* Multi-chip digital bus — Phase 0 go/no-go proof (project/multichip-bus/).
*
* D-008: the cheapest falsification of the core assumption. If a shared net
* key does NOT make a byte written by one chip visible to another, the keying
* model is wrong and we stop before building the kernel. These tests prove:
*
* 1. Root cause A is fixed — two chips on one wire resolve to the SAME key.
* 2. The bug is real — per-endpoint syntheticChipPin keys differ.
* 3. Byte exchange works — a write on the driver's keys is visible
* synchronously to watchers the reader registered on its own keys.
* 4. The flag gates it — off by default (legacy path untouched).
* 5. No regression — a single-chip chip-to-component net is NOT collapsed,
* so rules 2/3 still own it.
*
* This is WASM-free on purpose: it exercises the resolver keying + the real
* PinManager fan-out directly. The full two-real-chips-light-8-LEDs milestone
* is verified live in the app once the flag is flipped (see 03-phases.md).
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { PinManager } from '../simulation/PinManager';
import {
resolveChipNetKey,
setChipBusEnabledForTest,
resetChipNetIndexForTest,
type ChipNetState,
} from '../simulation/customChips/chipNets';
import { syntheticChipPin } from '../simulation/customChips/syntheticPins';
// ── Builders ─────────────────────────────────────────────────────────────────
const chip = (id: string) => ({ id, metadataId: 'custom-chip' });
const part = (id: string, metadataId: string) => ({ id, metadataId });
const wire = (aId: string, aPin: string, bId: string, bPin: string) => ({
start: { componentId: aId, pinName: aPin },
end: { componentId: bId, pinName: bPin },
});
const range = (n: number) => Array.from({ length: n }, (_, i) => i);
// A CPU chip and a ROM chip with D0..D7 wired straight across, no board.
function busState(): ChipNetState {
return {
wires: range(8).map((i) => wire('cpu', `D${i}`, 'rom', `D${i}`)),
components: [chip('cpu'), chip('rom')],
boards: [],
};
}
describe('chipbus Phase 0 — net-identity shared key', () => {
beforeEach(() => {
setChipBusEnabledForTest(true);
resetChipNetIndexForTest();
});
afterEach(() => {
setChipBusEnabledForTest(null);
resetChipNetIndexForTest();
});
it('two chips on one wire resolve to the SAME key (root cause A fixed)', () => {
const state = busState();
const kCpu = resolveChipNetKey(state, 'cpu', 'D0');
const kRom = resolveChipNetKey(state, 'rom', 'D0');
expect(kCpu).not.toBeNull();
expect(kCpu).toBe(kRom);
});
it('distinct data lines get distinct keys (no cross-talk between D0 and D1)', () => {
const state = busState();
expect(resolveChipNetKey(state, 'cpu', 'D0')).not.toBe(
resolveChipNetKey(state, 'cpu', 'D1'),
);
});
it('documents the bug: per-endpoint synthetic keys differ for one net', () => {
expect(syntheticChipPin('cpu', 'D0')).not.toBe(syntheticChipPin('rom', 'D0'));
});
it('byte exchange — a write on the driver is visible synchronously to the reader', () => {
const state = busState();
const pm = new PinManager();
// Reader (ROM) registers a watcher on EACH of its resolved data-bus keys,
// exactly as vx_pin_watch would after the net key fix.
let received = 0;
for (const i of range(8)) {
const key = resolveChipNetKey(state, 'rom', `D${i}`)!;
pm.onPinChange(key, (_p, s) => {
if (s) received |= 1 << i;
else received &= ~(1 << i);
});
}
// Driver (CPU) writes 0xA5 onto ITS resolved keys (vx_pin_write).
const byte = 0xa5;
for (const i of range(8)) {
const key = resolveChipNetKey(state, 'cpu', `D${i}`)!;
pm.triggerPinChange(key, ((byte >> i) & 1) === 1);
}
// The reader latched exactly the driver's byte, within the same call stack.
expect(received).toBe(0xa5);
});
it('the same key reads back the driven level via getPinState', () => {
const state = busState();
const pm = new PinManager();
const driveKey = resolveChipNetKey(state, 'cpu', 'D3')!;
const readKey = resolveChipNetKey(state, 'rom', 'D3')!;
pm.triggerPinChange(driveKey, true);
expect(pm.getPinState(readKey)).toBe(true);
});
it('flag OFF (default): chip-to-chip net is NOT collapsed (legacy path)', () => {
setChipBusEnabledForTest(false);
resetChipNetIndexForTest();
expect(resolveChipNetKey(busState(), 'cpu', 'D0')).toBeNull();
});
it('chip-to-component (single chip on net) returns null — rules 2/3 preserved', () => {
const state: ChipNetState = {
wires: [wire('chip', 'LED0', 'led1', 'A')],
components: [chip('chip'), part('led1', 'led')],
boards: [],
};
expect(resolveChipNetKey(state, 'chip', 'LED0')).toBeNull();
});
it('a board on the net defers to board priority (returns null)', () => {
const state: ChipNetState = {
wires: [
wire('cpu', 'D0', 'rom', 'D0'),
wire('cpu', 'D0', 'uno', '7'),
],
components: [chip('cpu'), chip('rom')],
boards: [{ id: 'uno', boardKind: 'arduino-uno' }],
};
expect(resolveChipNetKey(state, 'cpu', 'D0')).toBeNull();
});
it('three chips on one bus line all share one key', () => {
const state: ChipNetState = {
wires: [wire('cpu', 'D0', 'rom', 'D0'), wire('rom', 'D0', 'ram', 'D0')],
components: [chip('cpu'), chip('rom'), chip('ram')],
boards: [],
};
const a = resolveChipNetKey(state, 'cpu', 'D0');
const b = resolveChipNetKey(state, 'rom', 'D0');
const c = resolveChipNetKey(state, 'ram', 'D0');
expect(a).not.toBeNull();
expect(a).toBe(b);
expect(b).toBe(c);
});
});