fix: harden OAuth identity and cookies and correct quota and JSON display

This commit is contained in:
elky
2026-09-08 10:51:25 +08:00
parent 7113d04f8a
commit cf8ea19856
24 changed files with 1327 additions and 154 deletions
@@ -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: {
+16 -3
View File
@@ -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: {