mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 17:07:46 +08:00
697 lines
23 KiB
TypeScript
697 lines
23 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||
import { createApp, defineComponent, h, nextTick, ref, type App, type Ref } from 'vue'
|
||
|
||
import type { RequestDetail } from '@/api/dashboard'
|
||
import RequestDetailDrawer from '../RequestDetailDrawer.vue'
|
||
|
||
const apiMocks = vi.hoisted(() => ({
|
||
getRequestDetail: vi.fn(),
|
||
}))
|
||
|
||
vi.mock('@/api/dashboard', async (importOriginal) => {
|
||
const actual = await importOriginal<typeof import('@/api/dashboard')>()
|
||
return {
|
||
...actual,
|
||
dashboardApi: {
|
||
...actual.dashboardApi,
|
||
getRequestDetail: apiMocks.getRequestDetail,
|
||
},
|
||
}
|
||
})
|
||
|
||
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
|
||
|
||
afterEach(() => {
|
||
for (const { app, root } of mountedApps.splice(0)) {
|
||
app.unmount()
|
||
root.remove()
|
||
}
|
||
apiMocks.getRequestDetail.mockReset()
|
||
})
|
||
|
||
function buildEmbeddingDetail(): RequestDetail {
|
||
return {
|
||
id: 'usage-embedding-1',
|
||
request_id: 'req-embedding-1',
|
||
user: {
|
||
id: 'user-1',
|
||
username: 'embedding-user',
|
||
email: '[email protected]',
|
||
},
|
||
api_key: {
|
||
id: 'key-1',
|
||
name: 'test-key',
|
||
display: 'test-key',
|
||
},
|
||
provider: 'embedding-provider',
|
||
model: 'embedding-model',
|
||
tokens: { input: 100, output: 0, total: 100 },
|
||
cost: { input: 0.00001, output: 0, total: 0.00001 },
|
||
request_type: 'embedding',
|
||
is_stream: false,
|
||
status: 'completed',
|
||
status_code: 200,
|
||
response_time_ms: 10,
|
||
created_at: '2026-07-16T00:00:00Z',
|
||
request_headers: { 'content-type': 'application/json' },
|
||
settlement: {
|
||
settlement_snapshot: {
|
||
pricing_snapshot: {
|
||
pricing_source: 'global_default',
|
||
tiered_pricing: {
|
||
tiers: [{ up_to: null, input_price_per_1m: 0.1 }],
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
function buildFastTierDetail(): RequestDetail {
|
||
return {
|
||
...buildEmbeddingDetail(),
|
||
id: 'usage-fast-tier-1',
|
||
request_id: 'req-fast-tier-1',
|
||
model: 'gpt-5.6-sol',
|
||
service_tier: 'fast',
|
||
tokens: { input: 1116, output: 3028, total: 4144 },
|
||
input_tokens: 1116,
|
||
output_tokens: 3028,
|
||
cache_read_input_tokens: 205568,
|
||
input_cost: 0.01395,
|
||
output_cost: 0.2271,
|
||
cache_creation_cost: 0,
|
||
cache_read_cost: 0.25696,
|
||
cost: { input: 0.01395, output: 0.2271, total: 0.49801 },
|
||
settlement: {
|
||
settlement_snapshot: {
|
||
pricing_snapshot: {
|
||
billing_processing_tier: 'fast',
|
||
processing_tier_price_multiplier: 2.5,
|
||
tiered_pricing: {
|
||
tiers: [{
|
||
up_to: null,
|
||
input_price_per_1m: 12.5,
|
||
output_price_per_1m: 75,
|
||
cache_creation_price_per_1m: 15.625,
|
||
cache_read_price_per_1m: 1.25,
|
||
cache_ttl_pricing: [
|
||
{ ttl_minutes: 5, cache_creation_price_per_1m: 15.625 },
|
||
{ ttl_minutes: 60, cache_creation_price_per_1m: 31.25 },
|
||
],
|
||
}],
|
||
},
|
||
},
|
||
},
|
||
billing_snapshot: {
|
||
resolved_dimensions: {
|
||
cache_ttl_minutes: 30,
|
||
},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
describe('RequestDetailDrawer settlement pricing', () => {
|
||
it('labels an unmetered OpenAI Live WebSocket detail without rendering zero usage as billing', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue({
|
||
...buildEmbeddingDetail(),
|
||
id: 'usage-live-ws-1',
|
||
request_id: 'req-live-ws-1',
|
||
tokens: { input: 0, output: 0, total: 0 },
|
||
input_tokens: 0,
|
||
output_tokens: 0,
|
||
total_tokens: 0,
|
||
cost: { input: 0, output: 0, total: 0 },
|
||
total_cost: 0,
|
||
is_stream: true,
|
||
is_websocket: true,
|
||
websocket_transport: 'codex_live_sideband',
|
||
usage_available: false,
|
||
} satisfies RequestDetail)
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-live-ws-1',
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
await vi.waitFor(() => {
|
||
const transport = document.body.querySelector<HTMLElement>('[data-usage-transport="websocket"]')
|
||
expect(transport?.textContent?.trim()).toBe('WS')
|
||
expect(transport?.title).toBe('OpenAI Live Sideband WebSocket')
|
||
expect(document.body.querySelector('[data-usage-transport="http"]')).toBeNull()
|
||
expect(document.body.querySelector('[data-request-lifecycle-status]')?.textContent?.trim())
|
||
.toBe('200')
|
||
expect(document.body.querySelector('[data-request-detail-usage-unavailable]')).not.toBeNull()
|
||
})
|
||
|
||
const totals = [...document.body.querySelectorAll<HTMLElement>('[data-request-detail-total-cost]')]
|
||
expect(totals.length).toBe(2)
|
||
expect(totals.every(total => total.textContent?.trim() === '不可用')).toBe(true)
|
||
})
|
||
|
||
it('shows end-to-end latency while keeping output TPS scoped to candidate timing', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue({
|
||
...buildEmbeddingDetail(),
|
||
tokens: { input: 100, output: 50, total: 150 },
|
||
input_tokens: 100,
|
||
output_tokens: 50,
|
||
total_tokens: 150,
|
||
is_stream: true,
|
||
upstream_is_stream: true,
|
||
response_time_ms: 626,
|
||
first_byte_time_ms: 100,
|
||
end_to_end_time_ms: 10_626,
|
||
end_to_end_first_byte_time_ms: 10_120,
|
||
} satisfies RequestDetail)
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-embedding-1',
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
await vi.waitFor(() => {
|
||
expect(document.body.querySelector('[data-usage-transport="http"]')?.textContent?.trim())
|
||
.toBe('HTTP 流式')
|
||
expect(document.body.textContent).toContain('10.12s / 10.63s')
|
||
expect(document.body.textContent).toContain('95.1tps')
|
||
expect(document.body.textContent).not.toContain('98.8tps')
|
||
})
|
||
})
|
||
|
||
it('renders an input-only embedding tier without treating the missing output price as zero', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue(buildEmbeddingDetail())
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-embedding-1',
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
await vi.waitFor(() => {
|
||
expect(document.body.querySelector('[data-usage-transport="http"]')?.textContent?.trim())
|
||
.toBe('HTTP 标准')
|
||
expect(document.body.textContent).toContain('输入 $0.1/M')
|
||
expect(document.body.textContent).toContain('输出 -')
|
||
})
|
||
expect(document.body.textContent).not.toContain('输出 $0/M')
|
||
})
|
||
|
||
it('shows Fast pricing as the base token cost multiplied by the Fast tier', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue(buildFastTierDetail())
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-fast-tier-1',
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
await vi.waitFor(() => {
|
||
expect(document.body.textContent).toContain('$0.199204 × 2.5 (Fast 层级)')
|
||
expect(document.body.textContent).toContain('输入 $5/M')
|
||
expect(document.body.textContent).toContain('输出 $30/M')
|
||
expect(document.body.textContent).toContain('缓存创建 $6.25/M')
|
||
expect(document.body.textContent).toContain('缓存读取 $0.5/M')
|
||
expect(document.body.textContent).not.toContain('缓存创建(30min)')
|
||
expect(document.body.querySelector('[data-testid="service-tier-facts"]')).toBeNull()
|
||
})
|
||
})
|
||
|
||
it('shows the compact request badge in the model header', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue({
|
||
...buildEmbeddingDetail(),
|
||
id: 'usage-compact-1',
|
||
request_id: 'req-compact-1',
|
||
request_type: 'compact',
|
||
})
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-compact-1',
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
await vi.waitFor(() => {
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="compact"]')?.textContent?.trim())
|
||
.toBe('会话压缩')
|
||
})
|
||
})
|
||
|
||
it('shows mapping, reasoning, Fast, and Cyber together in the model header', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue({
|
||
...buildEmbeddingDetail(),
|
||
id: 'usage-cyber-risk-demo',
|
||
request_id: 'req_usage-cyber-risk-demo',
|
||
model: 'gpt-5',
|
||
target_model: 'gpt-5.1',
|
||
requested_reasoning_effort: 'xhigh',
|
||
reasoning_effort: 'max',
|
||
request_body: {
|
||
model: 'gpt-5',
|
||
reasoning: { effort: 'xhigh' },
|
||
},
|
||
service_tier: 'priority',
|
||
// A response-side tier must not be used for the Fast badge or billing.
|
||
actual_service_tier: 'default',
|
||
provider_request_body: {
|
||
model: 'gpt-5.1',
|
||
reasoning: { effort: 'max' },
|
||
service_tier: 'priority',
|
||
},
|
||
status: 'failed',
|
||
status_code: 400,
|
||
error_message: 'This content was flagged for possible cybersecurity risk. To get authorized for security work, join the Trusted Access for Cyber program: https://chatgpt.com/cyber',
|
||
response_body: {
|
||
error: {
|
||
type: 'invalid_request',
|
||
message: 'This content was flagged for possible cybersecurity risk.',
|
||
code: 400,
|
||
},
|
||
},
|
||
})
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-cyber-risk-demo',
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
await vi.waitFor(() => {
|
||
expect(document.body.querySelector('[data-request-detail-model-display]')?.textContent)
|
||
.toContain('gpt-5')
|
||
expect(document.body.querySelector('[data-request-detail-model-display]')?.textContent)
|
||
.toContain('gpt-5.1')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
|
||
.toContain('xhigh -> max')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')?.textContent)
|
||
.toContain('Fast')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')?.textContent?.trim())
|
||
.toBe('Fast')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
|
||
.toContain('Cyber')
|
||
const modelLayout = document.body.querySelector(
|
||
'[data-request-detail-model-layout="inline"]',
|
||
)
|
||
const modelRow = modelLayout?.firstElementChild
|
||
expect(modelRow?.textContent).toContain('gpt-5')
|
||
expect(modelRow?.textContent).toContain('->')
|
||
expect(modelRow?.textContent).toContain('gpt-5.1')
|
||
expect(modelRow?.querySelector('[data-usage-model-mapping]')).not.toBeNull()
|
||
expect(modelRow?.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
|
||
.toContain('xhigh -> max')
|
||
expect(modelRow?.querySelector('[data-request-detail-model-badge="fast"]')?.textContent)
|
||
.toContain('Fast')
|
||
expect(modelRow?.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
|
||
.toContain('Cyber')
|
||
expect(modelLayout?.querySelector('[data-request-detail-model-badges-row]')).toBeNull()
|
||
const serviceTierFacts = document.body.querySelector('[data-testid="service-tier-facts"]')
|
||
expect([...serviceTierFacts?.querySelectorAll('dt') ?? []].map(node => node.textContent?.trim()))
|
||
.toEqual(['上游请求层级', '计费层级'])
|
||
expect([...serviceTierFacts?.querySelectorAll('dd') ?? []].map(node => node.textContent?.trim()))
|
||
.toEqual(['Fast', 'Fast'])
|
||
})
|
||
})
|
||
|
||
it('keeps the Cyber badge stable from the selected row while lightweight detail loads', async () => {
|
||
let resolveDetail!: (value: RequestDetail) => void
|
||
apiMocks.getRequestDetail.mockReturnValue(new Promise<RequestDetail>((resolve) => {
|
||
resolveDetail = resolve
|
||
}))
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const requestId = ref('usage-cyber-summary')
|
||
const summaryRecord = ref<Record<string, unknown>>({
|
||
id: 'usage-cyber-summary',
|
||
model: 'gpt-5',
|
||
target_model: 'gpt-5.1',
|
||
requested_reasoning_effort: 'xhigh',
|
||
reasoning_effort: 'max',
|
||
service_tier: 'priority',
|
||
error_message: 'This content was flagged for possible cybersecurity risk. https://chatgpt.com/cyber',
|
||
})
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: requestId.value,
|
||
summaryRecord: summaryRecord.value as never,
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
expect(apiMocks.getRequestDetail).toHaveBeenCalledTimes(1)
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
|
||
.toContain('Cyber')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')?.textContent)
|
||
.toContain('Fast')
|
||
|
||
resolveDetail({
|
||
...buildEmbeddingDetail(),
|
||
id: 'usage-cyber-summary',
|
||
request_id: 'usage-cyber-summary',
|
||
model: 'gpt-5',
|
||
// Lightweight detail can legitimately omit these final-provider facts.
|
||
target_model: null,
|
||
requested_reasoning_effort: null,
|
||
reasoning_effort: null,
|
||
service_tier: null,
|
||
status: 'failed',
|
||
status_code: 400,
|
||
error_message: 'execution runtime stream returned non-success status 400',
|
||
response_body: null,
|
||
})
|
||
await vi.waitFor(() => {
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
|
||
.toContain('Cyber')
|
||
expect(document.body.querySelector('[data-usage-model-mapping]')?.textContent)
|
||
.toContain('gpt-5.1')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
|
||
.toContain('xhigh -> max')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')?.textContent)
|
||
.toContain('Fast')
|
||
const tierValues = [
|
||
...document.body.querySelectorAll('[data-testid="service-tier-facts"] dd'),
|
||
].map(node => node.textContent?.trim())
|
||
expect(tierValues).toEqual(['Fast', 'Fast'])
|
||
})
|
||
})
|
||
|
||
it('clears a stale summary Cyber badge when newer detail completed successfully', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue({
|
||
...buildEmbeddingDetail(),
|
||
id: 'usage-cyber-recovered',
|
||
request_id: 'usage-cyber-recovered',
|
||
model: 'gpt-5',
|
||
status: 'completed',
|
||
status_code: 200,
|
||
error_message: undefined,
|
||
updated_at: '2026-07-17T00:00:02Z',
|
||
})
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-cyber-recovered',
|
||
summaryRecord: {
|
||
id: 'usage-cyber-recovered',
|
||
model: 'gpt-5',
|
||
input_tokens: 0,
|
||
output_tokens: 0,
|
||
total_tokens: 0,
|
||
cost: 0,
|
||
is_stream: false,
|
||
status: 'failed',
|
||
status_code: 400,
|
||
error_message: 'This content was flagged for possible cybersecurity risk. https://chatgpt.com/cyber',
|
||
created_at: '2026-07-17T00:00:00Z',
|
||
updated_at: '2026-07-17T00:00:01Z',
|
||
},
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
await vi.waitFor(() => {
|
||
expect(apiMocks.getRequestDetail).toHaveBeenCalledTimes(1)
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="cyber"]'))
|
||
.toBeNull()
|
||
})
|
||
})
|
||
|
||
it('uses populated detail fallbacks without overriding a populated summary tier', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue({
|
||
...buildEmbeddingDetail(),
|
||
id: 'usage-standard-summary',
|
||
request_id: 'usage-standard-summary',
|
||
model: 'gpt-5',
|
||
target_model: 'gpt-5.1',
|
||
requested_reasoning_effort: 'xhigh',
|
||
reasoning_effort: 'max',
|
||
// The non-empty summary tier remains authoritative over this stale fact.
|
||
service_tier: 'priority',
|
||
})
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const summaryRecord = ref<Record<string, unknown>>({
|
||
id: 'usage-standard-summary',
|
||
model: 'gpt-5',
|
||
target_model: null,
|
||
model_version: null,
|
||
requested_reasoning_effort: 'xhigh',
|
||
reasoning_effort: null,
|
||
service_tier: 'default',
|
||
})
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-standard-summary',
|
||
summaryRecord: summaryRecord.value as never,
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
await vi.waitFor(() => {
|
||
expect(apiMocks.getRequestDetail).toHaveBeenCalledTimes(1)
|
||
expect(document.body.querySelector('[data-usage-model-mapping]')?.textContent?.trim())
|
||
.toBe('映射模型gpt-5.1')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent?.trim())
|
||
.toBe('xhigh -> max')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')).toBeNull()
|
||
const tierValues = [
|
||
...document.body.querySelectorAll('[data-testid="service-tier-facts"] dd'),
|
||
].map(node => node.textContent?.trim())
|
||
expect(tierValues).toEqual(['default', 'default'])
|
||
})
|
||
})
|
||
|
||
it('does not use legacy model_version as a response-model fallback', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue({
|
||
...buildEmbeddingDetail(),
|
||
id: 'usage-version-summary',
|
||
request_id: 'usage-version-summary',
|
||
model: 'gpt-5',
|
||
target_model: null,
|
||
model_version: 'gpt-5.1-2026-07-17',
|
||
})
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-version-summary',
|
||
summaryRecord: {
|
||
id: 'usage-version-summary',
|
||
model: 'gpt-5',
|
||
target_model: null,
|
||
model_version: null,
|
||
input_tokens: 0,
|
||
output_tokens: 0,
|
||
total_tokens: 0,
|
||
cost: 0,
|
||
is_stream: false,
|
||
created_at: '2026-07-17T00:00:00Z',
|
||
},
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await nextTick()
|
||
|
||
await vi.waitFor(() => {
|
||
expect(document.body.querySelector('[data-usage-model-response]')).toBeNull()
|
||
expect(document.body.querySelector('[data-usage-model-mapping]')).toBeNull()
|
||
})
|
||
})
|
||
|
||
it('lets a newer final-provider summary clear facts cached from an earlier candidate', async () => {
|
||
apiMocks.getRequestDetail.mockResolvedValue({
|
||
...buildEmbeddingDetail(),
|
||
id: 'usage-final-candidate',
|
||
request_id: 'usage-final-candidate',
|
||
model: 'gpt-5',
|
||
target_model: 'gpt-5.1',
|
||
requested_reasoning_effort: 'xhigh',
|
||
reasoning_effort: 'max',
|
||
service_tier: 'priority',
|
||
status: 'streaming',
|
||
updated_at: '2026-07-17T00:00:01Z',
|
||
})
|
||
|
||
let isOpen!: Ref<boolean>
|
||
const summaryRecord = ref<Record<string, unknown>>({
|
||
id: 'usage-final-candidate',
|
||
model: 'gpt-5',
|
||
target_model: 'gpt-5.1',
|
||
requested_reasoning_effort: 'xhigh',
|
||
reasoning_effort: 'max',
|
||
service_tier: 'priority',
|
||
status: 'streaming',
|
||
updated_at: '2026-07-17T00:00:01Z',
|
||
})
|
||
const Host = defineComponent({
|
||
setup() {
|
||
isOpen = ref(false)
|
||
return () => h(RequestDetailDrawer, {
|
||
isOpen: isOpen.value,
|
||
requestId: 'usage-final-candidate',
|
||
summaryRecord: summaryRecord.value as never,
|
||
})
|
||
},
|
||
})
|
||
|
||
const root = document.createElement('div')
|
||
document.body.appendChild(root)
|
||
const app = createApp(Host)
|
||
app.mount(root)
|
||
mountedApps.push({ app, root })
|
||
|
||
isOpen.value = true
|
||
await vi.waitFor(() => {
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')).not.toBeNull()
|
||
})
|
||
|
||
summaryRecord.value = {
|
||
...summaryRecord.value,
|
||
target_model: null,
|
||
reasoning_effort: null,
|
||
service_tier: null,
|
||
updated_at: '2026-07-17T00:00:02Z',
|
||
}
|
||
await nextTick()
|
||
|
||
expect(document.body.querySelector('[data-usage-model-mapping]')).toBeNull()
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent?.trim())
|
||
.toBe('xhigh')
|
||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')).toBeNull()
|
||
expect([...document.body.querySelectorAll('[data-testid="service-tier-facts"] dd')]
|
||
.some(node => node.textContent?.trim() === 'Fast')).toBe(false)
|
||
})
|
||
})
|