glytch/fktry/src/ui/power.test.ts
type-two 3cec62368a [ui] fix: BROWNOUT was lying about a covered deficit
The banner keyed on `out || ratio > 1`, so any time draw exceeded generation
the panel screamed BROWNOUT — even while the buffer tanks were quietly
covering it, the sim's flag was false and THE SCREEN was calm. That state is
the factory working as designed, not failing. Only the HUD was panicking.

BROWNOUT is now the sim's flag and nothing else. A covered deficit gets its
own amber, steady, non-flashing state: "ON RESERVE · 34s", where the seconds
are stored/(draw-gen) — real arithmetic, because v3 ruled stored is
bandwidth-SECONDS and gen/draw are per-second. Same maths SCREEN uses.

Four states (ok / tight / reserve / brownout) now live in a pure power.ts and
are pinned by tests, including the exact case that lied: gen 100, draw 134,
stored 1000, brownout false -> reserve, not brownout.

Also uses v4 bandwidth.capacity to turn the buffer readout into a fuel gauge
(0/900) rather than a bare number.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:13:23 +10:00

78 lines
3.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { SimSnapshot } from '../contracts';
import { formatReserve, powerState, TIGHT_AT } from './power';
type BW = SimSnapshot['bandwidth'];
const bw = (over: Partial<BW> = {}): BW =>
({ gen: 100, draw: 50, stored: 0, brownout: false, ...over });
describe('powerState', () => {
it('is ok with headroom', () => {
expect(powerState(bw()).state).toBe('ok');
});
it('goes tight at 85% of generation, before anything is wrong', () => {
expect(powerState(bw({ draw: 84 })).state).toBe('ok');
expect(powerState(bw({ draw: TIGHT_AT * 100 })).state).toBe('tight');
expect(powerState(bw({ draw: 99 })).state).toBe('tight');
});
// ---- the round-3 bug, pinned ------------------------------------------------
it('a covered deficit is ON RESERVE, not a brownout', () => {
// draw exceeds gen, tanks have charge, and the sim says no brownout.
// The old code keyed on `out || ratio > 1` and screamed BROWNOUT at this.
const p = powerState(bw({ gen: 100, draw: 134, stored: 1000, brownout: false }));
expect(p.state).toBe('reserve');
expect(p.state).not.toBe('brownout');
});
it('reports seconds of reserve left, because stored is bandwidth-SECONDS', () => {
// 1000 bandwidth-seconds stored, bleeding 34/sec => ~29s
const p = powerState(bw({ gen: 100, draw: 134, stored: 1000, brownout: false }));
expect(p.reserveSeconds).toBeCloseTo(1000 / 34, 5);
});
it('only the sim gets to say BROWNOUT', () => {
// Same covered-deficit numbers, but the sim has raised the flag: believe the flag.
expect(powerState(bw({ gen: 100, draw: 134, stored: 1000, brownout: true })).state)
.toBe('brownout');
// ...and a brownout at nominal draw is still a brownout.
expect(powerState(bw({ gen: 100, draw: 1, brownout: true })).state).toBe('brownout');
});
it('treats an exhausted reserve honestly rather than rounding it up', () => {
const p = powerState(bw({ gen: 10, draw: 20, stored: 0, brownout: false }));
expect(p.state).toBe('reserve');
expect(p.reserveSeconds).toBe(0); // about to become the sim's problem
});
it('handles drawing with no generation at all', () => {
const p = powerState(bw({ gen: 0, draw: 4, stored: 40, brownout: false }));
expect(p.state).toBe('reserve');
expect(p.ratio).toBe(Infinity);
expect(p.reserveSeconds).toBe(10);
});
it('is ok on an empty floor — no generation, no draw', () => {
const p = powerState(bw({ gen: 0, draw: 0 }));
expect(p.state).toBe('ok');
expect(p.ratio).toBe(0);
});
it('exactly break-even is not a deficit', () => {
expect(powerState(bw({ gen: 100, draw: 100, stored: 5 })).state).toBe('tight');
});
});
describe('formatReserve', () => {
it('floors, because a countdown that rounds up lies at the end', () => {
expect(formatReserve(29.9)).toBe('29s');
expect(formatReserve(0.4)).toBe('0s');
});
it('caps absurd numbers instead of printing a wall of digits', () => {
expect(formatReserve(100000)).toBe('999s');
expect(formatReserve(Infinity)).toBe('∞');
});
});