// @vitest-environment jsdom // The overview. import { expect, it } from 'vitest' import { en, zh } from '../src/client/locales.ts' import { duration } from '../src/client/text.ts' import { usePanel, mount, remount, all, find, tab, click, text } from './fixtures/panel.tsx' import { server, droppedReply, threadSummary } from './fixtures/panel-server.ts' usePanel() it('leads the overview with the pending Tickets, then problems, the last day and the Executor, each linking to its stage', async () => { await mount() expect(tab(zh['tab.overview']).getAttribute('aria-selected')).toBe('true') expect(all('[role="tab"]').map(tab => tab.textContent)).toEqual(['概览', '来源', '触发', '评估', '执行', '设置']) expect(all('.dt-section-title').map(title => title.textContent)).toEqual(['待处理工单2', '系统问题2', '存活', '工单目录', '过去 24 小时详情 ›', '执行器详情 ›']) expect(all('.dt-cards .dt-ticket-stage').map(item => item.textContent)).toEqual(['待回答', '待处理失败']) expect(all('.dt-cards .dt-att-what').map(item => item.textContent)).toEqual(['飞书 来源异常', '评估未完成:这个文案好奇怪']) // Actions sit below the title and Threads, apart from the stage, each with its icon. expect(all('.dt-ticket').every(row => row.querySelector('.dt-ticket-stage button, .dt-ticket-stage a') === null && [...row.querySelectorAll('.dt-ticket-actions button, .dt-ticket-actions a')].every(action => action.querySelector('svg')))).toBe(true) expect(text()).toContain('来源会在每次轮询时自动重试') expect(text()).toContain('需要确认按钮颜色') // Durations are formatted by the interface language, so the readout is asserted by label, and only the polling word verbatim. expect(all('.dt-stat-label').map(label => label.textContent)).toEqual([zh.livenessUptime, zh.livenessLastPoll, zh.livenessPolling, zh.workspaceHeld, zh.workspaceCandidates, zh.workspaceDirCount, zh.reclaimAfterDays, zh.flowReceivedLabel, zh.flowFilteredLabel, zh.flowJudgedLabel, zh.flowAcceptedLabel, zh.executingLabel, zh.queuedLabel, zh.idleLabel]) expect(all('.dt-stat-value').map(value => value.textContent).slice(7)).toEqual(['12', '2', '5', '1', '2 / 3', '0', '1']) expect(all('.dt-stat-value').map(value => value.textContent)[3]).toBe('58.0 MB') expect(all('.dt-stat-value').map(value => value.textContent).slice(0, 3)).toEqual([expect.stringContaining('小时'), expect .stringContaining('秒'), zh.livenessPolling]) expect(all('.dt-chip').map(chip => chip.textContent)[0]).toBe('GitHub#42CI 失败1') expect(document.querySelector('.dt-chip-problem')?.textContent).toBe('CI 失败1') expect(document.querySelector('a.dt-chip-link')?.href).toBe('https://github.com/acme/widgets/pull/42') await click(all('.dt-more-link').at(-1)!) expect(tab(zh['tab.execution']).getAttribute('aria-selected')).toBe('true') }) it('re-evaluates or dismisses an unjudged Eval from the problem list', async () => { await mount() await click(zh.requeue) expect(server.requests.some(request => request.path === 'triggers/e3/requeue' && request.method === 'POST')).toBe(true) await click(zh.dismiss) expect(server.requests.some(request => request.path === 'decisions/u1/dismiss' && request.method === 'POST')).toBe(true) expect(server.requests.filter(request => request.path === 'overview').length).toBeGreaterThan(2) }) it('opens the Thread of an unjudged Eval at its Trigger from the problem list', async () => { await mount() await click('评估未完成:这个文案好奇怪') expect(server.requests.some(request => request.path.startsWith('threads/feishu:oc_1:om_root'))).toBe(true) expect(document.querySelector('.dt-highlight')?.textContent).toContain('message e3') }) it('follows the last day\'s counts to the status of each stage', async () => { await mount() // Arrived Triggers include filtered ones, which have no Thread, so no list has that count to link to. expect(all('.dt-stat button').map(button => button.textContent)).toEqual(['2', '1']) await click(all('.dt-stat button')[0]!) expect(tab(zh['tab.source']).getAttribute('aria-selected')).toBe('true') await click(zh.sourceFilteredTitle) expect(text()).toContain('looks wrong') expect(text()).toContain(zh['reason.source_disabled']) }) it('offers each Ticket that needs a person its actions on the overview: answer in its Channel, retry or open a failed worker, and stop', async () => { await mount() expect(all('.dt-ticket-actions a').map(link => [link.textContent, link.getAttribute('href')])).toEqual([['去飞书回答', 'https://applink.feishu.cn/client/chat/open?openChatId=oc_1']]) expect(all('.dt-ticket-note').map(note => note.textContent)).toContain('LlmError: no adapter registered for provider "provider-a"') await click(zh.retry) await click(zh.confirm) expect(server.requests.some(request => request.path === 'tickets/t4/retry' && request.method === 'POST')).toBe(true) await click(all('.dt-ticket-actions button').find(button => button.textContent === '打开 Session')!) expect(server.opened).toEqual(['worker-t4']) await click(all('.dt-ticket-actions button').filter(button => button.textContent === '停止').at(-1)!) await click(zh.confirm) expect(server.requests.some(request => request.path === 'tickets/t4/stop' && request.method === 'POST')).toBe(true) expect(text()).not.toContain('截图工具') }) it('shows paused processing as a system problem with its resume action, and says so on the Policy records', async () => { server.settings = { ...server.settings, paused: true } await mount() expect(text()).toContain(zh.problemPaused) await click(zh['tab.eval']) expect(text()).toContain(zh.evalRecordsPaused) await click(zh.resumeWhere) expect(tab(zh['tab.execution']).getAttribute('aria-selected')).toBe('true') await click(zh['tab.overview']) await click(find(zh.resumeProcessing, '.dt-att button')) await click(zh.confirm) expect(server.requests.find(request => request.path === 'executor/pause')).toMatchObject({ method: 'POST', body: { paused: false } }) }) it('offers the step that fixes a settings problem as a button on the overview', async () => { server.settings = { ...server.settings, policy: { ...server.settings.policy, sources: { feishu: { allowed: [], admission: {} }, github: { allowed: [], admission: {} } } } } await mount() expect(text()).toContain(zh.problemNoAllowed) await click(zh.actionAddAllowed) expect(tab(zh['tab.settings']).getAttribute('aria-selected')).toBe('true') expect(document.querySelector('.dt-settings-link[aria-current="page"]')?.textContent).toBe(zh['term.eval']) }) it('counts the messages not sent in the last day, linking to the Source status, and shows nothing when there are none', async () => { await mount() expect(all('.dt-stat-label').map(label => label.textContent)).not.toContain(zh.flowDroppedLabel) server.dropped = [droppedReply('m2', 1, 'feishu', 'channelGone'), droppedReply('m1', 2, 'github', 'channelIgnored'), droppedReply('m0', 30, 'feishu', 'channelGone')] await remount() const stat = all('.dt-stat').find(item => item.querySelector('.dt-stat-label')?.textContent === zh.flowDroppedLabel)! expect(stat.textContent).toBe(`${zh.flowDroppedLabel}2`) await click(stat.querySelector('button')!) expect(tab(zh['tab.source'])?.getAttribute('aria-selected')).toBe('true') }) it('names a failing part of dsh-superagent itself by its label', async () => { server.health = { launch: { state: 'error', detail: 'spawn failed' }, relay: { state: 'ready', detail: 'Nothing waiting' } } await mount() expect(all('.dt-cards .dt-att-what').map(item => item.textContent)).toContain(zh.problemRuntime.replace('{name}', zh['health.launch'])) expect(text()).toContain('spawn failed') }) it('words a refused retry in the interface language by its code', async () => { await mount() // Simulate another user retrying the ticket after the overview loaded. server.person = server.person.map(ticket => ticket.id === 't4' ? { ...ticket, actions: { ...ticket.actions, retry: false } } : ticket) await click(find(zh.retry, '.dt-ticket-actions button')) await click(zh.confirm) expect(document.querySelector('.dt-toast')?.textContent).toBe(zh['userError.notRetryable']) expect(text()).not.toContain('Only an open ticket can be retried, and only when its worker last reported failed or blocked and no person has sent a message since, or when delivering an Input to its worker failed') }) it('shows the overview in English', async () => { await mount(en) expect(all('.dt-section-title').map(title => title.textContent)).toEqual([`${en.pendingTickets}2`, `${en.problems}2`, en.livenessTitle, en.workspaceTitle, `${en.lastDay}${en.details}`, `${en['term.executor']}${en.details}`]) expect(all('.dt-cards .dt-ticket-stage').map(item => item.textContent)).toEqual([en['stage.answer'], en['stage.failed']]) expect(all('.dt-stat-label').map(label => label.textContent)).toEqual([en.livenessUptime, en.livenessLastPoll, en.livenessPolling, en.workspaceHeld, en.workspaceCandidates, en.workspaceDirCount, en.reclaimAfterDays, en.flowReceivedLabel, en.flowFilteredLabel, en.flowJudgedLabel, en.flowAcceptedLabel, en.executingLabel, en.queuedLabel, en.idleLabel]) }) it('lists the unavailable Evals one by one, without the health entry that counts them as a failure of dsh-superagent', async () => { server.health.evals = { state: 'error', detail: '1 Eval(s) unavailable in the last hour without a later judgement' } await mount() expect(all('.dt-cards .dt-att-what').map(item => item.textContent)).toEqual(['飞书 来源异常', '评估未完成:这个文案好奇怪']) expect(text()).not.toContain(zh['health.evals']) }) it('says when no Source is on, an Eval is past its deadline or a message keeps failing, each with the way to it', async () => { for (const kind of ['feishu', 'github']) server.settings.sources[kind] = { ...server.settings.sources[kind]!, enabled: false } server.threads = [threadSummary({ key: 'github:9', title: '#9 迟迟未决', stage: 'evaluating', evaluating: { id: 'v9', triggers: 1, strength: 'inferred', startedAt: '2026-08-31T23:58:00Z', deadline: '2026-09-01T00:00:00Z' } })] server.failing = [{ id: 'm1', channel: { source: 'github', thread: 'github:42', address: { number: '42' } }, text: '已修复', attempts: 4, error: 'HTTP 502 from GitHub' }] await mount() const titles = () => all('.dt-cards .dt-att-what').map(item => item.textContent) // The Feishu Source is off, so its error is no problem any more. expect(titles()).toEqual([zh.problemNoSource, '评估超时未决定:#9 迟迟未决', '评估未完成:这个文案好奇怪', 'GitHub 回复发送失败(已重试 4 次)']) expect(text()).toContain('HTTP 502 from GitHub') await click(zh.actionOpenSources) expect(document.querySelector('.dt-settings-link[aria-current="page"]')?.textContent).toBe(zh['settingsGroup.source']) await click(tab(zh['tab.overview'])) await click('评估超时未决定:#9 迟迟未决') expect(server.requests.some(request => request.path.startsWith('threads/github:9'))).toBe(true) }) it('says when a Source stopped polling, with how long its last round was and the way to it', async () => { server.stalled = [{ id: 'github', title: { zh: 'GitHub', en: 'GitHub' }, ageMs: 26 * 60_000, intervalMs: 60_000 }] await mount() const line = all('.dt-cards .dt-att-what').map(item => item.textContent).find(title => title?.includes('GitHub 最后一次轮询在')) // The same duration helper the panel uses, so the assertion does not depend on the locale's spacing rules. expect(line).toBe(zh.problemStalled.replace('{source}', 'GitHub').replace('{age}', duration(26 * 60_000, 'zh'))) expect(text()).toContain(zh.problemStalledHint) }) it('reads out what the ended Tickets still hold, and that nothing is ever removed when the retention is zero', async () => { server.workspace = { root: '/srv/work', afterDays: 0, directories: 3, candidates: 0, bytes: 0, lastScanAt: new Date().toISOString(), unreadable: 0 } await mount() expect(text()).toContain(zh.workspaceKeepForever) expect(all('.dt-stat-value').map(value => value.textContent)).toContain('0') })