111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
|
|
/**
|
|||
|
|
* Runtime burnout (P4) — pure decision + stress-computation logic.
|
|||
|
|
*/
|
|||
|
|
import { describe, it, expect } from 'vitest';
|
|||
|
|
import { componentStress, decideBurn } from '../simulation/parts/runtimeBurnout';
|
|||
|
|
|
|||
|
|
describe('decideBurn — thermal delay', () => {
|
|||
|
|
it('destroys instantly at a catastrophic ratio (>=3x)', () => {
|
|||
|
|
expect(decideBurn(5, 0, 1000).burn).toBe(true);
|
|||
|
|
});
|
|||
|
|
it('does not destroy on a first over-limit sample — starts the clock', () => {
|
|||
|
|
expect(decideBurn(1.5, 0, 1000)).toEqual({ burn: false, overSince: 1000 });
|
|||
|
|
});
|
|||
|
|
it('destroys once sustained over the limit past the delay', () => {
|
|||
|
|
expect(decideBurn(1.5, 1000, 1800).burn).toBe(true); // 800ms > 700ms
|
|||
|
|
});
|
|||
|
|
it('tolerates a brief spike (under the delay)', () => {
|
|||
|
|
expect(decideBurn(1.5, 1000, 1500).burn).toBe(false); // 500ms < 700ms
|
|||
|
|
});
|
|||
|
|
it('cools down (resets the clock) when back under the limit', () => {
|
|||
|
|
expect(decideBurn(0.5, 1000, 2000)).toEqual({ burn: false, overSince: 0 });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('componentStress — resistor power', () => {
|
|||
|
|
const pinNetMap = new Map([
|
|||
|
|
['r1:1', 'n0'],
|
|||
|
|
['r1:2', '0'],
|
|||
|
|
]);
|
|||
|
|
it('100Ω across 9V dissipates 0.81W → over the 2x-safety burn threshold', () => {
|
|||
|
|
const s = componentStress(
|
|||
|
|
{ id: 'r1', metadataId: 'resistor', properties: { value: '100' } },
|
|||
|
|
{ n0: 9, '0': 0 },
|
|||
|
|
pinNetMap,
|
|||
|
|
);
|
|||
|
|
expect(s).not.toBeNull();
|
|||
|
|
// 0.81W / (2 × 0.25W) = 1.62 → would burn after the sustained delay
|
|||
|
|
expect(s!.ratio).toBeCloseTo(1.62, 1);
|
|||
|
|
expect(s!.ratio).toBeGreaterThan(1);
|
|||
|
|
});
|
|||
|
|
it('1k across 9V is well within rating (no stress)', () => {
|
|||
|
|
const s = componentStress(
|
|||
|
|
{ id: 'r1', metadataId: 'resistor', properties: { value: '1k' } },
|
|||
|
|
{ n0: 9, '0': 0 },
|
|||
|
|
pinNetMap,
|
|||
|
|
);
|
|||
|
|
expect(s!.ratio).toBeLessThan(1);
|
|||
|
|
});
|
|||
|
|
it('respects a custom power property', () => {
|
|||
|
|
const s = componentStress(
|
|||
|
|
{ id: 'r1', metadataId: 'resistor', properties: { value: '100', power: 5 } },
|
|||
|
|
{ n0: 9, '0': 0 },
|
|||
|
|
pinNetMap,
|
|||
|
|
);
|
|||
|
|
expect(s!.ratio).toBeLessThan(1); // 0.81W / 5W
|
|||
|
|
});
|
|||
|
|
it('returns null when a terminal is not on a solved net', () => {
|
|||
|
|
const s = componentStress(
|
|||
|
|
{ id: 'r1', metadataId: 'resistor', properties: { value: '100' } },
|
|||
|
|
{ n0: 9 },
|
|||
|
|
new Map([['r1:1', 'n0']]), // pin 2 unwired
|
|||
|
|
);
|
|||
|
|
expect(s).toBeNull();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('componentStress — electrolytic capacitor', () => {
|
|||
|
|
it('9V across a 6.3V cap → over-voltage (>1x)', () => {
|
|||
|
|
const s = componentStress(
|
|||
|
|
{ id: 'c1', metadataId: 'capacitor-electrolytic', properties: { voltage: '6.3' } },
|
|||
|
|
{ n0: 9, '0': 0 },
|
|||
|
|
new Map([
|
|||
|
|
['c1:+', 'n0'],
|
|||
|
|
['c1:−', '0'],
|
|||
|
|
]),
|
|||
|
|
);
|
|||
|
|
expect(s!.ratio).toBeCloseTo(1.43, 1);
|
|||
|
|
});
|
|||
|
|
it('within rating → no stress', () => {
|
|||
|
|
const s = componentStress(
|
|||
|
|
{ id: 'c1', metadataId: 'capacitor-electrolytic', properties: { voltage: '25' } },
|
|||
|
|
{ n0: 5, '0': 0 },
|
|||
|
|
new Map([
|
|||
|
|
['c1:+', 'n0'],
|
|||
|
|
['c1:−', '0'],
|
|||
|
|
]),
|
|||
|
|
);
|
|||
|
|
expect(s!.ratio).toBeLessThan(1);
|
|||
|
|
});
|
|||
|
|
it('reverse polarity is catastrophic regardless of magnitude', () => {
|
|||
|
|
const s = componentStress(
|
|||
|
|
{ id: 'c1', metadataId: 'capacitor-electrolytic', properties: { voltage: '25' } },
|
|||
|
|
{ n0: 9, '0': 0 },
|
|||
|
|
new Map([
|
|||
|
|
['c1:+', '0'], // + at 0V, − at 9V → reverse-biased
|
|||
|
|
['c1:−', 'n0'],
|
|||
|
|
]),
|
|||
|
|
);
|
|||
|
|
expect(s!.ratio).toBeGreaterThanOrEqual(3);
|
|||
|
|
expect(s!.message).toMatch(/backwards|reverse/i);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('componentStress — non-monitored parts', () => {
|
|||
|
|
it('returns null for an LED (handled separately in BasicParts)', () => {
|
|||
|
|
expect(
|
|||
|
|
componentStress({ id: 'd1', metadataId: 'led', properties: {} }, {}, new Map()),
|
|||
|
|
).toBeNull();
|
|||
|
|
});
|
|||
|
|
});
|