mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 01:17:46 +08:00
fix: harden OAuth identity and cookies and correct quota and JSON display
This commit is contained in:
@@ -169,11 +169,6 @@ function getDisplayHtml(line: JsonDisplayLine): string {
|
||||
|
||||
function toggleFold(line: JsonDisplayLine, index: number) {
|
||||
const overrides = new Map(foldOverrides.value)
|
||||
if (line.collapsed) {
|
||||
for (const path of overrides.keys()) {
|
||||
if (path.startsWith(`${line.id}/`)) overrides.delete(path)
|
||||
}
|
||||
}
|
||||
overrides.set(line.id, !line.collapsed)
|
||||
foldOverrides.value = overrides
|
||||
localReader = undefined
|
||||
|
||||
@@ -10,23 +10,46 @@ const apps: Array<{ app: App, root: HTMLElement }> = []
|
||||
afterEach(() => { for (const { app, root } of apps.splice(0)) { app.unmount(); root.remove() } })
|
||||
|
||||
function mountBody(value: unknown) {
|
||||
const engine = new BodyDocumentEngine(value)
|
||||
const engine = new BodyDocumentEngine(value)
|
||||
const json = vi.fn(async (options: BodyJsonOptions) => engine.json(options))
|
||||
const bodyDocument = shallowRef({ json } as unknown as BodyDocument)
|
||||
const expandDepth = shallowRef(999)
|
||||
const errors: unknown[] = []
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(defineComponent({ setup: () => () => h(JsonContent, {
|
||||
data: null, bodyDocument: bodyDocument.value, expandDepth: 999, viewMode: 'formatted', isDark: false, emptyMessage: '无数据',
|
||||
data: null, bodyDocument: bodyDocument.value, expandDepth: expandDepth.value, viewMode: 'formatted', isDark: false, emptyMessage: '无数据',
|
||||
onLoadError: (error: unknown) => errors.push(error),
|
||||
}) }))
|
||||
app.mount(root)
|
||||
apps.push({ app, root })
|
||||
return { root, json, bodyDocument, engine, errors }
|
||||
return { root, json, bodyDocument, expandDepth, engine, errors }
|
||||
}
|
||||
function button(root: HTMLElement, label: string) { return [...root.querySelectorAll('button')].find(button => button.textContent?.includes(label))! }
|
||||
|
||||
describe('worker-backed body views', () => {
|
||||
it('resets every layer on collapse-all and reopens worker-backed nodes one layer at a time', async () => {
|
||||
const value = { messages: [{ content: { text: 'deep worker content' } }] }
|
||||
const { root, json, expandDepth, engine } = mountBody(value)
|
||||
await vi.waitFor(() => expect(root.textContent).toContain('deep worker content'))
|
||||
for (let cycle = 0; cycle < 2; cycle += 1) {
|
||||
expandDepth.value = 0
|
||||
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(3))
|
||||
expect(json).toHaveBeenLastCalledWith(expect.objectContaining({ expandDepth: 0, foldOverrides: new Map() }))
|
||||
for (const lineCount of [5, 7, 9]) {
|
||||
expect(root.textContent).not.toContain('deep worker content')
|
||||
root.querySelector<HTMLButtonElement>('button[aria-label="展开节点"]')!.click()
|
||||
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(lineCount))
|
||||
}
|
||||
expect(root.textContent).toContain('deep worker content')
|
||||
expandDepth.value = 999
|
||||
await vi.waitFor(() => expect(json).toHaveBeenLastCalledWith(expect.objectContaining({ expandDepth: 999, foldOverrides: new Map() })))
|
||||
await vi.waitFor(() => expect(root.textContent).toContain('deep worker content'))
|
||||
expect(root.querySelector('button[aria-label="展开节点"]')).toBeNull()
|
||||
}
|
||||
expect(JSON.parse(engine.copy())).toEqual(value)
|
||||
})
|
||||
|
||||
it('automatically reads worker chunks while scrolling and preserves the complete copy', async () => {
|
||||
const values = Array.from({ length: 1000 }, (_value, index) => `value-${index}`)
|
||||
const { root, json, engine } = mountBody(values)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h, shallowRef, type App } from 'vue'
|
||||
import JsonContent from '../RequestDetailDrawer/JsonContent.vue'
|
||||
import JsonContentPanel from '../JsonContentPanel.vue'
|
||||
import { JSON_PAGE_SIZE, JSON_SCROLL_CHUNK_SIZE, JSON_TEXT_CHUNK_SIZE } from '../../utils/json-viewer'
|
||||
|
||||
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
|
||||
@@ -48,15 +49,57 @@ describe('JsonContent lazy rendering', () => {
|
||||
expect(root.querySelector<HTMLElement>('.virtual-body-scroll')?.scrollTop).toBe(0)
|
||||
})
|
||||
|
||||
it('opens the complete subtree with one bracket click', async () => {
|
||||
const { root } = mountJson({ messages: [{ content: 'hidden content' }] }, 0)
|
||||
it('opens one layer per bracket click after collapsing all descendants', async () => {
|
||||
const { root } = mountJson({ messages: [{ content: { text: 'hidden content' } }] }, 0)
|
||||
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(3))
|
||||
expect(root.textContent).not.toContain('hidden content')
|
||||
root.querySelector<HTMLButtonElement>('button[aria-label="展开节点"]')!.click()
|
||||
for (const lineCount of [5, 7, 9]) {
|
||||
expect(root.textContent).not.toContain('hidden content')
|
||||
root.querySelector<HTMLElement>('.line-content.clickable-collapsed')!.click()
|
||||
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(lineCount))
|
||||
}
|
||||
await vi.waitFor(() => expect(root.textContent).toContain('hidden content'))
|
||||
expect(root.querySelector('button[aria-label="展开节点"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('collapses every layer through the toolbar and keeps expand-all working', async () => {
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(defineComponent({ setup: () => () => h(JsonContentPanel, {
|
||||
data: { messages: [{ content: { text: 'deep content' } }] }, isDark: false,
|
||||
}) }))
|
||||
app.mount(root)
|
||||
mountedApps.push({ app, root })
|
||||
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(3))
|
||||
for (let cycle = 0; cycle < 2; cycle += 1) {
|
||||
root.querySelector<HTMLButtonElement>('button[title="展开全部"]')!.click()
|
||||
await vi.waitFor(() => expect(root.textContent).toContain('deep content'))
|
||||
expect(root.querySelector('button[aria-label="展开节点"]')).toBeNull()
|
||||
root.querySelector<HTMLButtonElement>('button[title="收缩全部"]')!.click()
|
||||
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(3))
|
||||
for (const lineCount of [5, 7]) {
|
||||
root.querySelector<HTMLButtonElement>('button[aria-label="展开节点"]')!.click()
|
||||
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(lineCount))
|
||||
expect(root.textContent).not.toContain('deep content')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves an explicitly folded child when its parent is reopened', async () => {
|
||||
const { root } = mountJson({ messages: [{ content: { text: 'hidden content' } }] })
|
||||
const nodeButton = (key: string) => [...root.querySelectorAll('.json-line')]
|
||||
.find(line => line.querySelector('.token-key')?.textContent === JSON.stringify(key))!
|
||||
.querySelector<HTMLButtonElement>('button')!
|
||||
await vi.waitFor(() => expect(root.textContent).toContain('hidden content'))
|
||||
nodeButton('content').click()
|
||||
await vi.waitFor(() => expect(nodeButton('content').getAttribute('aria-expanded')).toBe('false'))
|
||||
nodeButton('messages').click()
|
||||
await vi.waitFor(() => expect(nodeButton('messages').getAttribute('aria-expanded')).toBe('false'))
|
||||
nodeButton('messages').click()
|
||||
await vi.waitFor(() => expect(nodeButton('messages').getAttribute('aria-expanded')).toBe('true'))
|
||||
expect(nodeButton('content').getAttribute('aria-expanded')).toBe('false')
|
||||
expect(root.textContent).not.toContain('hidden content')
|
||||
})
|
||||
|
||||
it('shows complete long strings without an extra expansion button', async () => {
|
||||
const content = `${'x'.repeat(JSON_TEXT_CHUNK_SIZE * 10) }末尾🙂`
|
||||
const { root, data } = mountJson({ content })
|
||||
|
||||
@@ -65,11 +65,34 @@ describe('lazy JSON pages', () => {
|
||||
expect(reader.read(21).lines).toEqual([])
|
||||
})
|
||||
|
||||
it('expands all descendants of an opened node while honoring an explicitly closed child', () => {
|
||||
it.each([false, true])('opens collapsed descendants one layer at a time with indexPaths=%s', indexPaths => {
|
||||
const data = { messages: [{ content: { text: 'complete content' } }] }
|
||||
const opened = getJsonPage(data, { expandDepth: 1, foldOverrides: new Map([['$/messages', false]]) })
|
||||
const paths = indexPaths ? ['$/0', '$/0/0', '$/0/0/0'] : ['$/messages', '$/messages/0', '$/messages/0/content']
|
||||
const foldOverrides = new Map<string, boolean>()
|
||||
for (const path of paths) {
|
||||
const folded = getJsonPage(data, { expandDepth: 0, foldOverrides, indexPaths })
|
||||
expect(folded.lines.find(line => line.id === path)).toMatchObject({ collapsed: true })
|
||||
expect(folded.lines.some(line => line.value === 'complete content')).toBe(false)
|
||||
foldOverrides.set(path, false)
|
||||
}
|
||||
const opened = getJsonPage(data, { expandDepth: 0, foldOverrides, indexPaths })
|
||||
expect(opened.lines.some(line => line.value === 'complete content')).toBe(true)
|
||||
const folded = getJsonPage(data, { expandDepth: 1, foldOverrides: new Map([['$/messages', false], ['$/messages/0/content', true]]) })
|
||||
})
|
||||
|
||||
it('does not read unopened descendants after opening their parent', () => {
|
||||
const readContent = vi.fn(() => { throw new Error('unopened content must not be read') })
|
||||
const message = Object.defineProperty({}, 'content', { enumerable: true, get: readContent })
|
||||
const opened = getJsonPage({ messages: [message] }, { expandDepth: 0, foldOverrides: new Map([['$/messages', false]]) })
|
||||
expect(opened.lines.find(line => line.id === '$/messages/0')).toMatchObject({ collapsed: true })
|
||||
expect(readContent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still expands every layer in expand-all mode while honoring explicitly closed children', () => {
|
||||
const data = { messages: [{ content: { text: 'complete content' } }] }
|
||||
const expanded = getJsonPage(data, { expandDepth: 999 })
|
||||
expect(expanded.lines.some(line => line.value === 'complete content')).toBe(true)
|
||||
expect(expanded.lines.filter(line => line.canFold).every(line => !line.collapsed)).toBe(true)
|
||||
const folded = getJsonPage(data, { expandDepth: 999, foldOverrides: new Map([['$/messages', false], ['$/messages/0/content', true]]) })
|
||||
expect(folded.lines.some(line => line.value === 'complete content')).toBe(false)
|
||||
})
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ function* splitJsonLine(line: JsonTreeLine, chunkSize: number): Generator<JsonTr
|
||||
function* walkJsonLines(data: unknown, options: JsonPageOptions): Generator<JsonDisplayLine> {
|
||||
const depth = options.expandDepth === 0 || options.expandDepth == null ? 1 : options.expandDepth
|
||||
|
||||
function* walk(value: unknown, path: string, indent: number, comma: string, key?: string, expandedParent = false): Generator<JsonTreeLine> {
|
||||
function* walk(value: unknown, path: string, indent: number, comma: string, key?: string): Generator<JsonTreeLine> {
|
||||
if (value == null || typeof value !== 'object') {
|
||||
yield { id: path, indent, key, value, comma, canFold: false, collapsed: false }
|
||||
return
|
||||
@@ -140,8 +140,7 @@ function* walkJsonLines(data: unknown, options: JsonPageOptions): Generator<Json
|
||||
const keys = isArray ? [] : Object.keys(value)
|
||||
const childCount = isArray ? value.length : keys.length
|
||||
const override = options.foldOverrides?.get(path)
|
||||
const expandedSubtree = expandedParent || override === false
|
||||
const collapsed = childCount === 0 || (override ?? (!expandedSubtree && indent >= depth))
|
||||
const collapsed = childCount === 0 || (override ?? (indent >= depth))
|
||||
const bracket = isArray ? '[' : '{'
|
||||
const closingBracket = isArray ? ']' : '}'
|
||||
yield { id: path, indent, key, comma, bracket, closingBracket, childCount, isArray, collapsed, canFold: childCount > 0 }
|
||||
@@ -151,7 +150,7 @@ function* walkJsonLines(data: unknown, options: JsonPageOptions): Generator<Json
|
||||
const childKey = isArray ? String(index) : keys[index]
|
||||
const childPath = `${path}/${options.indexPaths ? index : childKey.replace(/~/g, '~0').replace(/\//g, '~1')}`
|
||||
const childValue = isArray ? value[index] : (value as Record<string, unknown>)[childKey]
|
||||
yield* walk(childValue, childPath, indent + 1, index === childCount - 1 ? '' : ',', isArray ? undefined : childKey, expandedSubtree)
|
||||
yield* walk(childValue, childPath, indent + 1, index === childCount - 1 ? '' : ',', isArray ? undefined : childKey)
|
||||
}
|
||||
yield { id: `close:${path}`, indent, bracket: closingBracket, comma, canFold: false, collapsed: false }
|
||||
}
|
||||
|
||||
@@ -29,6 +29,85 @@ describe('getCodexQuotaWindowPresentation', () => {
|
||||
expect(windows.sort((a, b) => a.sortOrder - b.sortOrder).map(item => item.label)).toEqual(['5H', '周'])
|
||||
})
|
||||
|
||||
it('distinguishes account and model quotas with the same duration', () => {
|
||||
const windows = [
|
||||
{ code: 'weekly', label: '周', scope: 'account', window_minutes: 10_080 },
|
||||
{ code: 'additional_0_primary', label: 'gpt-reserve', scope: 'model', window_minutes: 10_080 },
|
||||
]
|
||||
|
||||
expect(windows.map(window => getCodexQuotaWindowPresentation(window)?.label))
|
||||
.toEqual(['周', 'gpt-reserve 周'])
|
||||
expect(getCodexQuotaWindowLimitLabel(windows[1])).toBe('gpt-reserve 周限额')
|
||||
})
|
||||
|
||||
it.each([
|
||||
[300, 'gpt-reserve 5H'],
|
||||
[10_080, 'gpt-reserve 周'],
|
||||
[43_800, 'gpt-reserve 月'],
|
||||
])('includes the model name for a %i-minute window', (windowMinutes, expectedLabel) => {
|
||||
expect(getCodexQuotaWindowPresentation({
|
||||
code: 'additional_0_primary',
|
||||
label: ' gpt-reserve ',
|
||||
scope: ' Model ',
|
||||
window_minutes: windowMinutes,
|
||||
})?.label).toBe(expectedLabel)
|
||||
})
|
||||
|
||||
it.each(['model', 'quota_group_label', 'quota_group'] as const)(
|
||||
'falls back to %s when a model quota label is blank',
|
||||
(field) => {
|
||||
expect(getCodexQuotaWindowPresentation({
|
||||
code: 'additional_0_primary',
|
||||
label: ' ',
|
||||
scope: 'model',
|
||||
[field]: 'gpt-reserve',
|
||||
window_minutes: 10_080,
|
||||
})?.label).toBe('gpt-reserve 周')
|
||||
},
|
||||
)
|
||||
|
||||
it('keeps model identities when legacy snapshots have no window duration', () => {
|
||||
expect(getCodexQuotaWindowPresentation({
|
||||
code: 'additional_0_primary',
|
||||
label: 'gpt-reserve',
|
||||
scope: 'model',
|
||||
})?.label).toBe('gpt-reserve')
|
||||
expect(getCodexQuotaWindowPresentation({
|
||||
code: 'additional_0_primary',
|
||||
model: 'gpt-reserve',
|
||||
scope: 'model',
|
||||
})?.label).toBe('gpt-reserve')
|
||||
expect(getCodexQuotaWindowPresentation({
|
||||
code: 'weekly',
|
||||
label: 'gpt-reserve',
|
||||
scope: 'model',
|
||||
})?.label).toBe('gpt-reserve 周')
|
||||
})
|
||||
|
||||
it('uses the code when a model quota has no other identity', () => {
|
||||
expect(getCodexQuotaWindowPresentation({
|
||||
code: 'model-reserve',
|
||||
scope: 'model',
|
||||
window_minutes: 10_080,
|
||||
})?.label).toBe('model-reserve 周')
|
||||
})
|
||||
|
||||
it('preserves Spark formatting without stripping other model names', () => {
|
||||
expect(getCodexQuotaWindowPresentation({
|
||||
code: 'spark_weekly',
|
||||
label: 'Spark 周',
|
||||
window_minutes: 10_080,
|
||||
})).toEqual({ label: 'Spark周', sortOrder: 10_010_080 })
|
||||
expect(getCodexQuotaWindowPresentation({ code: 'spark_5h', label: 'Spark 5H' })?.label)
|
||||
.toBe('Spark5H')
|
||||
expect(getCodexQuotaWindowPresentation({
|
||||
code: 'additional_0_primary',
|
||||
label: 'Spark Reserve',
|
||||
scope: 'model',
|
||||
window_minutes: 10_080,
|
||||
})?.label).toBe('Spark Reserve 周')
|
||||
})
|
||||
|
||||
it('builds the provider limit label from the actual window duration', () => {
|
||||
expect(getCodexQuotaWindowLimitLabel({ code: 'weekly', window_minutes: 10_080 })).toBe('周限额')
|
||||
expect(getCodexQuotaWindowLimitLabel({ code: 'weekly', window_minutes: 43_800 })).toBe('月限额')
|
||||
|
||||
@@ -71,6 +71,37 @@ describe('providerKeyQuota', () => {
|
||||
}, 'codex')).toBe('月剩余 86.0%')
|
||||
})
|
||||
|
||||
it('keeps account and model Codex weekly quotas distinct in display text', () => {
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
oauth: { code: 'valid' },
|
||||
account: { code: 'ok', blocked: false },
|
||||
quota: {
|
||||
provider_type: 'codex',
|
||||
code: 'ok',
|
||||
exhausted: false,
|
||||
windows: [
|
||||
{
|
||||
code: 'weekly',
|
||||
label: '周',
|
||||
scope: 'account',
|
||||
window_minutes: 10_080,
|
||||
remaining_ratio: 0.9,
|
||||
},
|
||||
{
|
||||
code: 'additional_0_primary',
|
||||
label: 'gpt-reserve',
|
||||
scope: 'model',
|
||||
model: 'gpt-reserve',
|
||||
window_minutes: 10_080,
|
||||
remaining_ratio: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}, 'codex')).toBe('周剩余 90.0% | gpt-reserve 周剩余 40.0%')
|
||||
})
|
||||
|
||||
it('formats Grok account quota from structured quota windows', () => {
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
|
||||
@@ -41,7 +41,13 @@ export function getCodexQuotaWindowPresentation(
|
||||
const code = String(window.code || '').trim().toLowerCase()
|
||||
const isSpark = code.startsWith('spark_')
|
||||
const baseCode = isSpark ? code.slice('spark_'.length) : code
|
||||
const rawLabel = String(window.label || '').trim().replace(/^Spark\s*/i, '')
|
||||
const rawLabel = String(window.label || '').trim()
|
||||
const modelLabel = String(window.scope || '').trim().toLowerCase() === 'model' && !isSpark
|
||||
? [rawLabel, window.model, window.quota_group_label, window.quota_group, window.code]
|
||||
.map(value => String(value || '').trim())
|
||||
.find(Boolean)
|
||||
: undefined
|
||||
const legacyLabel = isSpark ? rawLabel.replace(/^Spark\s*/i, '') : rawLabel || modelLabel || ''
|
||||
const hasExplicitWindowMinutes = window.window_minutes != null
|
||||
const windowMinutes = Number(window.window_minutes)
|
||||
|
||||
@@ -51,12 +57,19 @@ export function getCodexQuotaWindowPresentation(
|
||||
|
||||
const period = hasExplicitWindowMinutes
|
||||
? formatCodexQuotaPeriod(windowMinutes)
|
||||
: getLegacyCodexQuotaPeriod(baseCode, rawLabel)
|
||||
: getLegacyCodexQuotaPeriod(baseCode, legacyLabel)
|
||||
if (!period) return null
|
||||
|
||||
let label = period
|
||||
if (isSpark) {
|
||||
label = `Spark${period}`
|
||||
} else if (modelLabel && modelLabel !== period) {
|
||||
label = `${modelLabel} ${period}`
|
||||
}
|
||||
|
||||
const fallbackOrder = baseCode === '5h' ? 300 : baseCode === 'weekly' ? 10_080 : 1_000_000
|
||||
return {
|
||||
label: isSpark ? `Spark${period}` : period,
|
||||
label,
|
||||
sortOrder: (isSpark ? 10_000_000 : 0) + (hasExplicitWindowMinutes ? windowMinutes : fallbackOrder),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,6 +768,60 @@ describe('PoolManagement Codex cycle stats mode', () => {
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-single-marker"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders separate account and model weekly quotas in desktop and mobile layouts', async () => {
|
||||
const codexKey = createPoolKey('codex', {
|
||||
status_snapshot: {
|
||||
oauth: { code: 'valid' },
|
||||
account: { code: 'ok', blocked: false },
|
||||
quota: {
|
||||
code: 'ok',
|
||||
exhausted: false,
|
||||
provider_type: 'codex',
|
||||
windows: [
|
||||
{
|
||||
code: 'weekly',
|
||||
label: '周',
|
||||
scope: 'account',
|
||||
remaining_ratio: 0.9,
|
||||
window_minutes: 10_080,
|
||||
usage: { request_count: 12 },
|
||||
},
|
||||
{
|
||||
code: 'additional_0_primary',
|
||||
label: 'gpt-reserve',
|
||||
scope: 'model',
|
||||
model: 'gpt-reserve',
|
||||
remaining_ratio: 0.4,
|
||||
window_minutes: 10_080,
|
||||
usage: { request_count: 99 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
endpointMocks.getPoolOverview.mockResolvedValue({ items: [createOverview('codex')] })
|
||||
endpointMocks.listPoolKeys.mockResolvedValue(createKeyPage(codexKey))
|
||||
endpointMocks.getProvider.mockResolvedValue(createProvider('codex'))
|
||||
|
||||
const root = mountPoolManagement()
|
||||
await settle()
|
||||
|
||||
const panels = root.querySelectorAll('[data-testid="pool-quota-rows"]')
|
||||
expect(panels).toHaveLength(2)
|
||||
for (const panel of panels) {
|
||||
const rows = Array.from(panel.children).map(row => ({
|
||||
label: row.querySelector('[data-testid="pool-quota-period-label"]')?.textContent?.trim(),
|
||||
meter: row.querySelector('[data-testid="pool-quota-meter-text"]')?.textContent?.trim(),
|
||||
}))
|
||||
expect(rows).toHaveLength(2)
|
||||
expect(rows).toEqual(expect.arrayContaining([
|
||||
{ label: '周', meter: '90.0%' },
|
||||
{ label: 'gpt-reserve 周', meter: '40.0%' },
|
||||
]))
|
||||
}
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('-/12')
|
||||
})
|
||||
|
||||
it('opens only one score popover across desktop and mobile layouts', async () => {
|
||||
const scoredKey = createPoolKey('codex', {
|
||||
pool_score: {
|
||||
|
||||
Reference in New Issue
Block a user