Skip to content
Open
75 changes: 66 additions & 9 deletions packages/app/cypress/e2e/overview.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const MATRIX_ROWS = 8;
const AGENTX = 'agentx';
const AGENTX_LABEL = 'Long Context Multi-Turn Realistic Agentic Scenario (AgentX)';
const AGENTX_LABEL_ZH = '长上下文多轮真实智能体场景(AgentX)';
/** Shared by both locales: the scenario is named after its acronym. */
const AGENTX_SHORT = 'AgentX';

const PAGE_TITLE = 'Inference Cost per Million Tokens';
const PAGE_TITLE_ZH = '推理每百万 token 成本';
Expand Down Expand Up @@ -58,6 +60,21 @@ function expectNoHorizontalScroller(testId: string) {
});
}

/**
* The row header shows the acronym so the 22%-wide model column stays one line,
* and keeps the full scenario name for assistive tech and the hover title. Both
* strings are in the cell, so assert the two layers rather than their
* concatenation. Scenarios already named by a short label render one node.
*/
function expectAgentxScenario(fullLabel: string) {
cy.get('[data-testid="overview-model-scenario"]')
.should('have.attr', 'title', fullLabel)
.within(() => {
cy.get('.sr-only').should('have.text', fullLabel);
cy.get('[aria-hidden="true"]').should('have.text', AGENTX_SHORT);
});
}

/** Visible dates and snapshot framing must be gone; evidence stays in labels. */
function expectNoVisibleDatesOrSnapshot() {
cy.get('[data-testid="overview-pair-evidence-date"]').should('not.exist');
Expand Down Expand Up @@ -407,8 +424,11 @@ describe('Overview page', () => {
cy.viewport(1280, 900);
cy.visit('/overview');

cy.get('[data-testid="overview-page"]')
.children('[data-testid="overview-comparison-switcher"]')
// The tabs sit inside the surface handed to the Fullscreen API so that
// presenting keeps them with the matrix, which costs them their old spot as
// a direct child of the page.
cy.get('[data-testid="overview-presentation-surface"]')
.find('[data-testid="overview-comparison-switcher"]')
.should('have.length', 1)
.and('have.class', 'justify-center');
cy.get('[data-testid="overview-comparison-switcher"]')
Expand Down Expand Up @@ -603,6 +623,43 @@ describe('Overview page', () => {
}
});

// Clicking the control, not just building its href: the matrix reads from a
// client data cache, and a cache keyed without the row params moves the
// address bar while leaving every row on screen.
it('narrows the matrix when the row filter is clicked in either comparison mode', () => {
cy.viewport(1280, 900);

for (const [href, attribute, key, sentence] of [
['/overview?compare=30d', 'data-overview-row-scope', 'rows=changed', 'no 30-day change'],
[
'/overview',
'data-overview-hardware-row-scope',
'hwrows=priced',
'no result on any platform',
],
] as const) {
cy.visit(href);
cy.get('[data-testid="overview-desktop-model"]').should('have.length', MATRIX_ROWS);

cy.get(`a[${attribute}]`)
.should('contain.text', 'Hide ')
.invoke('text')
.then((label) => {
const hidden = Number(/\d+/.exec(label)?.[0]);
expect(hidden, `hidden row count in "${label}"`).to.be.greaterThan(0);

cy.get(`a[${attribute}]`).click();
cy.location('search').should('contain', key);
cy.get('[data-testid="overview-desktop-model"]').should(
'have.length',
MATRIX_ROWS - hidden,
);
// The action label names the click, so it flips once the scope lands.
cy.get(`a[${attribute}]`).should('contain.text', `Show ${hidden} rows with ${sentence}`);
});
}
});

it('defaults to community engine scope and switches with canonical links preserving tier and locale', () => {
cy.viewport(1280, 900);
cy.visit('/overview');
Expand Down Expand Up @@ -632,7 +689,7 @@ describe('Overview page', () => {
});

desktopModel('GLM-5.2').within(() => {
cy.get('[data-testid="overview-model-scenario"]').should('have.text', AGENTX_LABEL);
expectAgentxScenario(AGENTX_LABEL);
cy.get('[data-testid="overview-pair-missing"]').should('have.length', 5);
});
cy.get(
Expand Down Expand Up @@ -872,9 +929,9 @@ describe('Overview page', () => {
cy.get('[data-testid="overview-desktop-matrix"]').should('contain.text', label);
}
for (const model of ['Kimi-K3', 'GLM-5.2']) {
desktopModel(model)
.find('[data-testid="overview-model-scenario"]')
.should('have.text', AGENTX_LABEL);
desktopModel(model).within(() => {
expectAgentxScenario(AGENTX_LABEL);
});
}
for (const model of ['DeepSeek-V4-Pro', 'MiniMax-M3', 'Qwen-3.5-397B-A17B']) {
desktopModel(model, SINGLE_TURN)
Expand All @@ -897,7 +954,7 @@ describe('Overview page', () => {
});

desktopModel('DeepSeek-V4-Pro', AGENTX).within(() => {
cy.get('[data-testid="overview-model-scenario"]').should('have.text', AGENTX_LABEL);
expectAgentxScenario(AGENTX_LABEL);
cy.contains('DeepSeek V4 Pro 1.6T').should('exist');
// Priced from the AgentX rows alone — the single-turn sweep never leaks in.
cy.get(
Expand Down Expand Up @@ -1412,14 +1469,14 @@ describe('Overview page', () => {
.and('have.attr', 'title', '缺少可比较的 B200 基线');
});
desktopModel('GLM-5.2').within(() => {
cy.get('[data-testid="overview-model-scenario"]').should('have.text', AGENTX_LABEL_ZH);
expectAgentxScenario(AGENTX_LABEL_ZH);
cy.get('[data-testid="overview-pair-missing"]').should('have.length', 5);
platform('b300')
.find('[data-testid="overview-pair-missing"]')
.should('contain.text', '该场景暂无数据');
});
desktopModel('DeepSeek-V4-Pro', AGENTX).within(() => {
cy.get('[data-testid="overview-model-scenario"]').should('have.text', AGENTX_LABEL_ZH);
expectAgentxScenario(AGENTX_LABEL_ZH);
cy.get(
'[data-testid="overview-pair-value"][data-hardware="b200"] [data-testid="overview-cost-evidence-link"]',
).should('have.text', '$0.064');
Expand Down
20 changes: 17 additions & 3 deletions packages/app/src/app/api/v1/overview/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,32 @@ describe('GET /api/v1/overview', () => {
mockGetOverviewPageData.mockResolvedValueOnce(data);

const response = await GET(
request('/api/v1/overview?tier=75&engine=all&compare=30d&ref=b300&models=all'),
request(
'/api/v1/overview?tier=75&engine=all&compare=30d&ref=b300&models=all&rows=changed&hwrows=priced',
),
);

expect(response.status).toBe(200);
expect(await response.json()).toEqual(data);
expect(mockGetOverviewPageData).toHaveBeenCalledWith(75, 'all', 'history', 'b300', 'all');
expect(mockGetOverviewPageData).toHaveBeenCalledWith(
75,
'all',
'history',
'b300',
'all',
'changed',
'priced',
);
expect(mockCachedJson).toHaveBeenCalledWith(data);
});

it('normalizes unsupported query values to overview defaults', async () => {
mockGetOverviewPageData.mockResolvedValueOnce({ models: [] });

await GET(
request('/api/v1/overview?tier=999&engine=vendor&compare=weekly&ref=h100&models=inactive'),
request(
'/api/v1/overview?tier=999&engine=vendor&compare=weekly&ref=h100&models=inactive&rows=some&hwrows=blank',
),
);

expect(mockGetOverviewPageData).toHaveBeenCalledWith(
Expand All @@ -59,6 +71,8 @@ describe('GET /api/v1/overview', () => {
'hardware',
'b200',
'default',
'all',
'all',
);
});

Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/app/api/v1/overview/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { cachedJson } from '@/lib/api-cache';
import {
resolveOverviewComparisonMode,
resolveOverviewEngineScope,
resolveOverviewHardwareRowScope,
resolveOverviewModelScope,
resolveOverviewReferenceHardware,
resolveOverviewRowScope,
resolveOverviewTier,
} from '@/lib/overview-data';
import { getOverviewPageData } from '@/lib/overview-data.server';
Expand All @@ -22,6 +24,8 @@ export async function GET(request: NextRequest) {
resolveOverviewComparisonMode(params.get('compare') ?? undefined),
resolveOverviewReferenceHardware(params.get('ref') ?? undefined),
resolveOverviewModelScope(params.get('models') ?? undefined),
resolveOverviewRowScope(params.get('rows') ?? undefined),
resolveOverviewHardwareRowScope(params.get('hwrows') ?? undefined),
);
return cachedJson(data);
} catch (error) {
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/app/overview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { enAlternates } from '@/lib/i18n';
import {
resolveOverviewComparisonMode,
resolveOverviewEngineScope,
resolveOverviewHardwareRowScope,
resolveOverviewModelScope,
resolveOverviewReferenceHardware,
resolveOverviewRowScope,
resolveOverviewTier,
} from '@/lib/overview-data';
import { getOverviewPageData } from '@/lib/overview-data.server';
Expand Down Expand Up @@ -47,6 +49,8 @@ export default async function OverviewPage({ searchParams }: Props) {
resolveOverviewComparisonMode(sp.compare),
resolveOverviewReferenceHardware(sp.ref),
resolveOverviewModelScope(sp.models),
resolveOverviewRowScope(sp.rows),
resolveOverviewHardwareRowScope(sp.hwrows),
);
return <OverviewPageContent data={data} locale="en" />;
}
4 changes: 4 additions & 0 deletions packages/app/src/app/zh/overview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { ZH_OG_LOCALE, zhAlternates } from '@/lib/i18n';
import {
resolveOverviewComparisonMode,
resolveOverviewEngineScope,
resolveOverviewHardwareRowScope,
resolveOverviewModelScope,
resolveOverviewReferenceHardware,
resolveOverviewRowScope,
resolveOverviewTier,
} from '@/lib/overview-data';
import { getOverviewPageData } from '@/lib/overview-data.server';
Expand Down Expand Up @@ -48,6 +50,8 @@ export default async function ZhOverviewPage({ searchParams }: Props) {
resolveOverviewComparisonMode(sp.compare),
resolveOverviewReferenceHardware(sp.ref),
resolveOverviewModelScope(sp.models),
resolveOverviewRowScope(sp.rows),
resolveOverviewHardwareRowScope(sp.hwrows),
);
return <OverviewPageContent data={data} locale="zh" />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useOverviewNavigation } from './overview-navigation';
const PREFETCH_DWELL_MS = 120;

interface OverviewNavAnalytics {
control: 'comparison' | 'engine' | 'models' | 'tier';
control: 'comparison' | 'engine' | 'hwrows' | 'models' | 'rows' | 'tier';
value: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ let root: Root;
let selectTier: (() => void) | undefined;
let selectEngine: (() => void) | undefined;
let selectReference: (() => void) | undefined;
let selectRowScope: (() => void) | undefined;
let selectHardwareRowScope: (() => void) | undefined;
let prefetchTier: (() => void) | undefined;

function pageData(tier: OverviewTier): OverviewPageData {
Expand All @@ -43,6 +45,10 @@ function pageData(tier: OverviewTier): OverviewPageData {
comparisonMode: 'hardware',
referenceHardware: 'b200',
modelScope: 'default',
rowScope: 'all',
hardwareRowScope: 'all',
unchangedRowCount: 0,
emptyRowCount: 0,
historicalWindow: null,
};
}
Expand All @@ -54,6 +60,8 @@ function Probe() {
selectTier = () => navigation.push('/overview?tier=75', ['tier']);
selectEngine = () => navigation.push('/overview?engine=all', ['engine']);
selectReference = () => navigation.push('/overview?ref=b300', ['ref']);
selectRowScope = () => navigation.push('/overview?compare=30d&rows=changed', ['rows']);
selectHardwareRowScope = () => navigation.push('/overview?hwrows=priced', ['hwrows']);
prefetchTier = () => navigation.prefetch('/overview?tier=75', ['tier']);
return (
<>
Expand Down Expand Up @@ -105,6 +113,8 @@ afterEach(() => {
selectTier = undefined;
selectEngine = undefined;
selectReference = undefined;
selectRowScope = undefined;
selectHardwareRowScope = undefined;
prefetchTier = undefined;
vi.unstubAllGlobals();
});
Expand Down Expand Up @@ -384,6 +394,32 @@ describe('OverviewNavigationProvider', () => {
expect(readProbe('reference')).toBe('b300');
});

// Both row scopes narrow the rows the server sends, so neither may collapse
// into the unfiltered payload's cache key the way `ref` deliberately does.
it('requests a narrowed matrix for each row scope instead of reusing the full one', () => {
deferredFetch();

renderProvider(pageData(50), '/overview?compare=30d');
act(() => selectRowScope?.());

expect(fetch).toHaveBeenCalledWith('/api/v1/overview?compare=30d&rows=changed', {
headers: { Accept: 'application/json' },
});
expect(readProbe('pending')).toBe('pending');
});

it('requests a narrowed matrix for the hardware row scope', () => {
deferredFetch();

renderProvider(pageData(50), '/overview');
act(() => selectHardwareRowScope?.());

expect(fetch).toHaveBeenCalledWith('/api/v1/overview?hwrows=priced', {
headers: { Accept: 'application/json' },
});
expect(readProbe('pending')).toBe('pending');
});

it('preserves unknown params and the fragment on the first selection', () => {
deferredFetch();

Expand Down
10 changes: 9 additions & 1 deletion packages/app/src/components/overview/overview-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import {
type OverviewReferenceHardware,
resolveOverviewComparisonMode,
resolveOverviewEngineScope,
resolveOverviewHardwareRowScope,
resolveOverviewModelScope,
resolveOverviewReferenceHardware,
resolveOverviewRowScope,
resolveOverviewTier,
} from '@/lib/overview-data';
import {
Expand All @@ -39,6 +41,10 @@ import {
* defaults, reordered params, campaign tags, a fragment — collapse to one key,
* so a `ref` change is a guaranteed hit and the CDN sees one entry per data
* state instead of one per link anyone has ever shared.
*
* Every param the server reads has to appear here. Both row scopes do: each
* narrows the rows the response carries, and the dormant one still reaches the
* payload so a tab switch can restore the other mode's answer.
*/
function overviewDataKey(href: string): string {
const url = new URL(href, 'https://inferencex.local');
Expand All @@ -50,10 +56,12 @@ function overviewDataKey(href: string): string {
resolveOverviewComparisonMode(params.get('compare') ?? undefined),
OVERVIEW_DEFAULT_REFERENCE_HARDWARE,
resolveOverviewModelScope(params.get('models') ?? undefined),
resolveOverviewRowScope(params.get('rows') ?? undefined),
resolveOverviewHardwareRowScope(params.get('hwrows') ?? undefined),
);
}

export type OverviewNavControl = 'comparison' | 'engine' | 'models' | 'tier';
export type OverviewNavControl = 'comparison' | 'engine' | 'hwrows' | 'models' | 'rows' | 'tier';
Comment thread
cursor[bot] marked this conversation as resolved.

interface OverviewNavigationValue {
isPending: boolean;
Expand Down
Loading