Skip to content

Commit dfdad9b

Browse files
committed
test(latest-image+use-feature-gate): extract pure helpers and add 29 unit tests
1 parent abf7f9f commit dfdad9b

4 files changed

Lines changed: 362 additions & 67 deletions

File tree

packages/app/src/components/latest-image/latest-image-content.tsx

Lines changed: 9 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ import {
1616
import { TooltipProvider } from '@/components/ui/tooltip';
1717
import { useFrameworkReleases } from '@/hooks/api/use-framework-releases';
1818
import { useLatestImages } from '@/hooks/api/use-latest-images';
19-
import type { FrameworkReleases, LatestImageRow } from '@/lib/api';
19+
import type { LatestImageRow } from '@/lib/api';
2020
import { track } from '@/lib/analytics';
2121
import { getFrameworkLabel } from '@/lib/utils';
22-
23-
/** Map framework variants to their base framework for release lookup. */
24-
const FRAMEWORK_TO_BASE: Record<string, string> = {
25-
vllm: 'vllm',
26-
sglang: 'sglang',
27-
'dynamo-sglang': 'sglang',
28-
'mori-sglang': 'sglang',
29-
};
22+
import {
23+
AGE_MAX_RED_DAYS,
24+
ageColorStyle,
25+
ageRowStyle,
26+
daysSince,
27+
getActualLatestTag,
28+
isOutdated,
29+
} from './latest-image-utils';
3030

3131
/**
3232
* Disaggregated frameworks pair a separate prefill/decode pool — identified by
@@ -83,64 +83,6 @@ function formatSpecMethod(method: string) {
8383
return method === 'none' ? 'Off' : method.toUpperCase();
8484
}
8585

86-
function getActualLatestTag(framework: string, releases: FrameworkReleases | undefined) {
87-
if (!releases) return null;
88-
const base = FRAMEWORK_TO_BASE[framework];
89-
if (!base) return null;
90-
return releases[base] ?? null;
91-
}
92-
93-
const UNSTABLE_PATTERNS = ['nightly', 'rocm/sgl-dev', 'sglang-rocm'];
94-
95-
/** Whole-day delta between today (UTC) and an ISO date string (YYYY-MM-DD). */
96-
function daysSince(dateStr: string, today: Date): number {
97-
const submitted = new Date(`${dateStr}T00:00:00Z`).getTime();
98-
const ms = today.getTime() - submitted;
99-
return Math.max(0, Math.floor(ms / 86_400_000));
100-
}
101-
102-
/** Age past which the cell is rendered at max red — anything older looks identical. */
103-
const AGE_MAX_RED_DAYS = 60;
104-
105-
/**
106-
* Returns inline style for the Days-Since-Update cell so older rows scream
107-
* louder visually. Ramps from a subtle red at 1 day to deep red at 60 days
108-
* (then clamps); 0-day rows return `undefined` so the cell falls back to the
109-
* muted-foreground class.
110-
*/
111-
function ageColorStyle(days: number): React.CSSProperties | undefined {
112-
if (days < 1) return undefined;
113-
const t = Math.min(AGE_MAX_RED_DAYS, days) / AGE_MAX_RED_DAYS; // 0.017 … 1
114-
// Perceptually-uniform OKLCH ramp at hue 25 (red): lightness drops as
115-
// chroma rises, so the cell goes from light pink to saturated dark red.
116-
const L = 0.78 - 0.28 * t;
117-
const C = 0.12 + 0.12 * t;
118-
return { color: `oklch(${L.toFixed(3)} ${C.toFixed(3)} 25)` };
119-
}
120-
121-
/**
122-
* Companion to `ageColorStyle` for the whole row's background tint — same
123-
* 1d → 60d ramp but expressed as a low-alpha fill so the row content stays
124-
* readable. 0-day rows return `undefined` so the row falls back to its
125-
* hover-only class background.
126-
*/
127-
function ageRowStyle(days: number): React.CSSProperties | undefined {
128-
if (days < 1) return undefined;
129-
const t = Math.min(AGE_MAX_RED_DAYS, days) / AGE_MAX_RED_DAYS;
130-
// Alpha tops out around 0.28 — enough that 60d+ rows are unmistakably
131-
// tinted without drowning out the text or competing with hover affordance.
132-
const alpha = (0.04 + 0.24 * t).toFixed(3);
133-
return { backgroundColor: `oklch(0.60 0.22 25 / ${alpha})` };
134-
}
135-
136-
/** Check if the image tag is outdated or uses an unstable/dev image. */
137-
function isOutdated(image: string, actualLatest: string | null): boolean {
138-
const lower = image.toLowerCase();
139-
if (UNSTABLE_PATTERNS.some((p) => lower.includes(p))) return true;
140-
if (!actualLatest) return false;
141-
return !image.includes(actualLatest);
142-
}
143-
14486
export function CurrentImageContent() {
14587
const { data, isLoading, error } = useLatestImages();
14688
const { data: releases } = useFrameworkReleases();
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import {
4+
AGE_MAX_RED_DAYS,
5+
ageColorStyle,
6+
ageRowStyle,
7+
daysSince,
8+
getActualLatestTag,
9+
isOutdated,
10+
} from './latest-image-utils';
11+
12+
const lightnessOf = (s: string) => Number(s.match(/oklch\(([\d.]+)/u)?.[1] ?? Number.NaN);
13+
const alphaOf = (s: string) => Number(s.match(/\/ ([\d.]+)\)/u)?.[1] ?? Number.NaN);
14+
15+
describe('daysSince', () => {
16+
it('returns 0 for today', () => {
17+
const today = new Date('2026-05-27T12:34:56Z');
18+
expect(daysSince('2026-05-27', today)).toBe(0);
19+
});
20+
21+
it('returns whole days for dates strictly in the past', () => {
22+
const today = new Date('2026-05-27T00:00:00Z');
23+
expect(daysSince('2026-05-26', today)).toBe(1);
24+
expect(daysSince('2026-05-20', today)).toBe(7);
25+
expect(daysSince('2026-03-28', today)).toBe(60);
26+
});
27+
28+
it('floors to whole days regardless of intraday hour offset', () => {
29+
// The submitted-day anchor is 00:00Z; an early-morning "today" still
30+
// belongs to the same UTC day, so 23:59 vs 00:01 must round identically.
31+
const today = new Date('2026-05-28T00:01:00Z');
32+
expect(daysSince('2026-05-27', today)).toBe(1);
33+
const lateToday = new Date('2026-05-28T23:59:00Z');
34+
expect(daysSince('2026-05-27', lateToday)).toBe(1);
35+
});
36+
37+
it('clamps at 0 for future-dated submissions (never returns negative)', () => {
38+
const today = new Date('2026-05-27T00:00:00Z');
39+
expect(daysSince('2026-06-01', today)).toBe(0);
40+
});
41+
});
42+
43+
describe('ageColorStyle', () => {
44+
it('returns undefined for 0-day rows so the muted-foreground class wins', () => {
45+
expect(ageColorStyle(0)).toBeUndefined();
46+
});
47+
48+
it('returns an oklch color for 1d and ramps toward darker red at AGE_MAX_RED_DAYS', () => {
49+
const oneDay = ageColorStyle(1);
50+
const maxDay = ageColorStyle(AGE_MAX_RED_DAYS);
51+
expect(oneDay?.color).toMatch(/^oklch\(/u);
52+
expect(maxDay?.color).toMatch(/^oklch\(/u);
53+
// Lightness L drops as days grow; AGE_MAX_RED_DAYS sits at L≈0.50, 1d at L≈0.77.
54+
expect(lightnessOf(maxDay!.color as string)).toBeLessThan(lightnessOf(oneDay!.color as string));
55+
});
56+
57+
it('clamps anything past AGE_MAX_RED_DAYS to the same color (no extrapolation past the ramp)', () => {
58+
const at60 = ageColorStyle(AGE_MAX_RED_DAYS);
59+
const at365 = ageColorStyle(365);
60+
expect(at60).toEqual(at365);
61+
});
62+
});
63+
64+
describe('ageRowStyle', () => {
65+
it('returns undefined for 0-day rows', () => {
66+
expect(ageRowStyle(0)).toBeUndefined();
67+
});
68+
69+
it('returns a low-alpha oklch background that ramps from ~0.04 to ~0.28', () => {
70+
const oneDay = ageRowStyle(1);
71+
const maxDay = ageRowStyle(AGE_MAX_RED_DAYS);
72+
const a1 = alphaOf(oneDay!.backgroundColor as string);
73+
const aMax = alphaOf(maxDay!.backgroundColor as string);
74+
expect(a1).toBeGreaterThan(0);
75+
expect(a1).toBeLessThan(0.1);
76+
expect(aMax).toBeGreaterThan(0.2);
77+
expect(aMax).toBeLessThanOrEqual(0.3);
78+
});
79+
80+
it('clamps past AGE_MAX_RED_DAYS so a 1y-old row looks identical to a 60d row', () => {
81+
expect(ageRowStyle(AGE_MAX_RED_DAYS)).toEqual(ageRowStyle(365));
82+
});
83+
});
84+
85+
describe('isOutdated', () => {
86+
it('is false when image contains the latest tag', () => {
87+
expect(isOutdated('sgl-project/sglang:v0.5.12-cu130', 'v0.5.12')).toBe(false);
88+
});
89+
90+
it('is true when image does not contain the latest tag', () => {
91+
expect(isOutdated('sgl-project/sglang:v0.5.10-cu130', 'v0.5.12')).toBe(true);
92+
});
93+
94+
it('is true for nightly tags regardless of latest', () => {
95+
expect(isOutdated('sgl-project/sglang:nightly', 'v0.5.12')).toBe(true);
96+
expect(isOutdated('sgl-project/sglang:nightly', null)).toBe(true);
97+
});
98+
99+
it('is true for rocm/sgl-dev and sglang-rocm patterns (case-insensitive)', () => {
100+
expect(isOutdated('rocm/sgl-dev:latest', 'v0.5.12')).toBe(true);
101+
expect(isOutdated('ROCM/SGL-DEV:foo', 'v0.5.12')).toBe(true);
102+
expect(isOutdated('myreg/sglang-rocm:bar', 'v0.5.12')).toBe(true);
103+
});
104+
105+
it('is false when actualLatest is null and tag is not in UNSTABLE_PATTERNS', () => {
106+
// No release data → can't classify as outdated. Avoid red-flagging a row
107+
// we have no ground truth for.
108+
expect(isOutdated('vllm/vllm-openai:v0.21.0', null)).toBe(false);
109+
});
110+
});
111+
112+
describe('getActualLatestTag', () => {
113+
it('looks up the base framework from FRAMEWORK_TO_BASE and returns its release', () => {
114+
const releases = { vllm: 'v0.21.0', sglang: 'v0.5.12' };
115+
expect(getActualLatestTag('vllm', releases)).toBe('v0.21.0');
116+
expect(getActualLatestTag('sglang', releases)).toBe('v0.5.12');
117+
expect(getActualLatestTag('dynamo-sglang', releases)).toBe('v0.5.12');
118+
expect(getActualLatestTag('mori-sglang', releases)).toBe('v0.5.12');
119+
});
120+
121+
it('returns null when releases is undefined (API still loading)', () => {
122+
expect(getActualLatestTag('vllm', undefined)).toBeNull();
123+
});
124+
125+
it('returns null for an unknown framework (no base mapping)', () => {
126+
expect(getActualLatestTag('trt', { vllm: 'v0.21.0', sglang: 'v0.5.12' })).toBeNull();
127+
});
128+
129+
it('returns null when the base release is missing from the releases map', () => {
130+
expect(getActualLatestTag('vllm', { sglang: 'v0.5.12' })).toBeNull();
131+
});
132+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { CSSProperties } from 'react';
2+
3+
import type { FrameworkReleases } from '@/lib/api';
4+
5+
/** Map framework variants to their base framework for release lookup. */
6+
export const FRAMEWORK_TO_BASE: Record<string, string> = {
7+
vllm: 'vllm',
8+
sglang: 'sglang',
9+
'dynamo-sglang': 'sglang',
10+
'mori-sglang': 'sglang',
11+
};
12+
13+
/**
14+
* Substrings that mark an image tag as unstable / pre-release. Lowercased
15+
* comparison — kept here (not inlined) so tests can re-import and stay in
16+
* sync with the runtime classifier.
17+
*/
18+
export const UNSTABLE_PATTERNS = ['nightly', 'rocm/sgl-dev', 'sglang-rocm'];
19+
20+
/** Age past which the cell is rendered at max red — anything older looks identical. */
21+
export const AGE_MAX_RED_DAYS = 60;
22+
23+
/** Whole-day delta between today (UTC) and an ISO date string (YYYY-MM-DD). */
24+
export function daysSince(dateStr: string, today: Date): number {
25+
const submitted = new Date(`${dateStr}T00:00:00Z`).getTime();
26+
const ms = today.getTime() - submitted;
27+
return Math.max(0, Math.floor(ms / 86_400_000));
28+
}
29+
30+
/**
31+
* Inline style for the Days-Since-Update cell so older rows scream louder
32+
* visually. Ramps from a subtle red at 1 day to deep red at 60 days (then
33+
* clamps); 0-day rows return `undefined` so the cell falls back to the
34+
* muted-foreground class.
35+
*/
36+
export function ageColorStyle(days: number): CSSProperties | undefined {
37+
if (days < 1) return undefined;
38+
const t = Math.min(AGE_MAX_RED_DAYS, days) / AGE_MAX_RED_DAYS;
39+
// Perceptually-uniform OKLCH ramp at hue 25 (red): lightness drops as
40+
// chroma rises, so the cell goes from light pink to saturated dark red.
41+
const L = 0.78 - 0.28 * t;
42+
const C = 0.12 + 0.12 * t;
43+
return { color: `oklch(${L.toFixed(3)} ${C.toFixed(3)} 25)` };
44+
}
45+
46+
/**
47+
* Companion to ageColorStyle for the whole row's background tint — same
48+
* 1d → 60d ramp but expressed as a low-alpha fill so the row content stays
49+
* readable. 0-day rows return undefined so the row falls back to its
50+
* hover-only class background.
51+
*/
52+
export function ageRowStyle(days: number): CSSProperties | undefined {
53+
if (days < 1) return undefined;
54+
const t = Math.min(AGE_MAX_RED_DAYS, days) / AGE_MAX_RED_DAYS;
55+
// Alpha tops out around 0.28 — enough that 60d+ rows are unmistakably
56+
// tinted without drowning out the text or competing with hover affordance.
57+
const alpha = (0.04 + 0.24 * t).toFixed(3);
58+
return { backgroundColor: `oklch(0.60 0.22 25 / ${alpha})` };
59+
}
60+
61+
/** Check if the image tag is outdated or uses an unstable/dev image. */
62+
export function isOutdated(image: string, actualLatest: string | null): boolean {
63+
const lower = image.toLowerCase();
64+
if (UNSTABLE_PATTERNS.some((p) => lower.includes(p))) return true;
65+
if (!actualLatest) return false;
66+
return !image.includes(actualLatest);
67+
}
68+
69+
export function getActualLatestTag(
70+
framework: string,
71+
releases: FrameworkReleases | undefined,
72+
): string | null {
73+
if (!releases) return null;
74+
const base = FRAMEWORK_TO_BASE[framework];
75+
if (!base) return null;
76+
return releases[base] ?? null;
77+
}

0 commit comments

Comments
 (0)