test(usage): cover user and admin reasoning effort page display

Mount the real usage tables and assert users only see the requested
effort, while admins can reveal mapped effort via column settings.
This commit is contained in:
刘佳林
2026-08-25 15:49:04 +08:00
parent 5705f4a4aa
commit a8cfe746b3
4 changed files with 328 additions and 2 deletions
@@ -0,0 +1,322 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { defineComponent } from 'vue'
import UserUsageView from '@/views/user/UsageView.vue'
import AdminUsageView from '@/views/admin/UsageView.vue'
const {
userQuery,
userGetStats,
userGetDashboardModels,
userGetDashboardSnapshotV2,
userKeysList,
userGroupsGetAvailable,
adminList,
adminGetStats,
adminGetSnapshotV2,
adminGetModelStats,
adminGetById,
listErrorLogs,
} = vi.hoisted(() => ({
userQuery: vi.fn(),
userGetStats: vi.fn(),
userGetDashboardModels: vi.fn(),
userGetDashboardSnapshotV2: vi.fn(),
userKeysList: vi.fn(),
userGroupsGetAvailable: vi.fn(),
adminList: vi.fn(),
adminGetStats: vi.fn(),
adminGetSnapshotV2: vi.fn(),
adminGetModelStats: vi.fn(),
adminGetById: vi.fn(),
listErrorLogs: vi.fn(),
}))
const messages: Record<string, string> = {
'admin.dashboard.timeRange': 'Time range',
'admin.dashboard.granularity': 'Granularity',
'admin.dashboard.day': 'Day',
'admin.dashboard.hour': 'Hour',
'admin.users.columnSettings': 'Columns',
'admin.usage.group': 'Group',
'admin.usage.user': 'User',
'admin.usage.account': 'Account',
'admin.usage.billingType': 'Billing type',
'admin.usage.billingMode': 'Billing mode',
'admin.usage.allTypes': 'All types',
'admin.usage.allBillingTypes': 'All billing types',
'admin.usage.billingTypeBalance': 'Balance',
'admin.usage.billingTypeSubscription': 'Subscription',
'admin.usage.allBillingModes': 'All billing modes',
'admin.usage.billingModeToken': 'Token',
'admin.usage.billingModePerRequest': 'Per request',
'admin.usage.billingModeImage': 'Image',
'admin.usage.allGroups': 'All groups',
'admin.usage.allModels': 'All models',
'admin.usage.ipAddress': 'IP',
'usage.allApiKeys': 'All API Keys',
'usage.apiKeyFilter': 'API Key',
'usage.model': 'Model',
'usage.type': 'Type',
'usage.reasoningEffort': 'Reasoning Effort',
'usage.endpoint': 'Endpoint',
'usage.tokens': 'Tokens',
'usage.cost': 'Cost',
'usage.latency': 'Latency',
'usage.time': 'Time',
'usage.userAgent': 'User Agent',
'usage.ws': 'WS',
'usage.stream': 'Stream',
'usage.sync': 'Sync',
'usage.exporting': 'Exporting',
'usage.exportCsv': 'Export CSV',
'usage.failedToLoad': 'Failed to load',
'usage.noRecords': 'No records',
'empty.noData': 'No data',
'common.refresh': 'Refresh',
'common.reset': 'Reset',
}
vi.mock('@/api', () => ({
usageAPI: {
query: userQuery,
getStats: userGetStats,
getDashboardModels: userGetDashboardModels,
getDashboardSnapshotV2: userGetDashboardSnapshotV2,
},
keysAPI: {
list: userKeysList,
},
userGroupsAPI: {
getAvailable: userGroupsGetAvailable,
},
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
usage: {
list: adminList,
getStats: adminGetStats,
},
dashboard: {
getSnapshotV2: adminGetSnapshotV2,
getModelStats: adminGetModelStats,
},
users: {
getById: adminGetById,
},
},
}))
vi.mock('@/api/admin/usage', () => ({
adminUsageAPI: {
list: adminList,
},
}))
vi.mock('@/api/admin/ops', () => ({
listErrorLogs,
}))
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError: vi.fn(),
showWarning: vi.fn(),
showSuccess: vi.fn(),
showInfo: vi.fn(),
}),
}))
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
t: (key: string) => messages[key] ?? key,
}),
}
})
vi.mock('vue-router', () => ({
useRoute: () => ({ query: {} }),
}))
const layoutStub = { template: '<div><slot /></div>' }
const chartStub = { template: '<div />' }
const iconStub = { template: '<span />' }
const adminFiltersStub = defineComponent({
template: '<div><slot name="after-reset" /></div>',
})
const emptyStats = {
total_requests: 1,
total_input_tokens: 10,
total_output_tokens: 20,
total_cache_tokens: 0,
total_tokens: 30,
total_cost: 0.1,
total_actual_cost: 0.08,
average_duration_ms: 12,
endpoints: [],
upstream_endpoints: [],
endpoint_paths: [],
}
const userMappedLog = {
id: 11,
request_id: 'req-user-mapped',
created_at: '2026-08-25T00:00:00Z',
model: 'gpt-5.4',
reasoning_effort: 'max',
actual_cost: 0.01,
total_cost: 0.01,
rate_multiplier: 1,
input_cost: 0.01,
output_cost: 0,
cache_creation_cost: 0,
cache_read_cost: 0,
input_tokens: 8,
output_tokens: 2,
cache_creation_tokens: 0,
cache_read_tokens: 0,
cache_creation_5m_tokens: 0,
cache_creation_1h_tokens: 0,
image_count: 0,
billing_mode: 'token',
request_type: 'sync',
stream: false,
api_key: { name: 'user-key' },
}
const adminMappedLog = {
...userMappedLog,
request_id: 'req-admin-mapped',
user_id: 7,
user: { email: 'liu.jialin@code-dance.com' },
account: { name: 'codex-wang' },
upstream_reasoning_effort: 'xhigh',
}
const sharedPageStubs = {
AppLayout: layoutStub,
Pagination: true,
Select: true,
DateRangePicker: true,
Icon: iconStub,
UsageStatsCards: chartStub,
ModelDistributionChart: chartStub,
GroupDistributionChart: chartStub,
EndpointDistributionChart: chartStub,
TokenUsageTrend: chartStub,
IpGeoCell: true,
EmptyState: true,
}
function reasoningCellText(wrapper: ReturnType<typeof mount>): string {
return wrapper.get('[data-testid="reasoning-effort-cell"]').text()
}
describe('usage reasoning effort page display', () => {
beforeEach(() => {
localStorage.clear()
userQuery.mockReset().mockResolvedValue({ items: [userMappedLog], total: 1, pages: 1 })
userGetStats.mockReset().mockResolvedValue(emptyStats)
userGetDashboardModels.mockReset().mockResolvedValue({ models: [], start_date: '2026-08-25', end_date: '2026-08-25' })
userGetDashboardSnapshotV2.mockReset().mockResolvedValue({
generated_at: '2026-08-25T00:00:00Z',
start_date: '2026-08-25',
end_date: '2026-08-25',
granularity: 'hour',
trend: [],
groups: [],
})
userKeysList.mockReset().mockResolvedValue({ items: [{ id: 1, name: 'user-key' }] })
userGroupsGetAvailable.mockReset().mockResolvedValue([{ id: 1, name: 'default' }])
adminList.mockReset().mockResolvedValue({ items: [adminMappedLog], total: 1, pages: 1 })
adminGetStats.mockReset().mockResolvedValue(emptyStats)
adminGetSnapshotV2.mockReset().mockResolvedValue({ trend: [], models: [], groups: [] })
adminGetModelStats.mockReset().mockResolvedValue({ models: [] })
adminGetById.mockReset().mockResolvedValue({ id: 7, email: 'liu.jialin@code-dance.com' })
listErrorLogs.mockReset().mockResolvedValue({ items: [], total: 0 })
})
afterEach(() => {
localStorage.clear()
})
it('user usage page only shows the requested reasoning effort', async () => {
const wrapper = mount(UserUsageView, {
global: { stubs: sharedPageStubs },
})
await flushPromises()
const cell = reasoningCellText(wrapper)
expect(cell).toContain('Max')
expect(cell).not.toContain('XHigh')
expect(cell).not.toContain('↳')
expect(wrapper.text()).not.toContain('XHigh')
})
it('admin usage page shows requested and mapped effort after the column is enabled', async () => {
const wrapper = mount(AdminUsageView, {
global: {
stubs: {
...sharedPageStubs,
UsageFilters: adminFiltersStub,
UsageExportProgress: true,
UsageCleanupDialog: true,
UserBalanceHistoryModal: true,
UserTokenRanking: true,
OpsErrorLogTable: true,
OpsErrorDetailModal: true,
},
},
})
await flushPromises()
expect(wrapper.find('[data-testid="reasoning-effort-cell"]').exists()).toBe(false)
await wrapper.get('[data-testid="usage-column-settings"]').trigger('click')
await wrapper.get('[data-testid="usage-column-toggle-reasoning_effort"]').trigger('click')
await flushPromises()
const cell = reasoningCellText(wrapper)
expect(cell).toContain('Max')
expect(cell).toContain('XHigh')
expect(cell).toContain('↳')
})
it('admin usage page shows a single value when reasoning effort was not mapped', async () => {
adminList.mockResolvedValue({
items: [{ ...adminMappedLog, request_id: 'req-admin-plain', reasoning_effort: 'high', upstream_reasoning_effort: null }],
total: 1,
pages: 1,
})
const wrapper = mount(AdminUsageView, {
global: {
stubs: {
...sharedPageStubs,
UsageFilters: adminFiltersStub,
UsageExportProgress: true,
UsageCleanupDialog: true,
UserBalanceHistoryModal: true,
UserTokenRanking: true,
OpsErrorLogTable: true,
OpsErrorDetailModal: true,
},
},
})
await flushPromises()
await wrapper.get('[data-testid="usage-column-settings"]').trigger('click')
await wrapper.get('[data-testid="usage-column-toggle-reasoning_effort"]').trigger('click')
await flushPromises()
const cell = reasoningCellText(wrapper)
expect(cell).toContain('High')
expect(cell).not.toContain('↳')
})
})
@@ -91,7 +91,7 @@
</template>
<template #cell-reasoning_effort="{ row }">
<div v-if="hasReasoningEffortMapping(row)" class="space-y-0.5 text-xs">
<div v-if="hasReasoningEffortMapping(row)" data-testid="reasoning-effort-cell" class="space-y-0.5 text-xs">
<div class="font-medium text-gray-900 dark:text-white">
{{ formatReasoningEffort(row.reasoning_effort) }}
</div>
@@ -99,7 +99,7 @@
<span class="mr-0.5"></span>{{ formatReasoningEffort(row.upstream_reasoning_effort) }}
</div>
</div>
<span v-else class="text-sm text-gray-900 dark:text-white">
<span v-else data-testid="reasoning-effort-cell" class="text-sm text-gray-900 dark:text-white">
{{ formatReasoningEffort(row.reasoning_effort) }}
</span>
</template>
+2
View File
@@ -87,6 +87,7 @@
<template #after-reset>
<div v-if="activeTab !== 'ranking'" class="relative" ref="columnDropdownRef">
<button
data-testid="usage-column-settings"
@click="showColumnDropdown = !showColumnDropdown"
class="btn btn-secondary px-2 md:px-3"
:title="t('admin.users.columnSettings')"
@@ -103,6 +104,7 @@
<button
v-for="col in currentToggleableColumns"
:key="col.key"
:data-testid="`usage-column-toggle-${col.key}`"
@click="toggleCurrentColumn(col.key)"
class="flex w-full items-center justify-between px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dark-700"
>
+2
View File
@@ -131,6 +131,7 @@
<div class="relative" ref="columnDropdownRef">
<button
type="button"
data-testid="usage-column-settings"
@click="showColumnDropdown = !showColumnDropdown"
class="btn btn-secondary px-2 md:px-3"
:title="t('admin.users.columnSettings')"
@@ -146,6 +147,7 @@
v-for="col in currentToggleableColumns"
:key="col.key"
type="button"
:data-testid="`usage-column-toggle-${col.key}`"
@click="toggleCurrentColumn(col.key)"
class="flex w-full items-center justify-between px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dark-700"
>