wip
fix: docs
This commit is contained in:
@@ -50,7 +50,9 @@ async function bench() {
|
||||
})
|
||||
const layoutFake = { openDetails: vi.fn(), closeDetails: vi.fn() }
|
||||
runtime.provide('layout', layoutFake)
|
||||
runtime.provide('locale', new LocaleService(runtime.ctx))
|
||||
const locale = new LocaleService(runtime.ctx)
|
||||
runtime.provide('locale', locale)
|
||||
runtime.slots.installLocale(locale)
|
||||
|
||||
// The AppFrame role: the conversation-package slots must be declared by a
|
||||
// live entry before apply can contribute into them.
|
||||
@@ -310,7 +312,7 @@ describe('conversation slot inject surface', () => {
|
||||
// Label falls back to the id when a rider declares none.
|
||||
const off2 = b.slots.register(
|
||||
{ name: 'conversation.view', id: 'bare', order: 6 } as never, (() => null) as never)
|
||||
expect(injected.views.list().map(v => v.label)).toEqual(['Chat', 'X', 'bare'])
|
||||
expect(injected.views.list().map(v => v.label)).toEqual(['对话', 'X', 'bare'])
|
||||
off()
|
||||
off2()
|
||||
unsub()
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ToolRowProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
// Export discipline: packages/client/AGENTS.md.
|
||||
import { AskQuestionRow, askQuestionToolview } from '../src/client/toolviews/ask-question-row.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -28,13 +30,16 @@ const resultNode = (argsRaw: string, resultText: string | null, over?: Partial<T
|
||||
const runningCall = (argsRaw: string) =>
|
||||
({ callId: 'c1', name: 'ask_user_question', argsRaw, turn: 1, step: 1, time: 1_000, callView: null })
|
||||
|
||||
function rowProps(block: unknown): ToolRowProps {
|
||||
// Standard locale seat stub mirroring the real ns → common → key chain.
|
||||
const t = makeTranslate(zh, commonZh)
|
||||
|
||||
function rowProps(block: unknown): Parameters<typeof AskQuestionRow>[0] {
|
||||
return {
|
||||
callId: 'c1', toolName: 'ask_user_question', block,
|
||||
callId: 'c1', toolName: 'ask_user_question', block, t,
|
||||
openFile: vi.fn(),
|
||||
sessionId: 's1',
|
||||
useSessions: () => undefined,
|
||||
} as unknown as ToolRowProps
|
||||
} as unknown as Parameters<typeof AskQuestionRow>[0]
|
||||
}
|
||||
|
||||
const answers = (entries: unknown[]): string => JSON.stringify({ answers: entries })
|
||||
@@ -42,8 +47,8 @@ const answers = (entries: unknown[]): string => JSON.stringify({ answers: entrie
|
||||
describe('AskQuestionRow', () => {
|
||||
it('running call reads waiting (args-independent: the composer takeover shows the questions)', () => {
|
||||
const view = render(<AskQuestionRow {...rowProps(runningCall(ARGS))} />)
|
||||
expect(screen.getByText('Ask question')).toBeTruthy()
|
||||
expect(screen.getByText('waiting')).toBeTruthy()
|
||||
expect(screen.getByText('提问')).toBeTruthy()
|
||||
expect(screen.getByText('等待回答')).toBeTruthy()
|
||||
expect(view.container.querySelector('[data-state="running"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -53,7 +58,7 @@ describe('AskQuestionRow', () => {
|
||||
{ id: 'b', selected: [], custom: 'freeform' },
|
||||
{ id: 'c', selected: ['y', 'z'], custom: '' },
|
||||
])))} />)
|
||||
expect(screen.getByText('3/3 answered')).toBeTruthy()
|
||||
expect(screen.getByText('3/3 已回答')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('skipped questions (no selection, no custom) stay out of the answered count', () => {
|
||||
@@ -62,7 +67,7 @@ describe('AskQuestionRow', () => {
|
||||
{ id: 'b', selected: [], custom: '' },
|
||||
{ id: 'c' },
|
||||
])))} />)
|
||||
expect(screen.getByText('1/3 answered')).toBeTruthy()
|
||||
expect(screen.getByText('1/3 已回答')).toBeTruthy()
|
||||
expect(view.container.querySelector('[data-state="ok"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -82,7 +87,7 @@ describe('AskQuestionRow', () => {
|
||||
// ASK_CANCELLED: the apiproxy ask_user_question handler's cancel error.
|
||||
const view = render(<AskQuestionRow {...rowProps(resultNode(ARGS, null,
|
||||
{ isError: true, error: { name: 'UserInteractionError', code: 'ASK_CANCELLED' } }))} />)
|
||||
expect(screen.getByText('cancelled')).toBeTruthy()
|
||||
expect(screen.getByText('已取消')).toBeTruthy()
|
||||
expect(view.container.querySelector('[data-state="error"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -90,7 +95,7 @@ describe('AskQuestionRow', () => {
|
||||
// ASK_ABORTED: the apiproxy ask handler's turn-abort settlement.
|
||||
const view = render(<AskQuestionRow {...rowProps(resultNode(ARGS, null,
|
||||
{ isError: true, error: { name: 'UserInteractionError', code: 'ASK_ABORTED' } }))} />)
|
||||
expect(screen.getByText('interrupted')).toBeTruthy()
|
||||
expect(screen.getByText('已中断')).toBeTruthy()
|
||||
expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -98,7 +103,7 @@ describe('AskQuestionRow', () => {
|
||||
const view = render(<AskQuestionRow {...rowProps(resultNode(ARGS, null,
|
||||
{ isError: true, error: { name: 'Interrupted', code: 'interrupted' } }))} />)
|
||||
expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull()
|
||||
expect(screen.queryByText('cancelled')).toBeNull()
|
||||
expect(screen.queryByText('已取消')).toBeNull()
|
||||
expect(screen.getByText(`ask_user_question · ${ARGS}`)).toBeTruthy()
|
||||
})
|
||||
|
||||
@@ -124,6 +129,9 @@ describe('AskQuestionRow', () => {
|
||||
expect(askQuestionToolview.inject).toEqual(['slots', 'conversation'])
|
||||
const register = vi.fn()
|
||||
askQuestionToolview.apply({ slots: { register } } as never)
|
||||
expect(register).toHaveBeenCalledWith({ name: 'conversation.chat.toolview', key: 'ask_user_question' }, AskQuestionRow)
|
||||
expect(register).toHaveBeenCalledWith(
|
||||
{ name: 'conversation.chat.toolview', key: 'ask_user_question', locale: 'conversation' },
|
||||
AskQuestionRow,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -82,7 +82,9 @@ const LAYOUT_CHILDREN = {
|
||||
async function bench(nodes: ToolResultNode[], opts?: { blank?: boolean }) {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
||||
runtime.provide('locale', new LocaleService(runtime.ctx))
|
||||
const locale = new LocaleService(runtime.ctx)
|
||||
runtime.provide('locale', locale)
|
||||
runtime.slots.installLocale(locale)
|
||||
await runtime.sessions.add({
|
||||
id: SID,
|
||||
summary: { title: 'S', displayTitle: 'S', cwd: '/proj' },
|
||||
@@ -116,7 +118,7 @@ describe('todo_write assembly (product registrations, no outlet twins)', () => {
|
||||
// (default-collapsed: the header summary shows; rows appear on expand).
|
||||
const panel = view.container.querySelector('[data-testid="todo-panel"]')
|
||||
expect(panel).not.toBeNull()
|
||||
expect(panel!.textContent).toContain('1/3 tasks · 1 in progress')
|
||||
expect(panel!.textContent).toContain('1/3 项任务 · 1 项进行中')
|
||||
fireEvent.click(panel!.querySelector('button')!)
|
||||
expect([...panel!.querySelectorAll('li')].map(li => li.getAttribute('data-status')))
|
||||
.toEqual(['completed', 'in_progress', 'pending'])
|
||||
@@ -162,7 +164,9 @@ describe('resident composer', () => {
|
||||
it('renders the locked view state while no session exists at all', async () => {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
||||
runtime.provide('locale', new LocaleService(runtime.ctx))
|
||||
const locale = new LocaleService(runtime.ctx)
|
||||
runtime.provide('locale', locale)
|
||||
runtime.slots.installLocale(locale)
|
||||
await runtime.root.declare(LAYOUT_CHILDREN, AppRoot)
|
||||
await runtime.mount({ inject: [...inject], apply })
|
||||
const view = runtime.renderRoot()
|
||||
@@ -171,7 +175,7 @@ describe('resident composer', () => {
|
||||
const textarea = view.container.querySelector('textarea')
|
||||
expect(textarea).not.toBeNull()
|
||||
expect(textarea!.disabled).toBe(true)
|
||||
expect(view.getByRole('button', { name: 'Choose workspace' })).toBeTruthy()
|
||||
expect(view.getByRole('button', { name: '选择工作区' })).toBeTruthy()
|
||||
await runtime.dispose()
|
||||
})
|
||||
|
||||
@@ -204,7 +208,9 @@ describe('prompt rejection through the assembled composer', () => {
|
||||
it('renders the promptError alert strip and keeps the draft in the machine', async () => {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
||||
runtime.provide('locale', new LocaleService(runtime.ctx))
|
||||
const locale = new LocaleService(runtime.ctx)
|
||||
runtime.provide('locale', locale)
|
||||
runtime.slots.installLocale(locale)
|
||||
const prompt = vi.fn<ISession['prompt']>(async () => ({
|
||||
ok: false, error: { code: 'agent-busy', message: 'prompt rejected before acceptance', details: { reason: 'busy' } },
|
||||
}))
|
||||
@@ -245,7 +251,7 @@ describe('title projection across assembled surfaces', () => {
|
||||
const runtime = await bench([])
|
||||
const view = runtime.renderRoot()
|
||||
// The strict session header breadcrumb reads useSessions ancestry.
|
||||
const crumb = within(view.container.querySelector('[aria-label="Session hierarchy"]') as HTMLElement)
|
||||
const crumb = within(view.container.querySelector('[aria-label="会话层级"]') as HTMLElement)
|
||||
expect(crumb.getByText('S')).toBeTruthy()
|
||||
|
||||
await runtime.sessions.updateSummary(SID, { displayTitle: '修订标题', title: '修订标题' })
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { apply, inject } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
@@ -23,7 +24,9 @@ async function bench() {
|
||||
await runtime.sessions.add(
|
||||
{ id: CHILD, summary: { title: 'C', displayTitle: 'C', parentId: ROOT } }, { current: false })
|
||||
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
||||
runtime.provide('locale', new LocaleService(runtime.ctx))
|
||||
const locale = new LocaleService(runtime.ctx)
|
||||
runtime.provide('locale', locale)
|
||||
runtime.slots.installLocale(locale)
|
||||
|
||||
// Declared by ui-layout's root entry in production; the test root declares
|
||||
// them here so the contributions land.
|
||||
@@ -52,7 +55,8 @@ describe('apply wiring', () => {
|
||||
const b = await bench()
|
||||
const entries = b.slots.entries('conversation.view')
|
||||
expect(entries.map(e => e.options.id)).toEqual(['chat'])
|
||||
expect(entries[0]?.options.label).toBe('Chat')
|
||||
// Label is a locale thunk resolving through the zh dictionary.
|
||||
expect(resolveSlotLabel(entries[0]?.options.label)).toBe('对话')
|
||||
expect(entries[0]?.options.order).toBe(0)
|
||||
// Declaring is claiming: the chat entry's registration put the hole on
|
||||
// the ledger with the contract's kind/scope.
|
||||
|
||||
@@ -8,15 +8,21 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import {
|
||||
formatMessageClock, msUntilNextLocalMidnight, startOfLocalDay,
|
||||
} from '../src/client/chat/message-chrome.ts'
|
||||
import { MessageItem } from '../src/client/chat/MessageItem.tsx'
|
||||
import { MessageItem, type MessageItemProps } from '../src/client/chat/MessageItem.tsx'
|
||||
import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
|
||||
import { StatsLine, type StatsLineProps } from '../src/client/chat/StatsLine.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
const t: MessageItemProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
describe('MessageItem arms', () => {
|
||||
it('user bubbles expose clock / copy / branch / edit; copy writes the text', () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
@@ -28,7 +34,7 @@ describe('MessageItem arms', () => {
|
||||
const now = new Date()
|
||||
const time = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 24).getTime()
|
||||
render(
|
||||
<MessageItem node={{
|
||||
<MessageItem t={t} node={{
|
||||
kind: 'user', seq: 1, time,
|
||||
content: [{ type: 'text', text: 'hello bubble' }] as never,
|
||||
source: null,
|
||||
@@ -54,7 +60,7 @@ describe('MessageItem arms', () => {
|
||||
value: exec,
|
||||
})
|
||||
render(
|
||||
<MessageItem node={{
|
||||
<MessageItem t={t} node={{
|
||||
kind: 'user', seq: 1, time: 1_000,
|
||||
content: [{ type: 'text', text: 'fallback body' }] as never,
|
||||
source: null,
|
||||
@@ -77,7 +83,7 @@ describe('MessageItem arms', () => {
|
||||
},
|
||||
})
|
||||
render(
|
||||
<MessageItem node={{
|
||||
<MessageItem t={t} node={{
|
||||
kind: 'user', seq: 1, time: 1_000,
|
||||
content: [{ type: 'text', text: 'quiet' }] as never,
|
||||
source: null,
|
||||
@@ -95,7 +101,7 @@ describe('MessageItem arms', () => {
|
||||
|
||||
it('steering bubbles carry the interjection badge and non-text rest blocks, without user actions', () => {
|
||||
const view = render(
|
||||
<MessageItem node={{
|
||||
<MessageItem t={t} node={{
|
||||
kind: 'steering', seq: 2, turn: 1, source: null,
|
||||
content: [{ type: 'text', text: 'steer!' }, { type: 'image', data: 'x' }] as never,
|
||||
} as never}
|
||||
@@ -109,7 +115,7 @@ describe('MessageItem arms', () => {
|
||||
|
||||
it('context uses the Tool calls disclosure chrome and keeps its JSON collapsed by default', () => {
|
||||
const ctxView = render(
|
||||
<MessageItem node={{
|
||||
<MessageItem t={t} node={{
|
||||
kind: 'context',
|
||||
seq: 3,
|
||||
content: [{ type: 'text', text: 'x\n"y":,[{}]' }],
|
||||
@@ -135,7 +141,7 @@ describe('MessageItem arms', () => {
|
||||
|
||||
it('context preserves the bounded JSON truncation contract', () => {
|
||||
const view = render(
|
||||
<MessageItem node={{
|
||||
<MessageItem t={t} node={{
|
||||
kind: 'context',
|
||||
seq: 3,
|
||||
content: [{ type: 'text', text: 'x'.repeat(21_000) }],
|
||||
@@ -150,7 +156,7 @@ describe('MessageItem arms', () => {
|
||||
|
||||
it('unknown nodes retain the generic JSON row', () => {
|
||||
const unknownView = render(
|
||||
<MessageItem node={{ kind: 'unknown', seq: 4, type: 'surface/next', data: { x: 1 } } as never} />,
|
||||
<MessageItem t={t} node={{ kind: 'unknown', seq: 4, type: 'surface/next', data: { x: 1 } } as never} />,
|
||||
)
|
||||
expect(unknownView.getByText(/未知 surface 事件:surface\/next/)).toBeTruthy()
|
||||
})
|
||||
@@ -160,15 +166,15 @@ describe('formatMessageClock', () => {
|
||||
const now = new Date(2026, 6, 29, 10, 0).getTime()
|
||||
|
||||
it('keeps HH:mm on the same calendar day', () => {
|
||||
expect(formatMessageClock(new Date(2026, 6, 29, 14, 24).getTime(), now)).toBe('14:24')
|
||||
expect(formatMessageClock(new Date(2026, 6, 29, 14, 24).getTime(), t, now)).toBe('14:24')
|
||||
})
|
||||
|
||||
it('prefixes month and day across days in the same year', () => {
|
||||
expect(formatMessageClock(new Date(2026, 0, 1, 14, 24).getTime(), now)).toBe('1月1日 14:24')
|
||||
expect(formatMessageClock(new Date(2026, 0, 1, 14, 24).getTime(), t, now)).toBe('1月1日 14:24')
|
||||
})
|
||||
|
||||
it('prefixes year, month, and day across years', () => {
|
||||
expect(formatMessageClock(new Date(2025, 11, 31, 9, 5).getTime(), now)).toBe('2025年12月31日 09:05')
|
||||
expect(formatMessageClock(new Date(2025, 11, 31, 9, 5).getTime(), t, now)).toBe('2025年12月31日 09:05')
|
||||
})
|
||||
|
||||
it('arms the next local midnight from an in-day instant', () => {
|
||||
@@ -191,7 +197,7 @@ describe('useCalendarDay boundary refresh', () => {
|
||||
vi.setSystemTime(dayStart)
|
||||
const time = new Date(2026, 6, 29, 14, 24).getTime()
|
||||
render(
|
||||
<MessageItem node={{
|
||||
<MessageItem t={t} node={{
|
||||
kind: 'user', seq: 1, time,
|
||||
content: [{ type: 'text', text: 'night bubble' }] as never,
|
||||
source: null,
|
||||
@@ -209,7 +215,7 @@ describe('useCalendarDay boundary refresh', () => {
|
||||
describe('small branch tails', () => {
|
||||
it('AssistantMarkdown single-line reasoning summary skips the newline cut', () => {
|
||||
const view = render(
|
||||
<AssistantMarkdown blocks={[{ kind: 'reasoning', text: 'one-liner' }]} streaming={false} />,
|
||||
<AssistantMarkdown t={t} blocks={[{ kind: 'reasoning', text: 'one-liner' }]} streaming={false} />,
|
||||
)
|
||||
expect(view.getByText('one-liner')).toBeTruthy()
|
||||
})
|
||||
@@ -224,6 +230,7 @@ describe('small branch tails', () => {
|
||||
const time = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 24).getTime()
|
||||
const settled = render(
|
||||
<AssistantMarkdown
|
||||
t={t}
|
||||
blocks={[{ kind: 'text', text: 'answer body' }, { kind: 'reasoning', text: 'hidden' }]}
|
||||
streaming={false}
|
||||
time={time}
|
||||
@@ -238,6 +245,7 @@ describe('small branch tails', () => {
|
||||
|
||||
const thinkOnly = render(
|
||||
<AssistantMarkdown
|
||||
t={t}
|
||||
blocks={[{ kind: 'reasoning', text: 'only thinking' }]}
|
||||
streaming={false}
|
||||
time={time}
|
||||
@@ -248,7 +256,7 @@ describe('small branch tails', () => {
|
||||
thinkOnly.unmount()
|
||||
|
||||
const streaming = render(
|
||||
<AssistantMarkdown blocks={[{ kind: 'text', text: 'partial' }]} streaming time={time} />,
|
||||
<AssistantMarkdown t={t} blocks={[{ kind: 'text', text: 'partial' }]} streaming time={time} />,
|
||||
)
|
||||
expect(streaming.queryByRole('button', { name: '复制' })).toBeNull()
|
||||
expect(streaming.queryByText('14:24')).toBeNull()
|
||||
|
||||
@@ -137,7 +137,9 @@ async function bench(snapshot: ConversationSnapshot) {
|
||||
}
|
||||
ctx.provide('workspaces', workspaces)
|
||||
ctx.provide('layout', layout)
|
||||
ctx.provide('locale', new LocaleService(ctx))
|
||||
const locale = new LocaleService(ctx)
|
||||
ctx.provide('locale', locale)
|
||||
slots.installLocale(locale)
|
||||
|
||||
slots.install(createSlotRenderer())
|
||||
slots.register({
|
||||
|
||||
@@ -11,9 +11,16 @@ import type {
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { ToolRowProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { StatsLine, deriveStats, formatDuration, formatTokens, type StatsLineProps } from '../src/client/chat/StatsLine.tsx'
|
||||
import { BashRow } from '../src/client/toolviews/bash-sample.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
type BashRowProps = Parameters<typeof BashRow>[0]
|
||||
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
const t: BashRowProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -168,12 +175,13 @@ describe('bash sample row', () => {
|
||||
|
||||
const rowProps = (sessionId: SessionId, over?: {
|
||||
store?: ReturnType<typeof listStore>
|
||||
}): ToolRowProps => ({
|
||||
}): BashRowProps => ({
|
||||
callId: 'c1', toolName: 'bash', block: result('c1'),
|
||||
openFile: vi.fn(),
|
||||
sessionId,
|
||||
useSessions: bindSnapshotSelector(over?.store ?? listStore()),
|
||||
} as unknown as ToolRowProps)
|
||||
t,
|
||||
} as unknown as BashRowProps)
|
||||
|
||||
it('differential rendering: the scoped variant in sub-sessions, global at roots', () => {
|
||||
const scoped = render(<BashRow {...rowProps(CHILD)} />)
|
||||
|
||||
@@ -4,11 +4,16 @@ import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
|
||||
afterEach(cleanup)
|
||||
import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { classifyTool, resolveToolPath, toolRowModel } from '../src/client/contract/tool-call-model.ts'
|
||||
import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
|
||||
import { ToolRow } from '../src/client/chat/ToolRow.tsx'
|
||||
import { GenericToolCard } from '../src/client/chat/GenericToolCard.tsx'
|
||||
import type { ToolRowOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { GenericToolCard, type GenericToolCardProps } from '../src/client/chat/GenericToolCard.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
const t: GenericToolCardProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
const running = (over?: Partial<RunningToolCall>): RunningToolCall => ({
|
||||
callId: 'c1', name: 'bash', argsRaw: '{"command":"ls -la","description":"List files"}',
|
||||
@@ -132,6 +137,7 @@ describe('tool-call-model', () => {
|
||||
|
||||
describe('ToolRow', () => {
|
||||
const rowProps = {
|
||||
t,
|
||||
variant: 'bash' as const, icon: <i data-testid="tool-icon" />, title: 'Bash',
|
||||
summary: 'List files', body: '{\n "a": 1\n}', state: 'ok' as const,
|
||||
}
|
||||
@@ -221,6 +227,7 @@ describe('ThinkRow', () => {
|
||||
it('expands from either Think or the reasoning summary', () => {
|
||||
const view = render(
|
||||
<AssistantMarkdown
|
||||
t={t}
|
||||
blocks={[{ kind: 'reasoning', text: 'Inspect the session\nCheck persistence' }]}
|
||||
streaming={false}
|
||||
/>,
|
||||
@@ -237,8 +244,8 @@ describe('ThinkRow', () => {
|
||||
})
|
||||
|
||||
describe('GenericToolCard', () => {
|
||||
const props = (toolName: string, block: RunningToolCall | ToolResultNode): ToolRowOwnerProps => ({
|
||||
callId: 'c1', toolName, block, openFile: vi.fn(),
|
||||
const props = (toolName: string, block: RunningToolCall | ToolResultNode): GenericToolCardProps => ({
|
||||
callId: 'c1', toolName, block, openFile: vi.fn(), t,
|
||||
})
|
||||
|
||||
it('renders the classified variant row from the frozen slice', () => {
|
||||
|
||||
@@ -65,7 +65,9 @@ async function bench(nodes: ToolResultNode[]) {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
const layout = { openDetails: vi.fn(), closeDetails: vi.fn() }
|
||||
runtime.provide('layout', layout)
|
||||
runtime.provide('locale', new LocaleService(runtime.ctx))
|
||||
const locale = new LocaleService(runtime.ctx)
|
||||
runtime.provide('locale', locale)
|
||||
runtime.slots.installLocale(locale)
|
||||
await runtime.sessions.add({
|
||||
id: SID,
|
||||
summary: { title: 'S', displayTitle: 'S' },
|
||||
@@ -193,7 +195,9 @@ describe('registrant load-order seam', () => {
|
||||
it("suspends a registrant on inject: ['slots', 'conversation'] until the service (and the hole) exists", async () => {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
||||
runtime.provide('locale', new LocaleService(runtime.ctx))
|
||||
const locale = new LocaleService(runtime.ctx)
|
||||
runtime.provide('locale', locale)
|
||||
runtime.slots.installLocale(locale)
|
||||
await runtime.root.declare(LAYOUT_CHILDREN, AppRoot)
|
||||
|
||||
// Third-party posture, mounted BEFORE ui-conversation: real fiber inject
|
||||
|
||||
@@ -14,8 +14,11 @@ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore, PendingWait } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ChatViewSlotProps, SelectionTarget } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { createChatStore } from '../src/client/stores.ts'
|
||||
import { ChatView } from '../src/client/chat/ChatView.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
import { assistantActionsSeqs, deriveChatFlow, flowKeys } from '../src/client/chat/chat-flow.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
@@ -122,6 +125,8 @@ function makeHarness(init?: Partial<ConversationSnapshot>) {
|
||||
openFile,
|
||||
loadOlder,
|
||||
forkAt,
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
t: makeTranslate(zh, commonZh),
|
||||
}
|
||||
const setSelection = (next: SelectionTarget | null): void => { chat.actions.select(next) }
|
||||
return { set, ChatView, props, openDetails, openFile, loadOlder, forkAt, setSelection }
|
||||
|
||||
@@ -8,12 +8,19 @@ import { cleanup, render } from '@testing-library/react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { RunningToolCall, SessionId, SessionListState, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ToolRowOwnerProps, ToolRowProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { apply as nodeApply } from '../src/index.ts'
|
||||
import { GenericToolCard } from '../src/client/chat/GenericToolCard.tsx'
|
||||
import { GenericToolCard, type GenericToolCardProps } from '../src/client/chat/GenericToolCard.tsx'
|
||||
import { ToolRow } from '../src/client/chat/ToolRow.tsx'
|
||||
import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
|
||||
import { BashRow } from '../src/client/toolviews/bash-sample.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
type BashRowProps = Parameters<typeof BashRow>[0]
|
||||
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
const t: GenericToolCardProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -24,7 +31,7 @@ describe('tails', () => {
|
||||
|
||||
it('ToolRow stopped state renders the warning dot in the leading slot', () => {
|
||||
const view = render(
|
||||
<ToolRow variant="bash" icon={<i data-testid="icon" />} title="Bash" summary="s" body={null} state="stopped" />,
|
||||
<ToolRow t={t} variant="bash" icon={<i data-testid="icon" />} title="Bash" summary="s" body={null} state="stopped" />,
|
||||
)
|
||||
expect(view.queryByTestId('icon')).toBeNull()
|
||||
expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull()
|
||||
@@ -33,6 +40,7 @@ describe('tails', () => {
|
||||
it('AssistantMarkdown renders reasoning as a Think row and unknown blocks as JSON fallback', () => {
|
||||
const view = render(
|
||||
<AssistantMarkdown
|
||||
t={t}
|
||||
blocks={[
|
||||
{ kind: 'reasoning', text: 'thinking hard\nsecond line' },
|
||||
{ kind: 'tool-call', callId: 'c', name: 'bash', argsRaw: '{}' },
|
||||
@@ -45,7 +53,7 @@ describe('tails', () => {
|
||||
expect(view.getByText('thinking hard')).toBeTruthy()
|
||||
expect(view.getByText(/未知内容块/)).toBeTruthy()
|
||||
const stopped = render(
|
||||
<AssistantMarkdown blocks={[{ kind: 'text', text: 'partial words' }]} streaming={false} interrupted />,
|
||||
<AssistantMarkdown t={t} blocks={[{ kind: 'text', text: 'partial words' }]} streaming={false} interrupted />,
|
||||
)
|
||||
expect(stopped.getByText('已停止')).toBeTruthy()
|
||||
})
|
||||
@@ -55,12 +63,13 @@ describe('tails', () => {
|
||||
// groups is layout noise (no text, no pulse, no interrupted marker).
|
||||
const empty = render(
|
||||
<AssistantMarkdown
|
||||
t={t}
|
||||
blocks={[{ kind: 'tool-call', callId: 'c', name: 'todo_write', argsRaw: '{}' }]}
|
||||
streaming={false}
|
||||
/>,
|
||||
)
|
||||
expect(empty.container.firstChild).toBeNull()
|
||||
const blank = render(<AssistantMarkdown blocks={[]} streaming={false} />)
|
||||
const blank = render(<AssistantMarkdown t={t} blocks={[]} streaming={false} />)
|
||||
expect(blank.container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
@@ -71,8 +80,8 @@ describe('tails', () => {
|
||||
callTime: 1_000,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
}
|
||||
const props: ToolRowOwnerProps = {
|
||||
callId: 'c5', toolName: 'todo_write', block: settled, openFile: vi.fn(),
|
||||
const props: GenericToolCardProps = {
|
||||
callId: 'c5', toolName: 'todo_write', block: settled, openFile: vi.fn(), t,
|
||||
}
|
||||
const view = render(<GenericToolCard {...props} />)
|
||||
// Settled ok state keeps the variant icon (sparkle) instead of a StateDot.
|
||||
@@ -91,7 +100,8 @@ describe('tails', () => {
|
||||
const props = (block: RunningToolCall | ToolResultNode) => ({
|
||||
callId: 'c1', toolName: 'bash', block, openFile: vi.fn(),
|
||||
sessionId: sid, useSessions: bindSnapshotSelector(list),
|
||||
} as unknown as ToolRowProps)
|
||||
t,
|
||||
} as unknown as BashRowProps)
|
||||
|
||||
const running: RunningToolCall = {
|
||||
callId: 'c1', name: 'bash', argsRaw: '{"command":"ls","description":"List"}',
|
||||
|
||||
@@ -7,10 +7,16 @@ import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { ConversationSnapshot, SessionId, SessionListState, WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SelectionTarget } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { createChatStore } from '../src/client/stores.ts'
|
||||
import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
|
||||
import { AssistantMarkdown, type AssistantMarkdownProps } from '../src/client/chat/AssistantMarkdown.tsx'
|
||||
import { StatsLine } from '../src/client/chat/StatsLine.tsx'
|
||||
import { DetailsPanel } from '../src/client/skeleton/DetailsPanel.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
const t: AssistantMarkdownProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -28,6 +34,7 @@ describe('render branch tails', () => {
|
||||
it('AssistantMarkdown reasoning row is ok-state when not the streaming tail', () => {
|
||||
const view = render(
|
||||
<AssistantMarkdown
|
||||
t={t}
|
||||
blocks={[{ kind: 'reasoning', text: 'done thinking' }, { kind: 'text', text: 'answer' }]}
|
||||
streaming
|
||||
/>,
|
||||
@@ -54,7 +61,7 @@ describe('render branch tails', () => {
|
||||
|
||||
it('AssistantMarkdown reasoning as the streaming tail renders the running ring', () => {
|
||||
const view = render(
|
||||
<AssistantMarkdown blocks={[{ kind: 'reasoning', text: 'still thinking' }]} streaming />,
|
||||
<AssistantMarkdown t={t} blocks={[{ kind: 'reasoning', text: 'still thinking' }]} streaming />,
|
||||
)
|
||||
expect(view.container.querySelector('[data-state="running"]')).not.toBeNull()
|
||||
})
|
||||
@@ -82,6 +89,7 @@ describe('render branch tails', () => {
|
||||
useStore={bindSnapshotSelector(chat)}
|
||||
actions={chat.actions}
|
||||
closeDetails={vi.fn()}
|
||||
t={t}
|
||||
/>,
|
||||
)
|
||||
expect(view.getByText('详情')).toBeTruthy()
|
||||
@@ -118,6 +126,7 @@ describe('render branch tails', () => {
|
||||
useStore={bindSnapshotSelector(chat)}
|
||||
actions={chat.actions}
|
||||
closeDetails={vi.fn()}
|
||||
t={t}
|
||||
/>,
|
||||
)
|
||||
// Sub-call material: the sub-tool name titles the panel, args pretty-print,
|
||||
|
||||
@@ -8,10 +8,13 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import type { ClientContext, ConversationSnapshot, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { SessionInputShell } from '../src/client/input/facade.ts'
|
||||
import { InputBar } from '../src/client/skeleton/InputBar.tsx'
|
||||
import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -42,7 +45,7 @@ interface BenchOptions {
|
||||
promptError?: ConversationSnapshot['promptError']
|
||||
variant?: 'hero' | 'composer'
|
||||
placeholder?: string
|
||||
translateHint?: (key: string) => string
|
||||
t?: InputBarProps['t']
|
||||
accessory?: React.ReactNode
|
||||
overlay?: React.ReactNode
|
||||
leftItems?: React.ReactNode
|
||||
@@ -101,11 +104,8 @@ function bench(over?: BenchOptions) {
|
||||
useLexicon: bindSnapshotSelector(shell.lexicon),
|
||||
stop,
|
||||
command: () => Promise.resolve(true),
|
||||
// Mirrors the en 'command.hint' locale entries the production apply wires in.
|
||||
translateHint: over?.translateHint ?? ((key: string) => ({
|
||||
'placeholder.default': 'Message the agent',
|
||||
'placeholder.plan': 'describe your task to generate plan',
|
||||
} as Record<string, string>)[key] ?? key),
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
t: over?.t ?? makeTranslate(zh, commonZh),
|
||||
renderSlot,
|
||||
variant: over?.variant ?? 'composer',
|
||||
...(over?.placeholder !== undefined ? { placeholder: over.placeholder } : {}),
|
||||
@@ -118,7 +118,7 @@ function bench(over?: BenchOptions) {
|
||||
const textarea = view.container.querySelector('textarea')!
|
||||
// aria-label (not role name): title carries the same label and would double-match.
|
||||
const button = view.container.querySelector<HTMLButtonElement>(
|
||||
`button[aria-label="${over?.running === true ? 'Stop generating' : 'Send message'}"]`,
|
||||
`button[aria-label="${over?.running === true ? '停止生成' : '发送消息'}"]`,
|
||||
)!
|
||||
return { view, textarea, button, props, sink, shell, wiring: shell, session, stop, slotCalls }
|
||||
}
|
||||
@@ -196,7 +196,7 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
fireEvent.change(textarea, { target: { value: '排队消息2' } })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('排队消息2', 'queue')
|
||||
expect(button.getAttribute('aria-label')).toBe('Stop generating')
|
||||
expect(button.getAttribute('aria-label')).toBe('停止生成')
|
||||
fireEvent.click(button)
|
||||
expect(stop).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -204,8 +204,8 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
it('disabled (session removed) locks the textarea and chrome', () => {
|
||||
const { textarea, view } = bench({ disabled: true })
|
||||
expect(textarea.disabled).toBe(true)
|
||||
expect(textarea.placeholder).toBe('Session unavailable')
|
||||
expect((view.getByLabelText('Add attachment') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect(textarea.placeholder).toBe('会话不可用')
|
||||
expect((view.getByLabelText('添加附件') as HTMLButtonElement).disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('idle primary sends and disables on empty draft', () => {
|
||||
@@ -222,7 +222,7 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
const textarea = first.view.container.querySelector('textarea')!
|
||||
expect(document.activeElement).toBe(textarea)
|
||||
textarea.blur()
|
||||
fireEvent.mouseDown(first.view.container.querySelector('button[aria-label="Send message"]')!)
|
||||
fireEvent.mouseDown(first.view.container.querySelector('button[aria-label="发送消息"]')!)
|
||||
expect(document.activeElement).toBe(textarea)
|
||||
})
|
||||
|
||||
@@ -285,22 +285,22 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
|
||||
it('disabled state shows the unavailable placeholder; custom placeholder wins', () => {
|
||||
const { textarea } = bench({ disabled: true })
|
||||
expect(textarea.placeholder).toBe('Session unavailable')
|
||||
expect(textarea.placeholder).toBe('会话不可用')
|
||||
const live = bench()
|
||||
expect(live.textarea.placeholder).toBe('Message the agent')
|
||||
expect(live.textarea.placeholder).toBe('给智能体发消息')
|
||||
const custom = bench({ placeholder: 'Custom placeholder' })
|
||||
expect(custom.textarea.placeholder).toBe('Custom placeholder')
|
||||
})
|
||||
|
||||
it('the plan projection swaps the placeholder while its effective target is plan mode', () => {
|
||||
const active = bench({ plan: { active: true, pending: false } })
|
||||
expect(active.textarea.placeholder).toBe('describe your task to generate plan')
|
||||
expect(active.textarea.placeholder).toBe('描述你的任务以生成计划')
|
||||
// /plan just ran: pending entry already reads as the plan target.
|
||||
const entering = bench({ plan: { active: false, pending: true } })
|
||||
expect(entering.textarea.placeholder).toBe('describe your task to generate plan')
|
||||
expect(entering.textarea.placeholder).toBe('描述你的任务以生成计划')
|
||||
// Pending exit: target is default again.
|
||||
const leaving = bench({ plan: { active: true, pending: true } })
|
||||
expect(leaving.textarea.placeholder).toBe('Message the agent')
|
||||
expect(leaving.textarea.placeholder).toBe('给智能体发消息')
|
||||
// Owner placeholder outranks the plan swap.
|
||||
const custom = bench({ plan: { active: true, pending: false }, placeholder: 'Custom placeholder' })
|
||||
expect(custom.textarea.placeholder).toBe('Custom placeholder')
|
||||
@@ -325,13 +325,14 @@ describe('machine pending lock', () => {
|
||||
expect(shell.snapshot.phase).toBe('submitting')
|
||||
const textarea = view.container.querySelector('textarea')!
|
||||
expect(textarea.readOnly).toBe(true)
|
||||
expect(view.container.querySelector<HTMLButtonElement>('button[aria-label="Send message"]')!.disabled).toBe(true)
|
||||
expect(view.container.querySelector<HTMLButtonElement>('button[aria-label="发送消息"]')!.disabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('decorations', () => {
|
||||
it('claimed token renders the mirror highlight and the blank-args hint', () => {
|
||||
const { view, shell } = bench()
|
||||
// Dictionary-less stub: an unmatched hint key keeps the machine's raw hint.
|
||||
const { view, shell } = bench({ t: makeTranslate({}) })
|
||||
act(() => {
|
||||
shell.setDraft('/goal ')
|
||||
shell.beginCommand(
|
||||
@@ -349,8 +350,7 @@ describe('decorations', () => {
|
||||
})
|
||||
|
||||
it('a locale entry for the claimed command overrides the raw claim hint (trailing-space token)', () => {
|
||||
const dict: Record<string, string> = { goal: '输入目标,智能体将持续执行' }
|
||||
const { view, shell } = bench({ translateHint: key => dict[key] ?? key })
|
||||
const { view, shell } = bench()
|
||||
act(() => {
|
||||
shell.setDraft('/goal ')
|
||||
shell.beginCommand(
|
||||
@@ -441,9 +441,9 @@ describe('strips and variants', () => {
|
||||
describe('placeholder chrome and control seats', () => {
|
||||
it('renders attach; the Access chip is absent without the permissions projection; plan/model seats render EMPTY without entries (B ruling)', () => {
|
||||
const { view, slotCalls } = bench()
|
||||
expect(view.getByLabelText('Add attachment')).toBeTruthy()
|
||||
expect(view.getByLabelText('添加附件')).toBeTruthy()
|
||||
// Capability absent (no projection value): the chip renders nothing.
|
||||
expect(view.queryByLabelText(/^Access mode/)).toBeNull()
|
||||
expect(view.queryByLabelText(/^访问模式/)).toBeNull()
|
||||
// Both seats dispatched, nothing rendered.
|
||||
expect(slotCalls.map(c => c.key)).toEqual(['conversation.input.plan', 'conversation.input.model'])
|
||||
expect(view.queryByLabelText('Plan mode')).toBeNull()
|
||||
@@ -459,7 +459,7 @@ describe('placeholder chrome and control seats', () => {
|
||||
currentValue: 'workspace-write',
|
||||
}
|
||||
const { view } = bench({ permissions })
|
||||
const trigger = view.getByLabelText(/^Access mode/) as HTMLButtonElement
|
||||
const trigger = view.getByLabelText(/^访问模式/) as HTMLButtonElement
|
||||
// Title-case display is presentation only; the menu ids stay machine names.
|
||||
expect(trigger.textContent).toBe('Workspace Write')
|
||||
fireEvent.click(trigger)
|
||||
@@ -467,11 +467,11 @@ describe('placeholder chrome and control seats', () => {
|
||||
expect(items.map(o => o.textContent)).toEqual(['Workspace Write', 'Danger Full Access'])
|
||||
fireEvent.click(items[1]!)
|
||||
// Optimistic pick + disable until admission resolves (command stub resolves true).
|
||||
const busy = view.getByLabelText(/^Access mode/) as HTMLButtonElement
|
||||
const busy = view.getByLabelText(/^访问模式/) as HTMLButtonElement
|
||||
expect(busy.textContent).toBe('Danger Full Access')
|
||||
expect(busy.disabled).toBe(true)
|
||||
await act(async () => {})
|
||||
expect((view.getByLabelText(/^Access mode/) as HTMLButtonElement).disabled).toBe(false)
|
||||
expect((view.getByLabelText(/^访问模式/) as HTMLButtonElement).disabled).toBe(false)
|
||||
})
|
||||
|
||||
it('a registered entry fills its seat and receives the locked owner prop', () => {
|
||||
@@ -492,10 +492,10 @@ describe('placeholder chrome and control seats', () => {
|
||||
it('disabled locks the Access chip and attach control (running does not)', () => {
|
||||
const permissions = { options: [{ value: 'workspace-write', name: 'workspace-write' }], currentValue: 'workspace-write' }
|
||||
const { view } = bench({ disabled: true, permissions })
|
||||
expect((view.getByLabelText('Add attachment') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect((view.getByLabelText(/^Access mode/) as HTMLButtonElement).disabled).toBe(true)
|
||||
expect((view.getByLabelText('添加附件') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect((view.getByLabelText(/^访问模式/) as HTMLButtonElement).disabled).toBe(true)
|
||||
cleanup()
|
||||
const live = bench({ running: true, permissions })
|
||||
expect((live.view.getByLabelText(/^Access mode/) as HTMLButtonElement).disabled).toBe(false)
|
||||
expect((live.view.getByLabelText(/^访问模式/) as HTMLButtonElement).disabled).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,9 +11,12 @@ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ClientContext, ConversationSnapshot, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SubmitOutcome } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { SessionInputShell } from '../src/client/input/facade.ts'
|
||||
import { InputBar } from '../src/client/skeleton/InputBar.tsx'
|
||||
import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -48,7 +51,8 @@ function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled
|
||||
renderSlot: (() => null) as InputBarProps['renderSlot'],
|
||||
stop: vi.fn(),
|
||||
command: () => Promise.resolve(true),
|
||||
translateHint: (key: string) => key,
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
t: makeTranslate(zh, commonZh),
|
||||
variant: 'composer',
|
||||
}
|
||||
return render(<InputBar {...props} />)
|
||||
@@ -92,7 +96,8 @@ describe('matrix row: claimed', () => {
|
||||
claim()
|
||||
expect(shell.snapshot.claim).toEqual({ token: '/goal ', hint: '目标' })
|
||||
expect(view.container.querySelector('[data-decoration="token"]')?.textContent).toBe('/goal ')
|
||||
expect(view.container.querySelector('[data-decoration="hint"]')?.textContent).toBe('目标')
|
||||
// The zh dictionary owns a hint.goal entry, which overrides the raw claim hint (production behavior).
|
||||
expect(view.container.querySelector('[data-decoration="hint"]')?.textContent).toBe('输入目标,智能体将持续执行')
|
||||
expect((textarea).readOnly).toBe(false)
|
||||
// Free editing beyond the token: hint drops, claim holds.
|
||||
fireEvent.change(textarea, { target: { value: '/goal 发布版本' } })
|
||||
@@ -170,7 +175,7 @@ describe('matrix row: locked (session disabled)', () => {
|
||||
it('disables the textarea and chrome; the machine currency is untouched', () => {
|
||||
const { view, textarea, shell } = bench({ disabled: true })
|
||||
expect((textarea).disabled).toBe(true)
|
||||
expect((view.getByLabelText('Add attachment') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect((view.getByLabelText('添加附件') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect(shell.snapshot.phase).toBe('plain')
|
||||
})
|
||||
|
||||
|
||||
@@ -15,9 +15,12 @@ import { SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { SlashService } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import type { ClientSessionContext, CommandClaim, PickOutcome, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import { FakeApiClient, ok } from '../../runtime/tests/fake-api.ts'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { SessionInputShell } from '../src/client/input/facade.ts'
|
||||
import { InputBar } from '../src/client/skeleton/InputBar.tsx'
|
||||
import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -134,7 +137,8 @@ async function scopedBench(register?: (slash: SlashService) => void) {
|
||||
renderSlot: (() => null) as InputBarProps['renderSlot'],
|
||||
stop: vi.fn(),
|
||||
command: () => Promise.resolve(true),
|
||||
translateHint: (key: string) => key,
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
t: makeTranslate(zh, commonZh),
|
||||
variant: 'composer',
|
||||
}
|
||||
const view = render(<InputBar {...barProps} />)
|
||||
@@ -168,7 +172,8 @@ describe('scenario A: menu-pick /goal, type args, enter submits', () => {
|
||||
expect(b.shell.snapshot.phase).toBe('claimed')
|
||||
expect(b.textarea.value).toBe('/goal ')
|
||||
expect(b.view.container.querySelector('[data-decoration="token"]')?.textContent).toBe('/goal ')
|
||||
expect(b.view.container.querySelector('[data-decoration="hint"]')?.textContent).toBe('目标内容')
|
||||
// The zh dictionary owns a hint.goal entry, which overrides the machine's raw hint (production behavior).
|
||||
expect(b.view.container.querySelector('[data-decoration="hint"]')?.textContent).toBe('输入目标,智能体将持续执行')
|
||||
// Continue typing args; hint drops; claim holds.
|
||||
b.type('/goal 发布 v1')
|
||||
expect(b.shell.snapshot.phase).toBe('claimed')
|
||||
|
||||
@@ -10,9 +10,12 @@ import type {
|
||||
ConversationSnapshot, QueuedMessage, SessionId, SessionListState,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import type { QueueItemId } from '../src/client/contract/queue.ts'
|
||||
import type { InputState } from '../src/client/input/contract.ts'
|
||||
import { QueueDock, queueDockEntry, type QueueDockInjected } from '../src/client/queue/QueueDock.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
import { QueueDock, queueDockEntry, type QueueDockInjected, type QueueDockProps } from '../src/client/queue/QueueDock.tsx'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -54,9 +57,13 @@ function liveSession(initial: ConversationSnapshot) {
|
||||
|
||||
const INPUT_STATE: InputState = { draft: '', draftRev: 0, phase: 'plain', occurrences: [], queue: [] }
|
||||
|
||||
// Standard locale seat stub mirroring the real ns → common → key chain.
|
||||
const t: QueueDockProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
function kitFor(snapshot: ConversationSnapshot, injected: Partial<QueueDockInjected> = {}) {
|
||||
return {
|
||||
sessionId: SID,
|
||||
t,
|
||||
useSessions: (() => { throw new Error('unused') }) as unknown as SnapshotSelectorHook<SessionListState>,
|
||||
useWorkspaces: (() => { throw new Error('unused') }) as never,
|
||||
useProjection: (() => undefined) as never,
|
||||
|
||||
@@ -11,8 +11,11 @@ import type {
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConversationRootProps } from '../src/client/skeleton/ConversationRoot.tsx'
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { createChatStore } from '../src/client/stores.ts'
|
||||
import { SessionInputShell } from '../src/client/input/facade.ts'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
import { ConversationRoot } from '../src/client/skeleton/ConversationRoot.tsx'
|
||||
import { ConversationSession } from '../src/client/skeleton/ConversationSession.tsx'
|
||||
import { InputBar } from '../src/client/skeleton/InputBar.tsx'
|
||||
@@ -44,6 +47,9 @@ beforeEach(() => {
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
})
|
||||
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
const t: ConversationRootProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
const sid = (id: string) => id as SessionId
|
||||
const wid = (id: string) => id as WorkspaceId
|
||||
const SID = sid('s1')
|
||||
@@ -126,6 +132,7 @@ function mount(
|
||||
}}
|
||||
bindDraftMirror={write => wiring.bindMirror(write)}
|
||||
open={open}
|
||||
t={t}
|
||||
{...owner}
|
||||
/>
|
||||
)
|
||||
@@ -149,7 +156,7 @@ function mount(
|
||||
useLexicon={bindSnapshotSelector(wiring.lexicon)}
|
||||
stop={stop}
|
||||
command={() => Promise.resolve(true)}
|
||||
translateHint={(key: string) => key}
|
||||
t={t}
|
||||
renderSlot={(() => null) as InputBarProps['renderSlot']}
|
||||
{...bar}
|
||||
/>
|
||||
@@ -181,6 +188,7 @@ function mount(
|
||||
renderSlot,
|
||||
renderSlotChain,
|
||||
selectWorkspace: retargetWorkspace,
|
||||
t,
|
||||
}
|
||||
const view = render(<ConversationRoot {...props} />)
|
||||
return {
|
||||
@@ -241,7 +249,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
const header = b.view.container.querySelector('header')
|
||||
expect(host).not.toBeNull()
|
||||
expect(header?.getAttribute('aria-hidden')).toBe('true')
|
||||
expect(b.view.getByText("Let's start building")).toBeTruthy()
|
||||
expect(b.view.getByText('开始构建吧')).toBeTruthy()
|
||||
expect(b.view.queryByTestId('view-chat')).toBeNull()
|
||||
// The same machine-backed textarea is live in the hero, and the
|
||||
// persistence mirror stays bound (ConversationSession mounts chrome-hidden
|
||||
@@ -252,7 +260,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
expect(b.chat.store.getSnapshot().draft).toBe('draft in hero')
|
||||
// Picker: open through the chip; a pick switches to the other
|
||||
// workspace's blank session (draft carry is apply-layer wiring).
|
||||
fireEvent.click(b.view.getByRole('button', { name: 'Choose workspace' }))
|
||||
fireEvent.click(b.view.getByRole('button', { name: '选择工作区' }))
|
||||
const owner = b.pickerOwner() as { open: boolean; onPick(id: WorkspaceId): void }
|
||||
expect(owner.open).toBe(true)
|
||||
act(() => { owner.onPick(wid('second')) })
|
||||
@@ -274,7 +282,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
expect(after.value).toBe('kept across flip')
|
||||
expect(b.chat.store.getSnapshot().draft).toBe('kept across flip')
|
||||
expect(b.view.container.querySelector('[data-conversation-scroll]')?.contains(after)).toBe(true)
|
||||
expect(b.view.queryByText("Let's start building")).toBeNull()
|
||||
expect(b.view.queryByText('开始构建吧')).toBeNull()
|
||||
expect(b.view.getByTestId('view-chat')).toBeTruthy()
|
||||
})
|
||||
|
||||
@@ -295,7 +303,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
],
|
||||
selectWorkspace,
|
||||
)
|
||||
fireEvent.click(b.view.getByRole('button', { name: 'Choose workspace' }))
|
||||
fireEvent.click(b.view.getByRole('button', { name: '选择工作区' }))
|
||||
const owner = b.pickerOwner() as { onPick(id: WorkspaceId): void }
|
||||
await act(async () => { owner.onPick(wid('second')); await Promise.resolve() })
|
||||
expect(selectWorkspace).toHaveBeenCalledWith(wid('second'))
|
||||
@@ -305,7 +313,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
|
||||
it('blank session keeps the interactive picker chip (workspace switchable until the first message)', () => {
|
||||
const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true }))
|
||||
const chip = b.view.getByRole('button', { name: 'Choose workspace' })
|
||||
const chip = b.view.getByRole('button', { name: '选择工作区' })
|
||||
expect((chip as HTMLButtonElement).disabled).toBe(false)
|
||||
expect(b.slotCalls).toContain('conversation.hero.workspace')
|
||||
})
|
||||
|
||||
@@ -12,12 +12,20 @@ import type {
|
||||
ConversationSnapshot, RunningToolCall, SessionId, SessionListState, ToolResultNode, WorkspaceListState,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SelectionTarget, ToolRowOwnerProps, ToolRowProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { SelectionTarget } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { CHAT_TERMINAL_MAX_LINES, terminalCardModel } from '../src/client/contract/terminal-card-model.ts'
|
||||
import { createChatStore } from '../src/client/stores.ts'
|
||||
import { GenericToolCard } from '../src/client/chat/GenericToolCard.tsx'
|
||||
import { GenericToolCard, type GenericToolCardProps } from '../src/client/chat/GenericToolCard.tsx'
|
||||
import { DetailsPanel } from '../src/client/skeleton/DetailsPanel.tsx'
|
||||
import { BashRow } from '../src/client/toolviews/bash-sample.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
type BashRowProps = Parameters<typeof BashRow>[0]
|
||||
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
const t: GenericToolCardProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -217,8 +225,8 @@ describe('terminalCardModel', () => {
|
||||
})
|
||||
|
||||
describe('chat row terminal body', () => {
|
||||
const ownerProps = (block: RunningToolCall | ToolResultNode): ToolRowOwnerProps => ({
|
||||
callId: 'c1', toolName: 'bash', block, openFile: vi.fn(),
|
||||
const ownerProps = (block: RunningToolCall | ToolResultNode): GenericToolCardProps => ({
|
||||
callId: 'c1', toolName: 'bash', block, openFile: vi.fn(), t,
|
||||
})
|
||||
|
||||
it('the expanded body is the command output, capped tighter than the panel', () => {
|
||||
@@ -316,10 +324,11 @@ describe('BashRow terminal card', () => {
|
||||
phase: 'ready',
|
||||
})
|
||||
|
||||
const rowProps = (block: RunningToolCall | ToolResultNode): ToolRowProps => ({
|
||||
const rowProps = (block: RunningToolCall | ToolResultNode): BashRowProps => ({
|
||||
callId: 'c1', toolName: 'bash', block, openFile: vi.fn(),
|
||||
sessionId: SID, useSessions: bindSnapshotSelector(list()),
|
||||
} as unknown as ToolRowProps)
|
||||
t,
|
||||
} as unknown as BashRowProps)
|
||||
|
||||
it('renders the command output under the summary row, without an expand gesture', () => {
|
||||
const view = render(<BashRow {...rowProps(settled())} />)
|
||||
@@ -400,6 +409,7 @@ describe('DetailsPanel Output section', () => {
|
||||
useStore={bindSnapshotSelector(chat)}
|
||||
actions={chat.actions}
|
||||
closeDetails={vi.fn()}
|
||||
t={t}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
@@ -507,7 +517,7 @@ describe('DetailsPanel Output section', () => {
|
||||
// No terminal card: the generic path renders the result text in the Output
|
||||
// section's <pre> (the Input section has its own, hence the scoping).
|
||||
expect(view.container.querySelector('[data-terminal]')).toBeNull()
|
||||
const output = view.getByText('Output').closest('section')
|
||||
const output = view.getByText('输出').closest('section')
|
||||
expect(output?.querySelector('pre')?.textContent).toContain('a.ts b.ts')
|
||||
})
|
||||
|
||||
@@ -524,8 +534,8 @@ describe('DetailsPanel Output section', () => {
|
||||
nodes: [settled({ call: null, callView: null, resultView: resultTerminal({ title: 'ls -la' }) })],
|
||||
}), target)
|
||||
expect(view.getByText('c1')).toBeTruthy()
|
||||
expect(view.queryByText('Input')).toBeNull()
|
||||
expect(view.getByText('Output')).toBeTruthy()
|
||||
expect(view.queryByText('输入')).toBeNull()
|
||||
expect(view.getByText('输出')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('scans past other nodes and other calls before reporting the call out of window', () => {
|
||||
@@ -571,6 +581,7 @@ describe('DetailsPanel Output section', () => {
|
||||
useStore={bindSnapshotSelector(chat)}
|
||||
actions={chat.actions}
|
||||
closeDetails={closeDetails}
|
||||
t={t}
|
||||
/>,
|
||||
)
|
||||
fireEvent.click(view.getByRole('button', { name: '关闭详情' }))
|
||||
@@ -586,7 +597,7 @@ describe('DetailsPanel Output section', () => {
|
||||
}), target)
|
||||
// Scope to the Output section: the Input section's CodeBlock renders a
|
||||
// <pre> of its own, and it comes first in document order.
|
||||
expect(nonText.getByText('Output').closest('section')?.querySelector('pre')?.textContent)
|
||||
expect(nonText.getByText('输出').closest('section')?.querySelector('pre')?.textContent)
|
||||
.toBe('{\n "type": "reasoning",\n "text": "why"\n}')
|
||||
cleanup()
|
||||
const empty = mount(snapshot({
|
||||
|
||||
@@ -11,11 +11,18 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { TodoItem, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ToolRowProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
// Export discipline: packages/client/AGENTS.md.
|
||||
import { TodoRow, todoToolview } from '../src/client/toolviews/todo-row.tsx'
|
||||
import type { TodoDockProps } from '../src/client/skeleton/TodoPanel.tsx'
|
||||
import { TodoDock, TodoPanel, todoDockEntry } from '../src/client/skeleton/TodoPanel.tsx'
|
||||
import { NS, zh } from '../src/client/locales.ts'
|
||||
|
||||
type TodoRowProps = Parameters<typeof TodoRow>[0]
|
||||
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
const t: TodoDockProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -27,21 +34,21 @@ const LIST: TodoItem[] = [
|
||||
|
||||
describe('TodoPanel', () => {
|
||||
it('renders nothing while the list is empty', () => {
|
||||
const { container } = render(<TodoPanel todos={[]} />)
|
||||
const { container } = render(<TodoPanel todos={[]} t={t} />)
|
||||
expect(container.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
it('starts collapsed with the progress summary visible', () => {
|
||||
render(<TodoPanel todos={LIST} />)
|
||||
render(<TodoPanel todos={LIST} t={t} />)
|
||||
expect(screen.getByTestId('todo-panel')).toBeTruthy()
|
||||
expect(screen.getByText('To-dos')).toBeTruthy()
|
||||
expect(screen.getByText('1/3 tasks · 1 in progress')).toBeTruthy()
|
||||
expect(screen.getByText('任务清单')).toBeTruthy()
|
||||
expect(screen.getByText('1/3 项任务 · 1 项进行中')).toBeTruthy()
|
||||
expect(screen.getByRole('button', { expanded: false })).toBeTruthy()
|
||||
expect(screen.queryByRole('list')).toBeNull()
|
||||
})
|
||||
|
||||
it('expands to show one row per item with its status glyph', () => {
|
||||
render(<TodoPanel todos={LIST} />)
|
||||
render(<TodoPanel todos={LIST} t={t} />)
|
||||
fireEvent.click(screen.getByRole('button', { expanded: false }))
|
||||
const items = screen.getAllByRole('listitem')
|
||||
expect(items.map(li => li.getAttribute('data-status'))).toEqual(['completed', 'in_progress', 'pending'])
|
||||
@@ -52,23 +59,23 @@ describe('TodoPanel', () => {
|
||||
})
|
||||
|
||||
it('collapse hides an expanded list; expand restores; header keeps the count summary', () => {
|
||||
render(<TodoPanel todos={LIST} />)
|
||||
render(<TodoPanel todos={LIST} t={t} />)
|
||||
fireEvent.click(screen.getByRole('button', { expanded: false }))
|
||||
const header = screen.getByRole('button', { expanded: true })
|
||||
fireEvent.click(header)
|
||||
expect(screen.queryByRole('list')).toBeNull()
|
||||
// Collapsed header is title + progress only (no in-progress content hint).
|
||||
expect(screen.getByText('1/3 tasks · 1 in progress')).toBeTruthy()
|
||||
expect(screen.getByText('1/3 项任务 · 1 项进行中')).toBeTruthy()
|
||||
expect(screen.queryByText('写组件')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { expanded: false }))
|
||||
expect(screen.getAllByRole('listitem')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('collapsed header still shows zero in-progress when nothing is active', () => {
|
||||
render(<TodoPanel todos={[{ content: '都完了', status: 'completed' }]} />)
|
||||
render(<TodoPanel todos={[{ content: '都完了', status: 'completed' }]} t={t} />)
|
||||
expect(screen.getByRole('button', { expanded: false })).toBeTruthy()
|
||||
expect(screen.queryByText('都完了')).toBeNull()
|
||||
expect(screen.getByText('1/1 tasks · 0 in progress')).toBeTruthy()
|
||||
expect(screen.getByText('1/1 项任务 · 0 项进行中')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,7 +83,7 @@ describe('TodoPanel', () => {
|
||||
function dockProps(store: ReturnType<typeof createSnapshotStore<{ value: readonly TodoItem[] | null | undefined }>>): TodoDockProps {
|
||||
const useProjection = (_key: string, selector?: (v: unknown) => unknown) =>
|
||||
bindSnapshotSelector(store)(s => (selector ?? (v => v))(s.value))
|
||||
return { useProjection } as unknown as TodoDockProps
|
||||
return { useProjection, t } as unknown as TodoDockProps
|
||||
}
|
||||
|
||||
describe('TodoDock', () => {
|
||||
@@ -86,7 +93,7 @@ describe('TodoDock', () => {
|
||||
// Capability absent (no baseline/frame yet) renders nothing.
|
||||
expect(screen.queryByTestId('todo-panel')).toBeNull()
|
||||
act(() => { store.set({ value: LIST }) })
|
||||
expect(screen.getByText('1/3 tasks · 1 in progress')).toBeTruthy()
|
||||
expect(screen.getByText('1/3 项任务 · 1 项进行中')).toBeTruthy()
|
||||
// The pre-first-write whole value (null) retires the strip (the panel owns no data).
|
||||
act(() => { store.set({ value: null }) })
|
||||
expect(screen.queryByTestId('todo-panel')).toBeNull()
|
||||
@@ -97,7 +104,7 @@ describe('TodoDock', () => {
|
||||
expect(todoDockEntry.inject).toEqual(['slots', 'conversation'])
|
||||
const register = vi.fn()
|
||||
todoDockEntry.apply({ slots: { register } } as never)
|
||||
expect(register).toHaveBeenCalledWith({ name: 'conversation.input.dock', id: 'todo', order: 10 }, TodoDock)
|
||||
expect(register).toHaveBeenCalledWith({ name: 'conversation.input.dock', id: 'todo', order: 10, locale: NS }, TodoDock)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -107,13 +114,14 @@ const resultNode = (argsRaw: string, over?: Partial<ToolResultNode>): ToolResult
|
||||
content: [], isError: false, callView: null, resultView: null, ...over,
|
||||
})
|
||||
|
||||
function rowProps(block: unknown): ToolRowProps {
|
||||
function rowProps(block: unknown): TodoRowProps {
|
||||
return {
|
||||
callId: 'c1', toolName: 'todo_write', block,
|
||||
openFile: vi.fn(),
|
||||
sessionId: 's1',
|
||||
useSessions: () => undefined,
|
||||
} as unknown as ToolRowProps
|
||||
t,
|
||||
} as unknown as TodoRowProps
|
||||
}
|
||||
|
||||
describe('TodoRow', () => {
|
||||
@@ -183,6 +191,6 @@ describe('TodoRow', () => {
|
||||
expect(todoToolview.inject).toEqual(['slots', 'conversation'])
|
||||
const register = vi.fn()
|
||||
todoToolview.apply({ slots: { register } } as never)
|
||||
expect(register).toHaveBeenCalledWith({ name: 'conversation.chat.toolview', key: 'todo_write' }, TodoRow)
|
||||
expect(register).toHaveBeenCalledWith({ name: 'conversation.chat.toolview', key: 'todo_write', locale: NS }, TodoRow)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user