Merge PR #469: 修复用户可见性、额度、验证与 Codex 探测

This commit is contained in:
fawney19
2026-05-16 13:53:25 +08:00
36 changed files with 1895 additions and 430 deletions

View File

@@ -0,0 +1,169 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h, nextTick, type App } from 'vue'
import Dashboard from '../Dashboard.vue'
const dashboardApiMocks = vi.hoisted(() => ({
getStats: vi.fn(),
getDailyStats: vi.fn(),
}))
vi.mock('@/stores/auth', () => ({
useAuthStore: () => ({
canAccessAdmin: false,
isAdmin: false,
isAuditAdmin: false,
}),
}))
vi.mock('@/api/dashboard', () => ({
dashboardApi: dashboardApiMocks,
}))
vi.mock('@/api/announcements', () => ({
announcementApi: {
getAnnouncements: vi.fn().mockResolvedValue({ items: [] }),
markAsRead: vi.fn().mockResolvedValue({}),
},
}))
vi.mock('@/components/charts/BarChart.vue', async () => {
const { defineComponent, h } = await import('vue')
return { default: defineComponent({ name: 'BarChartStub', setup: () => () => h('div') }) }
})
vi.mock('@/components/charts/DoughnutChart.vue', async () => {
const { defineComponent, h } = await import('vue')
return { default: defineComponent({ name: 'DoughnutChartStub', setup: () => () => h('div') }) }
})
vi.mock('@/components/charts/LineChart.vue', async () => {
const { defineComponent, h } = await import('vue')
return { default: defineComponent({ name: 'LineChartStub', setup: () => () => h('div') }) }
})
vi.mock('@/components/common', async () => {
const { defineComponent, h } = await import('vue')
return {
TimeRangePicker: defineComponent({
name: 'TimeRangePickerStub',
setup() {
return () => h('div')
},
}),
}
})
vi.mock('@/components/ui', async () => {
const { defineComponent, h } = await import('vue')
const passthrough = (name: string, tag = 'div') => defineComponent({
name,
setup(_, { slots }) {
return () => h(tag, slots.default?.())
},
})
return {
Card: passthrough('CardStub', 'section'),
Badge: passthrough('BadgeStub', 'span'),
Button: passthrough('ButtonStub', 'button'),
Skeleton: defineComponent({ name: 'SkeletonStub', setup: () => () => h('div') }),
Dialog: passthrough('DialogStub'),
Table: passthrough('TableStub', 'table'),
TableHeader: passthrough('TableHeaderStub', 'thead'),
TableBody: passthrough('TableBodyStub', 'tbody'),
TableRow: passthrough('TableRowStub', 'tr'),
TableHead: passthrough('TableHeadStub', 'th'),
TableCell: passthrough('TableCellStub', 'td'),
}
})
vi.mock('lucide-vue-next', async () => {
const Icon = defineComponent({
name: 'IconStub',
setup() {
return () => h('span')
},
})
return {
Users: Icon,
Activity: Icon,
TrendingUp: Icon,
DollarSign: Icon,
Key: Icon,
Hash: Icon,
Zap: Icon,
Bell: Icon,
AlertCircle: Icon,
AlertTriangle: Icon,
Info: Icon,
Wrench: Icon,
Loader2: Icon,
Clock: Icon,
Database: Icon,
Shuffle: Icon,
}
})
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
function mountDashboard() {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(Dashboard)
app.mount(root)
mountedApps.push({ app, root })
return root
}
async function settle() {
for (let index = 0; index < 8; index += 1) {
await Promise.resolve()
await nextTick()
}
}
beforeEach(() => {
dashboardApiMocks.getStats.mockReset()
dashboardApiMocks.getDailyStats.mockReset()
dashboardApiMocks.getDailyStats.mockResolvedValue({
daily_stats: [],
model_summary: [],
period: { start_date: '2026-05-01', end_date: '2026-05-15', days: 15 },
})
})
afterEach(() => {
for (const { app, root } of mountedApps.splice(0)) {
app.unmount()
root.remove()
}
document.body.innerHTML = ''
})
describe('Dashboard ordinary user wallet card', () => {
it('renders package and wallet balance split from mocked stats', async () => {
dashboardApiMocks.getStats.mockResolvedValue({
stats: [
{ name: 'API 密钥', value: '0', subValue: '活跃 0', icon: 'Activity' },
{ name: '本月请求', value: '0', subValue: '今日 0', icon: 'Users' },
{
name: '钱包余额',
value: '$110.00',
subValue: '套餐额度 $100.00 · 钱包余额 $10.00',
icon: 'DollarSign',
},
{ name: '本月 Token', value: '0', subValue: '输入 0 / 输出 0', icon: 'Zap' },
],
today: { requests: 0, tokens: 0, cost: 0 },
cache_stats: { cache_creation_tokens: 0, cache_read_tokens: 0, total_cache_tokens: 0 },
token_breakdown: { input: 0, output: 0, cache_creation: 0, cache_read: 0 },
monthly_cost: 0,
})
const root = mountDashboard()
await settle()
expect(root.textContent).toContain('$110.00')
expect(root.textContent).toContain('套餐额度 $100.00 · 钱包余额 $10.00')
})
})

View File

@@ -0,0 +1,69 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, nextTick, type App } from 'vue'
import type { PublicGlobalModel } from '@/api/public-models'
import UserModelDetailDrawer from '../components/UserModelDetailDrawer.vue'
vi.mock('@/composables/useClipboard', () => ({
useClipboard: () => ({
copyToClipboard: vi.fn(),
}),
}))
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
function model(overrides: Partial<PublicGlobalModel> = {}): PublicGlobalModel {
return {
id: 'gm-test',
name: 'gpt-5',
display_name: 'GPT 5',
is_active: true,
default_tiered_pricing: null,
default_price_per_request: null,
supported_capabilities: ['chat'],
config: null,
usage_count: 0,
...overrides,
}
}
function mountDrawer(selectedModel: PublicGlobalModel) {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(UserModelDetailDrawer, {
open: true,
model: selectedModel,
'onUpdate:open': vi.fn(),
})
app.mount(root)
mountedApps.push({ app, root })
return root
}
afterEach(() => {
for (const { app, root } of mountedApps.splice(0)) {
app.unmount()
root.remove()
}
document.body.innerHTML = ''
})
describe('user model catalog detail drawer', () => {
it('does not render model mapping fields for ordinary users', async () => {
mountDrawer(model({
config: {
description: 'User visible description',
model_mappings: ['gpt-5-upstream'],
provider_model_mappings: [{ name: 'provider-gpt-5' }],
},
}))
await nextTick()
const text = document.body.textContent || ''
expect(text).toContain('GPT 5')
expect(text).toContain('User visible description')
expect(text).not.toContain('模型映射')
expect(text).not.toContain('gpt-5-upstream')
expect(text).not.toContain('provider-gpt-5')
})
})