2026-05-13 02:55:15 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* circuitVerifier — unit tests against hand-crafted netlists that
|
|
|
|
|
|
* deliberately violate each safety rule.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Each test feeds the verifier the *normalised* shape that the live store
|
|
|
|
|
|
* already uses (components + wires arrays), runs an actual ngspice solve,
|
|
|
|
|
|
* and asserts that the right warning code surfaces.
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
|
import { verifyCircuit } from '../simulation/verify/circuitVerifier';
|
|
|
|
|
|
import type { BuildNetlistInput } from '../simulation/spice/types';
|
|
|
|
|
|
|
|
|
|
|
|
// ── Building blocks ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function pwr(id = 'src', volts = 5): BuildNetlistInput['components'][number] {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id,
|
|
|
|
|
|
metadataId: 'signal-generator',
|
|
|
|
|
|
properties: { waveform: 'dc', offset: volts, amplitude: 0, frequency: 1 },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function res(id: string, ohms: string): BuildNetlistInput['components'][number] {
|
|
|
|
|
|
return { id, metadataId: 'resistor', properties: { value: ohms } };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function led(id: string, color = 'red'): BuildNetlistInput['components'][number] {
|
|
|
|
|
|
return { id, metadataId: 'led', properties: { color } };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function w(
|
|
|
|
|
|
id: string,
|
|
|
|
|
|
from: [string, string],
|
|
|
|
|
|
to: [string, string],
|
|
|
|
|
|
): BuildNetlistInput['wires'][number] {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id,
|
|
|
|
|
|
start: { componentId: from[0], pinName: from[1] },
|
|
|
|
|
|
end: { componentId: to[0], pinName: to[1] },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe('verifyCircuit — clean circuits report no errors', () => {
|
|
|
|
|
|
it(
|
|
|
|
|
|
'a 220Ω + red LED across 5 V is fine',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src'), res('r1', '220'), led('led1')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['r1', '1']),
|
|
|
|
|
|
w('w2', ['r1', '2'], ['led1', 'A']),
|
|
|
|
|
|
w('w3', ['led1', 'C'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
expect(result.errors, JSON.stringify(result.errors)).toEqual([]);
|
|
|
|
|
|
// No "led-no-current" warning either — 5V/220Ω is well above µA.
|
|
|
|
|
|
expect(
|
|
|
|
|
|
result.warnings.filter((w) => w.code !== 'solver-failed'),
|
|
|
|
|
|
).toEqual([]);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('verifyCircuit — short circuit detection', () => {
|
|
|
|
|
|
it(
|
|
|
|
|
|
'fires an error when 5V is wired straight to GND',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
// Even with ESR-zero this represents a dead short — SPICE pushes huge
|
|
|
|
|
|
// current through the source. Use a tiny series R so SPICE doesn't
|
|
|
|
|
|
// produce a singular matrix, but well below 1Ω.
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src'), res('rShort', '0.01')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['rShort', '1']),
|
|
|
|
|
|
w('w2', ['rShort', '2'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const codes = result.errors.map((e) => e.code);
|
|
|
|
|
|
expect(codes, JSON.stringify(result.errors)).toContain('short-circuit');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('verifyCircuit — LED overcurrent', () => {
|
|
|
|
|
|
it(
|
|
|
|
|
|
'fires an error when an LED is wired with too small a series resistor',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
// 5V → 10Ω → LED (Vf ≈ 2V) → GND. I ≈ (5-2)/10 = 300 mA — well above
|
|
|
|
|
|
// the 20 mA absolute maximum.
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src'), res('r1', '10'), led('led1')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['r1', '1']),
|
|
|
|
|
|
w('w2', ['r1', '2'], ['led1', 'A']),
|
|
|
|
|
|
w('w3', ['led1', 'C'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const codes = result.errors.map((e) => e.code);
|
|
|
|
|
|
expect(codes, JSON.stringify(result.errors)).toContain('led-overcurrent');
|
|
|
|
|
|
// The LED itself should be tagged.
|
|
|
|
|
|
const ledErr = result.errors.find((e) => e.code === 'led-overcurrent');
|
|
|
|
|
|
expect(ledErr?.componentId).toBe('led1');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('verifyCircuit — resistor over-power', () => {
|
|
|
|
|
|
it(
|
|
|
|
|
|
'fires a warning when a small resistor across 5 V dissipates too much',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
// 5V across 10Ω → I = 0.5 A → P = 2.5 W. Way past the 1/4 W default.
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src'), res('rHot', '10')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['rHot', '1']),
|
|
|
|
|
|
w('w2', ['rHot', '2'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const warnCodes = result.warnings.map((e) => e.code);
|
|
|
|
|
|
// Resistor over-power is a non-blocking warning (real-world physical
|
|
|
|
|
|
// concern, not a sim error). Short-circuit and other safety issues
|
|
|
|
|
|
// still hit `errors`.
|
|
|
|
|
|
expect(warnCodes, JSON.stringify(result.warnings)).toContain('resistor-overpower');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'respects a custom power property on the resistor',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
// 5V across 10Ω with explicit 5W rating → no overpower warning.
|
|
|
|
|
|
const r: BuildNetlistInput['components'][number] = {
|
|
|
|
|
|
id: 'rBig',
|
|
|
|
|
|
metadataId: 'resistor',
|
|
|
|
|
|
properties: { value: '10', power: 5 },
|
|
|
|
|
|
};
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src'), r],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['rBig', '1']),
|
|
|
|
|
|
w('w2', ['rBig', '2'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const overpowerErr = result.errors.find((e) => e.code === 'resistor-overpower');
|
|
|
|
|
|
const overpowerWarn = result.warnings.find((e) => e.code === 'resistor-overpower');
|
|
|
|
|
|
expect(overpowerErr, JSON.stringify(result.errors)).toBeUndefined();
|
|
|
|
|
|
expect(overpowerWarn, JSON.stringify(result.warnings)).toBeUndefined();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('verifyCircuit — threshold overrides', () => {
|
|
|
|
|
|
it(
|
|
|
|
|
|
'bumping shortCircuitAmps suppresses the short-circuit error',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src'), res('rShort', '0.01')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['rShort', '1']),
|
|
|
|
|
|
w('w2', ['rShort', '2'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input, { shortCircuitAmps: 1000 });
|
|
|
|
|
|
const codes = result.errors.map((e) => e.code);
|
|
|
|
|
|
expect(codes).not.toContain('short-circuit');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-18 03:40:03 +07:00
|
|
|
|
describe('verifyCircuit — over-voltage on rated parts', () => {
|
|
|
|
|
|
function part(id: string, metadataId: string): BuildNetlistInput['components'][number] {
|
|
|
|
|
|
return { id, metadataId, properties: {} };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'warns when a 3.3-5V module (SSD1306 VIN) is fed 9 V',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src', 9), part('oled1', 'ssd1306')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['oled1', 'VIN']),
|
|
|
|
|
|
w('w2', ['oled1', 'GND'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const ov = result.warnings.find((x) => x.code === 'over-voltage');
|
|
|
|
|
|
expect(ov, JSON.stringify(result.warnings)).toBeDefined();
|
|
|
|
|
|
expect(ov?.componentId).toBe('oled1');
|
|
|
|
|
|
// over-voltage is non-blocking
|
|
|
|
|
|
expect(result.errors.map((e) => e.code)).not.toContain('over-voltage');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'does NOT warn when the same module is fed a safe 5 V on VIN',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src', 5), part('oled2', 'ssd1306')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['oled2', 'VIN']),
|
|
|
|
|
|
w('w2', ['oled2', 'GND'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
expect(result.warnings.map((x) => x.code)).not.toContain('over-voltage');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'warns when a strict 3.3 V pin (SSD1306 3V3) sits on a 5 V rail',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
// VCC-like pin names (VCC/VDD/3V3/5V) canonicalise to the shared
|
|
|
|
|
|
// `vcc_rail` net, which defaults to 5 V. A 10k load gives the rail a
|
|
|
|
|
|
// real path to ground so the .op solves. The OLED's 3V3 pin (abs max
|
|
|
|
|
|
// 3.6 V) on that 5 V rail must warn.
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [part('oled3', 'ssd1306'), res('rl', '10k')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['oled3', '3V3'], ['rl', '1']),
|
|
|
|
|
|
w('w2', ['rl', '2'], ['oled3', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const ov = result.warnings.find((x) => x.code === 'over-voltage');
|
|
|
|
|
|
expect(ov, JSON.stringify(result.warnings)).toBeDefined();
|
|
|
|
|
|
expect(ov?.componentId).toBe('oled3');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-18 06:19:13 +07:00
|
|
|
|
describe('verifyCircuit — board over-voltage (graph-based)', () => {
|
|
|
|
|
|
function board(id: string, boardKind: string): BuildNetlistInput['boards'][number] {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id,
|
|
|
|
|
|
boardKind,
|
|
|
|
|
|
vcc: 3.3,
|
|
|
|
|
|
pins: {},
|
|
|
|
|
|
groundPinNames: ['GND', 'GND.1', 'GND.2'],
|
|
|
|
|
|
vccPinNames: ['3V3', 'VIN', '5V'],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'warns when a 9 V battery is wired to an ESP32 VIN pin',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [{ id: 'bat', metadataId: 'battery-9v', properties: {} }],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
{ id: 'w1', start: { componentId: 'bat', pinName: '+' }, end: { componentId: 'esp32', pinName: 'VIN' } },
|
|
|
|
|
|
{ id: 'w2', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'bat', pinName: '−' } },
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [board('esp32', 'esp32')],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const ov = result.warnings.find((w) => w.code === 'over-voltage' && w.componentId === 'esp32');
|
|
|
|
|
|
expect(ov, JSON.stringify(result.warnings)).toBeDefined();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'does NOT warn when an AA battery (1.5 V) powers the VIN pin',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [{ id: 'bat', metadataId: 'battery-aa', properties: {} }],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
{ id: 'w1', start: { componentId: 'bat', pinName: '+' }, end: { componentId: 'esp32', pinName: 'VIN' } },
|
|
|
|
|
|
{ id: 'w2', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'bat', pinName: '−' } },
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [board('esp32', 'esp32')],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
expect(result.warnings.map((w) => w.code)).not.toContain('over-voltage');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('verifyCircuit — electrolytic capacitor', () => {
|
|
|
|
|
|
function cap(id: string, voltage: string): BuildNetlistInput['components'][number] {
|
|
|
|
|
|
return { id, metadataId: 'capacitor-electrolytic', properties: { value: '10u', voltage } };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'warns when the voltage across it exceeds its rating',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
// 24 V across a 16 V cap.
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src', 24), cap('c1', '16')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['c1', '+']),
|
|
|
|
|
|
w('w2', ['c1', '−'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const ov = result.warnings.find((x) => x.code === 'over-voltage' && x.componentId === 'c1');
|
|
|
|
|
|
expect(ov, JSON.stringify(result.warnings)).toBeDefined();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'does NOT warn when within its rating',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src', 5), cap('c2', '25')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['c2', '+']),
|
|
|
|
|
|
w('w2', ['c2', '−'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
expect(result.warnings.map((x) => x.code)).not.toContain('over-voltage');
|
|
|
|
|
|
expect(result.warnings.map((x) => x.code)).not.toContain('reverse-polarity');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'warns on reverse polarity',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
// +5 V on the minus pin, plus pin to ground → reverse-biased.
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src', 5), cap('c3', '25')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['c3', '−']),
|
|
|
|
|
|
w('w2', ['c3', '+'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const rp = result.warnings.find((x) => x.code === 'reverse-polarity' && x.componentId === 'c3');
|
|
|
|
|
|
expect(rp, JSON.stringify(result.warnings)).toBeDefined();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-18 07:05:42 +07:00
|
|
|
|
describe('verifyCircuit — wiring ERC (bad connections)', () => {
|
|
|
|
|
|
it(
|
|
|
|
|
|
'warns when a 2-terminal part is connected on only one side',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src', 5), res('r1', '1k')],
|
|
|
|
|
|
wires: [w('w1', ['src', 'SIG'], ['r1', '1'])], // r1 pin '2' left floating
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const mc = result.warnings.find((x) => x.code === 'missing-connection' && x.componentId === 'r1');
|
|
|
|
|
|
expect(mc, JSON.stringify(result.warnings)).toBeDefined();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'does NOT warn when both terminals are wired',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src', 5), res('r1', '1k')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['r1', '1']),
|
|
|
|
|
|
w('w2', ['r1', '2'], ['src', 'GND']),
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
expect(result.warnings.map((x) => x.code)).not.toContain('missing-connection');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'warns when a powered module is missing its ground connection',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src', 5), { id: 'o1', metadataId: 'ssd1306', properties: {} }],
|
|
|
|
|
|
wires: [w('w1', ['src', 'SIG'], ['o1', 'VIN'])], // VIN wired, GND not
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const mc = result.warnings.find((x) => x.code === 'missing-connection' && x.componentId === 'o1');
|
|
|
|
|
|
expect(mc, JSON.stringify(result.warnings)).toBeDefined();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-06-18 08:32:17 +07:00
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'errors when a VCC pin is wired directly to a GND pin',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [{ id: 'o1', metadataId: 'ssd1306', properties: {} }],
|
|
|
|
|
|
wires: [w('w1', ['o1', '3V3'], ['o1', 'GND'])], // VCC tied straight to GND
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const codes = result.errors.map((e) => e.code);
|
|
|
|
|
|
expect(codes, JSON.stringify(result.errors)).toContain('power-short');
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'warns when a part has both terminals on the same node (shorted out)',
|
|
|
|
|
|
{ timeout: 30_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const input: BuildNetlistInput = {
|
|
|
|
|
|
components: [pwr('src', 5), res('r1', '1k')],
|
|
|
|
|
|
wires: [
|
|
|
|
|
|
w('w1', ['src', 'SIG'], ['r1', '1']),
|
|
|
|
|
|
w('w2', ['src', 'SIG'], ['r1', '2']), // both terminals on the SIG net
|
|
|
|
|
|
],
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await verifyCircuit(input);
|
|
|
|
|
|
const sc = result.warnings.find((x) => x.code === 'shorted-component' && x.componentId === 'r1');
|
|
|
|
|
|
expect(sc, JSON.stringify(result.warnings)).toBeDefined();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-06-18 07:05:42 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-13 02:55:15 +07:00
|
|
|
|
// ── Sanity: shipping examples never trigger errors ─────────────────────────
|
|
|
|
|
|
// If any gallery example produces a verifier error, that's a bug in the
|
|
|
|
|
|
// example itself. Loop a handful of representative ones to catch
|
|
|
|
|
|
// regressions early.
|
|
|
|
|
|
import { digitalExamples } from '../data/examples-digital';
|
|
|
|
|
|
import { analogExamples } from '../data/examples-analog';
|
|
|
|
|
|
|
|
|
|
|
|
function toInput(ex: { components: any[]; wires: any[] }): BuildNetlistInput {
|
|
|
|
|
|
return {
|
|
|
|
|
|
components: ex.components.map((c: any) => ({
|
|
|
|
|
|
id: c.id,
|
|
|
|
|
|
metadataId: c.type.replace(/^(wokwi|velxio)-/, ''),
|
|
|
|
|
|
properties: c.properties ?? {},
|
|
|
|
|
|
})),
|
|
|
|
|
|
wires: ex.wires.map((wire: any) => ({
|
|
|
|
|
|
id: wire.id,
|
|
|
|
|
|
start: { componentId: wire.start.componentId, pinName: wire.start.pinName },
|
|
|
|
|
|
end: { componentId: wire.end.componentId, pinName: wire.end.pinName },
|
|
|
|
|
|
})),
|
|
|
|
|
|
boards: [],
|
|
|
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
describe('verifyCircuit — shipping gallery examples are clean', () => {
|
|
|
|
|
|
it(
|
|
|
|
|
|
'every digital example passes pre-flight verification',
|
|
|
|
|
|
{ timeout: 180_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const failures: string[] = [];
|
|
|
|
|
|
for (const ex of digitalExamples) {
|
|
|
|
|
|
const result = await verifyCircuit(toInput(ex));
|
|
|
|
|
|
if (result.errors.length > 0) {
|
|
|
|
|
|
failures.push(
|
|
|
|
|
|
`${ex.id}: ${result.errors.map((e) => `${e.code}(${e.componentId ?? '-'})`).join(', ')}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
expect(failures, failures.join('\n')).toEqual([]);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
'every analog example passes pre-flight verification',
|
|
|
|
|
|
{ timeout: 180_000 },
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const failures: string[] = [];
|
|
|
|
|
|
for (const ex of analogExamples) {
|
|
|
|
|
|
const result = await verifyCircuit(toInput(ex));
|
|
|
|
|
|
if (result.errors.length > 0) {
|
|
|
|
|
|
failures.push(
|
|
|
|
|
|
`${ex.id}: ${result.errors.map((e) => `${e.code}(${e.componentId ?? '-'})`).join(', ')}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
expect(failures, failures.join('\n')).toEqual([]);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|