velxio/frontend/src/__tests__/runtime-burnout.test.ts

111 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();
});
});