mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-15 05:29:28 +00:00
ProvidersView.vue's accordion detail row hardcoded text-destructive red for any non-closed health state, while the trigger row's Badge deliberately used grey (secondary) for half-open and red (destructive) only for open. Add healthDetailClass so the detail row's color always matches the badge's own severity, instead of overstating a half-open probe as an error. Add a describe block for formatUntil to format.spec.ts, covering the unset sentinel, undefined, a future timestamp, and a past one. It had no test coverage since being added in the previous round.
167 lines
7.2 KiB
TypeScript
167 lines
7.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
|
||
import { formatCompactCount, formatContextWindow, formatModelCostHover, formatUntil, refreshDetailLabel, refreshLabel } from './format'
|
||
|
||
const ZERO_TIME = '0001-01-01T00:00:00Z'
|
||
|
||
describe('refreshLabel', () => {
|
||
it('reads "discovery off" when discovery is disabled, regardless of lastRefresh', () => {
|
||
expect(refreshLabel(false, ZERO_TIME)).toBe('discovery off')
|
||
expect(refreshLabel(false, new Date().toISOString())).toBe('discovery off')
|
||
expect(refreshLabel(false, undefined)).toBe('discovery off')
|
||
})
|
||
|
||
it('reads "pending" when discovery is enabled but lastRefresh is still the unset sentinel', () => {
|
||
expect(refreshLabel(true, ZERO_TIME)).toBe('pending')
|
||
})
|
||
|
||
it('reads "pending" when discovery is enabled and lastRefresh is undefined (edge case)', () => {
|
||
expect(refreshLabel(true, undefined)).toBe('pending')
|
||
})
|
||
|
||
it('reads the relative-time "refreshed (Ns ago)" text once discovery has completed a refresh', () => {
|
||
const oneMinuteAgo = new Date(Date.now() - 60_000).toISOString()
|
||
expect(refreshLabel(true, oneMinuteAgo)).toMatch(/^refreshed \(\d+s ago\)$/)
|
||
})
|
||
})
|
||
|
||
// TestAdminOverview_ProviderLastErrAfterFailedRefresh's webui counterpart:
|
||
// the accordion DETAIL row (folded review minor, v0.22 review round) must
|
||
// share refreshLabel's own three-state logic instead of calling
|
||
// formatTimestamp directly — the bug this closes was a discovery-off
|
||
// provider's expanded row still reading the raw "never" right next to a
|
||
// trigger row that correctly said "discovery off".
|
||
describe('refreshDetailLabel', () => {
|
||
it('reads "discovery off" when discovery is disabled, regardless of lastRefresh', () => {
|
||
expect(refreshDetailLabel(false, ZERO_TIME)).toBe('discovery off')
|
||
expect(refreshDetailLabel(false, undefined)).toBe('discovery off')
|
||
})
|
||
|
||
it('reads "pending" when discovery is enabled but lastRefresh is still the unset sentinel', () => {
|
||
expect(refreshDetailLabel(true, ZERO_TIME)).toBe('pending')
|
||
})
|
||
|
||
it('reads the full formatTimestamp date/time once discovery has completed a refresh, not a relative "(Ns ago)"', () => {
|
||
const iso = new Date(2026, 7, 21, 12, 0, 0).toISOString()
|
||
const got = refreshDetailLabel(true, iso)
|
||
expect(got).not.toMatch(/ago/)
|
||
expect(got).not.toBe('pending')
|
||
expect(got).not.toBe('discovery off')
|
||
})
|
||
})
|
||
|
||
// Feature v0.23: per-model metadata exposure (context window, per-token
|
||
// cost). formatContextWindow is table-driven over good/edge cases;
|
||
// formatModelCostHover covers the free/priced split ModelChip's
|
||
// hover-detail relies on.
|
||
//
|
||
// Review fix: binary-K/M (÷1024) applies ONLY when tokens is an exact
|
||
// power of two; a decimal, round-thousands count uses decimal-K/M
|
||
// (÷1000) instead — see formatContextWindow's own doc comment for why a
|
||
// plain "tokens % 1024 == 0" check cannot tell these two cases apart
|
||
// (128000 divides evenly by 1024 too, yet must render as decimal "128k",
|
||
// not binary "125k").
|
||
describe('formatContextWindow', () => {
|
||
it.each([
|
||
// Genuine binary sizes (exact powers of two) — binary-K/M.
|
||
{ tokens: 262144, want: '256k' }, // 2^18 — LM Studio's own worked example
|
||
{ tokens: 131072, want: '128k' }, // 2^17
|
||
{ tokens: 32768, want: '32k' }, // 2^15
|
||
{ tokens: 8192, want: '8k' }, // 2^13
|
||
{ tokens: 1024, want: '1k' }, // 2^10 — boundary
|
||
{ tokens: 1_048_576, want: '1M' }, // 2^20 — binary M-tier boundary
|
||
// Decimal, marketing-style round counts — NOT powers of two, even
|
||
// when they happen to be exact multiples of 1024 (128000 = 125 ×
|
||
// 1024) — decimal-K/M.
|
||
{ tokens: 128_000, want: '128k' }, // review regression: was "125k"
|
||
{ tokens: 1_000_000, want: '1M' }, // review regression: was "977k"
|
||
{ tokens: 2_000_000, want: '2M' }, // review regression: was "1953k"
|
||
// Plain digits under 1024, either path.
|
||
{ tokens: 512, want: '512' },
|
||
{ tokens: 0, want: '0' },
|
||
])('formatContextWindow($tokens) = $want', ({ tokens, want }) => {
|
||
expect(formatContextWindow(tokens)).toBe(want)
|
||
})
|
||
})
|
||
|
||
describe('formatModelCostHover', () => {
|
||
it('renders "free" when both sides are exactly 0', () => {
|
||
expect(formatModelCostHover(0, 0)).toBe('free')
|
||
})
|
||
|
||
it('renders "in $X / out $Y per MTok" for a priced model', () => {
|
||
expect(formatModelCostHover(1.25, 10)).toBe('in $1.25 / out $10.00 per MTok')
|
||
})
|
||
|
||
it('renders a sub-dollar price with two decimals', () => {
|
||
expect(formatModelCostHover(0.19, 0.51)).toBe('in $0.19 / out $0.51 per MTok')
|
||
})
|
||
})
|
||
|
||
// Discovery circuit breaker (feat/provider-health, round 3): openUntil's
|
||
// future-time counterpart to formatAgo's past-time "(Ns ago)". Only
|
||
// caller today is ProvidersView.vue's healthBadgeLabel, rendering the
|
||
// breaker's live "open (in Ns)" countdown.
|
||
describe('formatUntil', () => {
|
||
it('renders "" for the unset zero-time sentinel', () => {
|
||
expect(formatUntil(ZERO_TIME)).toBe('')
|
||
})
|
||
|
||
it('renders "" for undefined', () => {
|
||
expect(formatUntil(undefined)).toBe('')
|
||
})
|
||
|
||
it('renders "in Ns" for a future timestamp', () => {
|
||
const inFortyTwoSeconds = new Date(Date.now() + 42_000).toISOString()
|
||
expect(formatUntil(inFortyTwoSeconds)).toMatch(/^in \d+s$/)
|
||
})
|
||
|
||
it('renders "" for a timestamp already in the past — a stale poll must not show a negative countdown', () => {
|
||
const oneMinuteAgo = new Date(Date.now() - 60_000).toISOString()
|
||
expect(formatUntil(oneMinuteAgo)).toBe('')
|
||
})
|
||
})
|
||
|
||
// Usage-count compact display (v0.23 addendum): SI-style, 3 significant
|
||
// figures, floor-not-round (the same honesty rule formatRatePercent
|
||
// already applies to the success-rate badge — a count must never appear
|
||
// to have crossed a boundary it has not actually reached).
|
||
describe('formatCompactCount', () => {
|
||
it.each([
|
||
// Below 10,000: exact integer with thousands separators.
|
||
{ n: 0, want: '0' },
|
||
{ n: 999, want: '999' },
|
||
{ n: 1_000, want: '1,000' }, // the 999/1000 boundary
|
||
{ n: 9_999, want: '9,999' },
|
||
// k tier.
|
||
{ n: 10_000, want: '10k' },
|
||
{ n: 12_345, want: '12.3k' },
|
||
{ n: 456_789, want: '456k' },
|
||
{ n: 999_949, want: '999k' }, // floor, not round: stays 999k
|
||
{ n: 999_950, want: '999k' }, // the classic round-up edge — floor must NOT bump this to 1,000k
|
||
{ n: 999_999, want: '999k' },
|
||
// M tier.
|
||
{ n: 1_000_000, want: '1M' },
|
||
{ n: 1_234_567, want: '1.23M' },
|
||
{ n: 45_600_000, want: '45.6M' },
|
||
{ n: 999_999_999, want: '999M' }, // just under the 1e9 boundary
|
||
// Review fix: IEEE-754 precision regressions. 8_700_000 / 1_000_000
|
||
// is not exactly representable in binary floating point (the double
|
||
// closest to 8.7 sits a hair BELOW it); the old
|
||
// `Math.floor(scaled*factor)/factor` implementation floored that
|
||
// slightly-under value to 869 instead of 870, rendering "8.69M".
|
||
{ n: 8_700_000, want: '8.7M' },
|
||
{ n: 1_130_000, want: '1.13M' },
|
||
// B tier.
|
||
{ n: 1_000_000_000, want: '1B' }, // the 1e9 boundary itself
|
||
{ n: 1_230_000_000, want: '1.23B' },
|
||
{ n: 12_345_000_000, want: '12.3B' },
|
||
])('formatCompactCount($n) = $want', ({ n, want }) => {
|
||
expect(formatCompactCount(n)).toBe(want)
|
||
})
|
||
|
||
it('preserves the sign for a negative count (defensive — counts are never negative in practice)', () => {
|
||
expect(formatCompactCount(-15_000_000)).toBe('-15M')
|
||
})
|
||
})
|