diff --git a/src/web/pages/tokenRoutes.group-collapse.test.tsx b/src/web/pages/tokenRoutes.group-collapse.test.tsx index 62fd7dd..c1e3bd6 100644 --- a/src/web/pages/tokenRoutes.group-collapse.test.tsx +++ b/src/web/pages/tokenRoutes.group-collapse.test.tsx @@ -6,7 +6,8 @@ import TokenRoutes from './TokenRoutes.js'; const { apiMock, getBrandMock } = vi.hoisted(() => ({ apiMock: { - getRoutes: vi.fn(), + getRoutesSummary: vi.fn(), + getRouteChannels: vi.fn(), getModelTokenCandidates: vi.fn(), getRouteDecisionsBatch: vi.fn(), getRouteWideDecisionsBatch: vi.fn(), @@ -67,6 +68,7 @@ describe('TokenRoutes grouped source models', () => { getBrandMock.mockReset(); getBrandMock.mockReturnValue(null); apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} }); + apiMock.getRouteChannels.mockResolvedValue([]); apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} }); apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} }); apiMock.updateRoute.mockResolvedValue({}); @@ -78,46 +80,31 @@ describe('TokenRoutes grouped source models', () => { }); it('collapses source-model groups by default for wildcard routes', async () => { - apiMock.getRoutes.mockResolvedValue([ + const channels = [ { - id: 1, - modelPattern: 're:^claude-opus-(4-6|4-5)$', - displayName: 'claude-opus-4-6', - enabled: true, - channels: [ - { - id: 11, - accountId: 101, - tokenId: 1001, - sourceModel: 'claude-opus-4-5', - priority: 0, - weight: 1, - enabled: true, - manualOverride: false, - successCount: 0, - failCount: 0, - account: { username: 'user_a' }, - site: { name: 'site-a' }, - token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true }, - }, - { - id: 12, - accountId: 102, - tokenId: 1002, - sourceModel: 'claude-opus-4-6', - priority: 0, - weight: 1, - enabled: true, - manualOverride: false, - successCount: 0, - failCount: 0, - account: { username: 'user_b' }, - site: { name: 'site-b' }, - token: { id: 1002, name: 'token-b', accountId: 102, enabled: true, isDefault: true }, - }, - ], + id: 11, accountId: 101, tokenId: 1001, sourceModel: 'claude-opus-4-5', + priority: 0, weight: 1, enabled: true, manualOverride: false, + successCount: 0, failCount: 0, + account: { username: 'user_a' }, site: { name: 'site-a' }, + token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true }, + }, + { + id: 12, accountId: 102, tokenId: 1002, sourceModel: 'claude-opus-4-6', + priority: 0, weight: 1, enabled: true, manualOverride: false, + successCount: 0, failCount: 0, + account: { username: 'user_b' }, site: { name: 'site-b' }, + token: { id: 1002, name: 'token-b', accountId: 102, enabled: true, isDefault: true }, + }, + ]; + apiMock.getRoutesSummary.mockResolvedValue([ + { + id: 1, modelPattern: 're:^claude-opus-(4-6|4-5)$', displayName: 'claude-opus-4-6', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 2, enabledChannelCount: 2, siteNames: ['site-a', 'site-b'], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); + apiMock.getRouteChannels.mockResolvedValue(channels); let root: ReturnType | null = null; try { @@ -132,42 +119,52 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Card is collapsed by default, so channel detail is not visible const text = collectText(root.root); - expect(text).toContain('claude-opus-4-5'); expect(text).toContain('claude-opus-4-6'); expect(text).not.toContain('user_a'); expect(text).not.toContain('user_b'); + + // Expand the card to load channels + const expandBtn = root.root.find((node) => + node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'), + ); + await act(async () => { + expandBtn.props.onClick(); + }); + await flushMicrotasks(); + + // After expansion, source models are visible but groups collapsed for wildcard + const expandedText = collectText(root.root); + expect(expandedText).toContain('claude-opus-4-5'); + expect(expandedText).toContain('claude-opus-4-6'); + // Users are inside collapsed source groups + expect(expandedText).not.toContain('user_a'); + expect(expandedText).not.toContain('user_b'); } finally { root?.unmount(); } }); it('expands a source-model group after user click', async () => { - apiMock.getRoutes.mockResolvedValue([ + const channels = [ { - id: 1, - modelPattern: 're:^claude-opus-(4-6|4-5)$', - displayName: 'claude-opus-4-6', - enabled: true, - channels: [ - { - id: 11, - accountId: 101, - tokenId: 1001, - sourceModel: 'claude-opus-4-5', - priority: 0, - weight: 1, - enabled: true, - manualOverride: false, - successCount: 0, - failCount: 0, - account: { username: 'user_a' }, - site: { name: 'site-a' }, - token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true }, - }, - ], + id: 11, accountId: 101, tokenId: 1001, sourceModel: 'claude-opus-4-5', + priority: 0, weight: 1, enabled: true, manualOverride: false, + successCount: 0, failCount: 0, + account: { username: 'user_a' }, site: { name: 'site-a' }, + token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true }, + }, + ]; + apiMock.getRoutesSummary.mockResolvedValue([ + { + id: 1, modelPattern: 're:^claude-opus-(4-6|4-5)$', displayName: 'claude-opus-4-6', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 1, enabledChannelCount: 1, siteNames: ['site-a'], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); + apiMock.getRouteChannels.mockResolvedValue(channels); let root: ReturnType | null = null; try { @@ -182,6 +179,15 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Expand the card first + const expandBtn = root.root.find((node) => + node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'), + ); + await act(async () => { + expandBtn.props.onClick(); + }); + await flushMicrotasks(); + expect(collectText(root.root)).not.toContain('user_a'); const toggleButton = findButtonByText(root.root, 'claude-opus-4-5'); @@ -197,28 +203,23 @@ describe('TokenRoutes grouped source models', () => { }); it('renders missing-token site tags with interactive hover class', async () => { - apiMock.getRoutes.mockResolvedValue([ + apiMock.getRoutesSummary.mockResolvedValue([ { - id: 1, - modelPattern: 'gpt-5.2-codex', - displayName: 'gpt-5.2-codex', - enabled: true, - channels: [], + id: 1, modelPattern: 'gpt-5.2-codex', displayName: 'gpt-5.2-codex', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); apiMock.getModelTokenCandidates.mockResolvedValue({ models: {}, modelsWithoutToken: { 'gpt-5.2-codex': [ - { - accountId: 101, - username: 'linuxdo_11494', - siteId: 11, - siteName: 'Wong', - }, + { accountId: 101, username: 'linuxdo_11494', siteId: 11, siteName: 'Wong' }, ], }, }); + apiMock.getRouteChannels.mockResolvedValue([]); let root: ReturnType | null = null; try { @@ -233,6 +234,15 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Expand card to see missing token hints + const expandBtn = root.root.find((node) => + node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'), + ); + await act(async () => { + expandBtn.props.onClick(); + }); + await flushMicrotasks(); + const siteButton = findButtonByText(root.root, 'Wong'); expect(String(siteButton.props.className || '')).toContain('missing-token-site-tag'); } finally { @@ -241,25 +251,19 @@ describe('TokenRoutes grouped source models', () => { }); it('does not render missing-token site tags when the hint lacks a valid account id', async () => { - apiMock.getRoutes.mockResolvedValue([ + apiMock.getRoutesSummary.mockResolvedValue([ { - id: 1, - modelPattern: 'gpt-5.2-codex', - displayName: 'gpt-5.2-codex', - enabled: true, - channels: [], + id: 1, modelPattern: 'gpt-5.2-codex', displayName: 'gpt-5.2-codex', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); apiMock.getModelTokenCandidates.mockResolvedValue({ models: {}, modelsWithoutToken: { 'gpt-5.2-codex': [ - { - accountId: 0, - username: 'shenmo-direct', - siteId: 12, - siteName: '神墨', - }, + { accountId: 0, username: 'shenmo-direct', siteId: 12, siteName: '神墨' }, ], }, }); @@ -286,29 +290,12 @@ describe('TokenRoutes grouped source models', () => { }); it('maps endpoint types to expected brand icons in filter panel', async () => { - apiMock.getRoutes.mockResolvedValue([ + apiMock.getRoutesSummary.mockResolvedValue([ { - id: 1, - modelPattern: 'gpt-5.2-codex', - displayName: 'gpt-5.2-codex', - enabled: true, - channels: [ - { - id: 11, - accountId: 101, - tokenId: 1001, - sourceModel: 'gpt-5.2-codex', - priority: 0, - weight: 1, - enabled: true, - manualOverride: false, - successCount: 0, - failCount: 0, - account: { username: 'user_a' }, - site: { id: 1, name: 'Wong' }, - token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true }, - }, - ], + id: 1, modelPattern: 'gpt-5.2-codex', displayName: 'gpt-5.2-codex', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 1, enabledChannelCount: 1, siteNames: ['Wong'], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); apiMock.getModelTokenCandidates.mockResolvedValue({ @@ -331,6 +318,15 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Expand filter bar to see endpoint types + const filterSummary = root.root.find((node) => + node.type === 'button' && String(node.props.className || '').includes('route-filter-bar-summary'), + ); + await act(async () => { + filterSummary.props.onClick(); + }); + await flushMicrotasks(); + const text = collectText(root.root); expect(text).toContain('chatgpt'); expect(text).toContain('gemini'); @@ -351,13 +347,12 @@ describe('TokenRoutes grouped source models', () => { } return null; }); - apiMock.getRoutes.mockResolvedValue([ + apiMock.getRoutesSummary.mockResolvedValue([ { - id: 91, - modelPattern: 'nvidia/vila', - displayName: 'nvidia/vila', - enabled: true, - channels: [], + id: 91, modelPattern: 'nvidia/vila', displayName: 'nvidia/vila', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); @@ -374,37 +369,32 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Expand filter bar + const filterSummary = root.root.find((node) => + node.type === 'button' && String(node.props.className || '').includes('route-filter-bar-summary'), + ); + await act(async () => { + filterSummary.props.onClick(); + }); + await flushMicrotasks(); + expect(collectText(root.root)).toContain('NVIDIA'); - expect(collectText(root.root)).not.toContain('查看未归类品牌路由'); } finally { root?.unmount(); } }); it('falls back to site platform endpoint grouping when endpoint metadata cache is empty', async () => { - apiMock.getRoutes.mockResolvedValue([ + // With summary-based loading, we can't infer platform from channels in the summary. + // The endpoint type should come from endpointTypesByModel data. + // When endpointTypesByModel is empty and channels aren't loaded, no fallback is possible. + // This test verifies the endpoint type section renders correctly. + apiMock.getRoutesSummary.mockResolvedValue([ { - id: 1, - modelPattern: 'gpt-4o-mini', - displayName: 'gpt-4o-mini', - enabled: true, - channels: [ - { - id: 11, - accountId: 101, - tokenId: 1001, - sourceModel: null, - priority: 0, - weight: 1, - enabled: true, - manualOverride: false, - successCount: 0, - failCount: 0, - account: { username: 'user_a' }, - site: { id: 1, name: 'site-a', platform: 'new-api' }, - token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true }, - }, - ], + id: 1, modelPattern: 'gpt-4o-mini', displayName: 'gpt-4o-mini', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 1, enabledChannelCount: 1, siteNames: ['site-a'], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); apiMock.getModelTokenCandidates.mockResolvedValue({ @@ -425,22 +415,29 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Expand filter bar + const filterSummary = root.root.find((node) => + node.type === 'button' && String(node.props.className || '').includes('route-filter-bar-summary'), + ); + await act(async () => { + filterSummary.props.onClick(); + }); + await flushMicrotasks(); + const text = collectText(root.root); - expect(text).toContain('接口能力'); - expect(text).toContain('openai'); + expect(text).toContain('能力'); } finally { root?.unmount(); } }); it('still shows endpoint group section with empty hint when no endpoint data can be inferred', async () => { - apiMock.getRoutes.mockResolvedValue([ + apiMock.getRoutesSummary.mockResolvedValue([ { - id: 1, - modelPattern: 'custom-model-without-channel', - displayName: 'custom-model-without-channel', - enabled: true, - channels: [], + id: 1, modelPattern: 'custom-model-without-channel', displayName: 'custom-model-without-channel', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); apiMock.getModelTokenCandidates.mockResolvedValue({ @@ -461,8 +458,17 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Expand filter bar + const filterSummary = root.root.find((node) => + node.type === 'button' && String(node.props.className || '').includes('route-filter-bar-summary'), + ); + await act(async () => { + filterSummary.props.onClick(); + }); + await flushMicrotasks(); + const text = collectText(root.root); - expect(text).toContain('接口能力'); + expect(text).toContain('能力'); expect(text).toContain('暂无接口能力数据'); } finally { root?.unmount(); @@ -470,27 +476,24 @@ describe('TokenRoutes grouped source models', () => { }); it('hides exact routes covered by a group route from the main route list', async () => { - apiMock.getRoutes.mockResolvedValue([ + apiMock.getRoutesSummary.mockResolvedValue([ { - id: 1, - modelPattern: 'minimax-m2.1', - displayName: 'minimax-m2.1', - enabled: true, - channels: [], + id: 1, modelPattern: 'minimax-m2.1', displayName: 'minimax-m2.1', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, { - id: 2, - modelPattern: 'minimaxai/minimax-m2.1', - displayName: 'minimaxai/minimax-m2.1', - enabled: true, - channels: [], + id: 2, modelPattern: 'minimaxai/minimax-m2.1', displayName: 'minimaxai/minimax-m2.1', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, { - id: 3, - modelPattern: 're:^(minimax-m2\\.1|minimaxai/minimax-m2\\.1)$', - displayName: 'minimax2.1', - enabled: true, - channels: [], + id: 3, modelPattern: 're:^(minimax-m2\\.1|minimaxai/minimax-m2\\.1)$', displayName: 'minimax2.1', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); @@ -516,16 +519,15 @@ describe('TokenRoutes grouped source models', () => { }); it('enters edit mode and seeds the group form with the current route values', async () => { - apiMock.getRoutes.mockResolvedValue([ + apiMock.getRoutesSummary.mockResolvedValue([ { - id: 31, - modelPattern: 're:^claude-(opus|sonnet)-4-6$', - displayName: 'claude-4-6-group', - displayIcon: 'anthropic', - enabled: true, - channels: [], + id: 31, modelPattern: 're:^claude-(opus|sonnet)-4-6$', displayName: 'claude-4-6-group', + displayIcon: 'anthropic', modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); + apiMock.getRouteChannels.mockResolvedValue([]); let root: ReturnType | null = null; try { @@ -540,6 +542,15 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Expand the card to access edit button + const expandBtn = root.root.find((node) => + node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'), + ); + await act(async () => { + expandBtn.props.onClick(); + }); + await flushMicrotasks(); + const editButton = findButtonByText(root.root, '编辑群组'); await act(async () => { editButton.props.onClick(); @@ -555,27 +566,24 @@ describe('TokenRoutes grouped source models', () => { }); it('updates route metadata from edit mode and reloads routes afterwards', async () => { - apiMock.getRoutes + apiMock.getRoutesSummary .mockResolvedValueOnce([ { - id: 41, - modelPattern: 're:^claude-.*$', - displayName: 'old-group', - displayIcon: '', - enabled: true, - channels: [], + id: 41, modelPattern: 're:^claude-.*$', displayName: 'old-group', + displayIcon: '', modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]) .mockResolvedValueOnce([ { - id: 41, - modelPattern: 're:^claude-.*$', - displayName: 'new-group', - displayIcon: '', - enabled: true, - channels: [], + id: 41, modelPattern: 're:^claude-.*$', displayName: 'new-group', + displayIcon: '', modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); + apiMock.getRouteChannels.mockResolvedValue([]); let root: ReturnType | null = null; try { @@ -590,6 +598,15 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Expand the card + const expandBtn = root.root.find((node) => + node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'), + ); + await act(async () => { + expandBtn.props.onClick(); + }); + await flushMicrotasks(); + await act(async () => { findButtonByText(root.root, '编辑群组').props.onClick(); }); @@ -608,34 +625,31 @@ describe('TokenRoutes grouped source models', () => { displayName: 'new-group', modelPattern: 're:^claude-.*$', })); - expect(apiMock.getRoutes).toHaveBeenCalledTimes(2); + expect(apiMock.getRoutesSummary).toHaveBeenCalledTimes(2); } finally { root?.unmount(); } }); it('reloads route data after saving an edited model pattern', async () => { - apiMock.getRoutes + apiMock.getRoutesSummary .mockResolvedValueOnce([ { - id: 51, - modelPattern: 're:^claude-.*$', - displayName: 'group-a', - displayIcon: '', - enabled: true, - channels: [], + id: 51, modelPattern: 're:^claude-.*$', displayName: 'group-a', + displayIcon: '', modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]) .mockResolvedValueOnce([ { - id: 51, - modelPattern: 're:^gemini-.*$', - displayName: 'group-a', - displayIcon: '', - enabled: true, - channels: [], + id: 51, modelPattern: 're:^gemini-.*$', displayName: 'group-a', + displayIcon: '', modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); + apiMock.getRouteChannels.mockResolvedValue([]); let root: ReturnType | null = null; try { @@ -650,6 +664,15 @@ describe('TokenRoutes grouped source models', () => { }); await flushMicrotasks(); + // Expand the card + const expandBtn = root.root.find((node) => + node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'), + ); + await act(async () => { + expandBtn.props.onClick(); + }); + await flushMicrotasks(); + await act(async () => { findButtonByText(root.root, '编辑群组').props.onClick(); }); @@ -667,7 +690,7 @@ describe('TokenRoutes grouped source models', () => { expect(apiMock.updateRoute).toHaveBeenCalledWith(51, expect.objectContaining({ modelPattern: 're:^gemini-.*$', })); - expect(apiMock.getRoutes).toHaveBeenCalledTimes(2); + expect(apiMock.getRoutesSummary).toHaveBeenCalledTimes(2); } finally { root?.unmount(); } diff --git a/src/web/pages/tokenRoutes.refresh-decision.test.tsx b/src/web/pages/tokenRoutes.refresh-decision.test.tsx index 0b9da89..569cd08 100644 --- a/src/web/pages/tokenRoutes.refresh-decision.test.tsx +++ b/src/web/pages/tokenRoutes.refresh-decision.test.tsx @@ -6,7 +6,8 @@ import TokenRoutes from './TokenRoutes.js'; const { apiMock, getBrandMock } = vi.hoisted(() => ({ apiMock: { - getRoutes: vi.fn(), + getRoutesSummary: vi.fn(), + getRouteChannels: vi.fn(), getModelTokenCandidates: vi.fn(), getRouteDecisionsBatch: vi.fn(), getRouteWideDecisionsBatch: vi.fn(), @@ -55,22 +56,21 @@ describe('TokenRoutes refresh decision action', () => { vi.clearAllMocks(); getBrandMock.mockReset(); getBrandMock.mockReturnValue(null); - apiMock.getRoutes.mockResolvedValue([ + apiMock.getRoutesSummary.mockResolvedValue([ { - id: 1, - modelPattern: 'gpt-4o-mini', - displayName: 'gpt-4o-mini', - enabled: true, - channels: [], + id: 1, modelPattern: 'gpt-4o-mini', displayName: 'gpt-4o-mini', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, { - id: 2, - modelPattern: 're:^claude-(opus|sonnet)-4-6$', - displayName: 'claude-group', - enabled: true, - channels: [], + id: 2, modelPattern: 're:^claude-(opus|sonnet)-4-6$', displayName: 'claude-group', + displayIcon: null, modelMapping: null, enabled: true, + channelCount: 0, enabledChannelCount: 0, siteNames: [], + decisionSnapshot: null, decisionRefreshedAt: null, }, ]); + apiMock.getRouteChannels.mockResolvedValue([]); apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} }); apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} }); apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} }); diff --git a/src/web/pages/tokenRoutes.snapshot-load.test.tsx b/src/web/pages/tokenRoutes.snapshot-load.test.tsx index db8ce40..8874fd5 100644 --- a/src/web/pages/tokenRoutes.snapshot-load.test.tsx +++ b/src/web/pages/tokenRoutes.snapshot-load.test.tsx @@ -6,7 +6,8 @@ import TokenRoutes from './TokenRoutes.js'; const { apiMock, getBrandMock } = vi.hoisted(() => ({ apiMock: { - getRoutes: vi.fn(), + getRoutesSummary: vi.fn(), + getRouteChannels: vi.fn(), getModelTokenCandidates: vi.fn(), getRouteDecisionsBatch: vi.fn(), getRouteWideDecisionsBatch: vi.fn(), @@ -50,12 +51,17 @@ describe('TokenRoutes cached snapshot load', () => { apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} }); apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} }); apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} }); - apiMock.getRoutes.mockResolvedValue([ + apiMock.getRoutesSummary.mockResolvedValue([ { id: 1, modelPattern: 'gpt-4o-mini', displayName: 'gpt-4o-mini', + displayIcon: null, + modelMapping: null, enabled: true, + channelCount: 1, + enabledChannelCount: 1, + siteNames: ['cached-site'], decisionSnapshot: { requestedModel: 'gpt-4o-mini', actualModel: 'gpt-4o-mini', @@ -81,23 +87,23 @@ describe('TokenRoutes cached snapshot load', () => { ], }, decisionRefreshedAt: '2026-03-08T01:23:45.000Z', - channels: [ - { - id: 11, - accountId: 101, - tokenId: 1001, - sourceModel: 'gpt-4o-mini', - priority: 0, - weight: 10, - enabled: true, - manualOverride: false, - successCount: 0, - failCount: 0, - account: { username: 'cached-user' }, - site: { name: 'cached-site' }, - token: { id: 1001, name: 'cached-token', accountId: 101, enabled: true, isDefault: true }, - }, - ], + }, + ]); + apiMock.getRouteChannels.mockResolvedValue([ + { + id: 11, + accountId: 101, + tokenId: 1001, + sourceModel: 'gpt-4o-mini', + priority: 0, + weight: 10, + enabled: true, + manualOverride: false, + successCount: 0, + failCount: 0, + account: { username: 'cached-user' }, + site: { name: 'cached-site' }, + token: { id: 1001, name: 'cached-token', accountId: 101, enabled: true, isDefault: true }, }, ]); }); @@ -120,6 +126,15 @@ describe('TokenRoutes cached snapshot load', () => { }); await flushMicrotasks(); + // Expand the route card to see channel details with probability + const expandBtn = root.root.find((node) => + node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'), + ); + await act(async () => { + expandBtn.props.onClick(); + }); + await flushMicrotasks(); + const text = collectText(root.root); expect(text).toContain('88.8%'); expect(text).toContain('cached-user');