Merge remote-tracking branch 'origin/master' into codex/session-pending-interactions

# Conflicts:
#	packages/client/connection/README.i18n.yaml
#	packages/client/connection/README.md
#	packages/client/connection/README.zh.md
This commit is contained in:
Turtle
2026-08-04 19:03:01 +08:00
108 changed files with 2160 additions and 515 deletions
@@ -122,7 +122,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 项任务 · 1 进行中')
expect(panel!.textContent).toContain('1 已完成\u2002·\u20021 进行中\u2002·\u20021 待处理')
fireEvent.click(panel!.querySelector('button')!)
expect([...panel!.querySelectorAll('li')].map(li => li.getAttribute('data-status')))
.toEqual(['completed', 'in_progress', 'pending'])
@@ -102,16 +102,10 @@ describe('MessageItem arms', () => {
expect(screen.getByRole('tooltip').textContent).toBe('仅可从已完成轮次的最后一条消息分支')
})
it('user copy stays quiet when execCommand throws or is absent', () => {
it('user copy never claims success when the host rejects the write', async () => {
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: undefined,
})
Object.defineProperty(document, 'execCommand', {
configurable: true,
value: () => {
throw new Error('denied')
},
value: { writeText: vi.fn().mockRejectedValue(new Error('denied')) },
})
render(
<MessageItem t={t} node={{
@@ -122,12 +116,91 @@ describe('MessageItem arms', () => {
/>,
)
fireEvent.click(screen.getByRole('button', { name: '复制' }))
await act(async () => {
await Promise.resolve()
await Promise.resolve()
})
expect(screen.getByRole('button', { name: '复制' })).toBeTruthy()
expect(screen.queryByRole('button', { name: '复制成功' })).toBeNull()
})
Object.defineProperty(document, 'execCommand', {
it('copy swaps to the check success chrome, gates re-clicks, and reverts after a second', async () => {
vi.useFakeTimers()
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: undefined,
value: { writeText },
})
render(
<MessageItem t={t} node={{
kind: 'user', seq: 1, time: 1_000,
content: [{ type: 'text', text: 'copied body' }] as never,
source: null,
}}
/>,
)
const copy = screen.getByRole('button', { name: '复制' })
fireEvent.click(copy)
fireEvent.click(copy)
expect(writeText).toHaveBeenCalledTimes(1)
// Two microtask ticks: writeClipboard's own await, then the .then that
// lands the success chrome.
await act(async () => {
await Promise.resolve()
await Promise.resolve()
})
const done = screen.getByRole('button', { name: '复制成功' })
fireEvent.click(done)
expect(writeText).toHaveBeenCalledTimes(1)
act(() => { vi.advanceTimersByTime(1000) })
expect(screen.getByRole('button', { name: '复制' })).toBeTruthy()
})
it('clears copy feedback work when the message unmounts', async () => {
vi.useFakeTimers()
let finishWrite!: () => void
const writeText = vi.fn(() => new Promise<void>((resolve) => { finishWrite = resolve }))
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText },
})
const view = render(
<MessageItem t={t} node={{
kind: 'user', seq: 1, time: 1_000,
content: [{ type: 'text', text: 'copied body' }] as never,
source: null,
}}
/>,
)
fireEvent.click(screen.getByRole('button', { name: '复制' }))
view.unmount()
await act(async () => {
finishWrite()
await Promise.resolve()
await Promise.resolve()
})
expect(vi.getTimerCount()).toBe(0)
const mounted = render(
<MessageItem t={t} node={{
kind: 'user', seq: 2, time: 1_000,
content: [{ type: 'text', text: 'copied body' }] as never,
source: null,
}}
/>,
)
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText: vi.fn().mockResolvedValue(undefined) },
})
fireEvent.click(screen.getByRole('button', { name: '复制' }))
await act(async () => {
await Promise.resolve()
await Promise.resolve()
})
expect(screen.getByRole('button', { name: '复制成功' })).toBeTruthy()
mounted.unmount()
expect(vi.getTimerCount()).toBe(0)
})
it('consumed steering renders copy and branch actions without a badge', () => {
@@ -502,6 +575,6 @@ describe('small branch tails', () => {
: undefined}
/>,
)
expect(view.container.textContent).toBe('1 turns · 1 steps|Input 0 tok · Output 10 tok')
expect(view.container.textContent).toBe('1 turns · 1 steps| Input 0 tok · Output 10 tok')
})
})
@@ -133,7 +133,7 @@ describe('StatsLine', () => {
const view = render(<StatsLine {...props(source)} />)
// No timing on the fixture: the duration group drops out whole. Tokens come
// from the projection, so paging the window cannot change them.
expect(view.container.textContent).toBe('1 turns · 1 steps|Cache hit 90%|Input 100 tok · Output 5 tok')
expect(view.container.textContent).toBe('1 turns · 1 steps| Cache hit 90%| Input 100 tok · Output 5 tok')
const empty = makeSource()
const emptyView = render(<StatsLine {...props(empty.source, {
tokenUsage: { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
@@ -149,7 +149,7 @@ describe('StatsLine', () => {
contextPressure: { pressureTokens: 32_000, contextWindow: 128_000 },
})} />)
expect(view.container.textContent)
.toBe('Context 25% of 128K|Cache hit 90%|Input 100 tok · Output 5 tok')
.toBe('Context 25% of 128K| Cache hit 90%| Input 100 tok · Output 5 tok')
})
it('renders context occupancy only when the projection knows a capacity', () => {
@@ -196,7 +196,7 @@ describe('StatsLine', () => {
const view = render(<StatsLine {...props(source, {
tokenUsage: { uncachedInputTokens: 0, outputTokens: 7, cacheReadTokens: 0, cacheWriteTokens: 0 },
})} />)
expect(view.container.textContent).toBe('1 turns · 1 steps|Input 0 tok · Output 7 tok')
expect(view.container.textContent).toBe('1 turns · 1 steps| Input 0 tok · Output 7 tok')
})
it('includes cache writes in billed input and the cache-hit denominator', () => {
@@ -210,7 +210,7 @@ describe('StatsLine', () => {
},
})} />)
expect(view.container.textContent)
.toBe('1 turns · 1 steps|Cache hit 45%|Input 200 tok · Output 7 tok')
.toBe('1 turns · 1 steps| Cache hit 45%| Input 200 tok · Output 7 tok')
})
it('renders ZERO times during streaming chunk frames (RFC hard acceptance)', () => {
@@ -143,7 +143,6 @@ function bench(over?: BenchOptions) {
}
const view = render(<InputBar {...props} />)
const textarea = view.container.querySelector('textarea')!
// aria-label (not role name): title carries the same label and would double-match.
const stopping = over?.running === true && over.subagent === undefined
const button = view.container.querySelector<HTMLButtonElement>(
`button[aria-label="${stopping ? '停止生成' : '发送消息'}"]`,
@@ -724,6 +723,8 @@ describe('command launcher chrome and control seats', () => {
const trigger = view.getByLabelText(/^访问模式/) as HTMLButtonElement
// Title-case display is presentation only; the menu ids stay machine names.
expect(trigger.textContent).toBe('Read Only')
expect([...trigger.querySelectorAll('svg')]
.every(icon => icon.closest('[aria-hidden="true"]') !== null)).toBe(true)
fireEvent.click(trigger)
const items = view.getAllByRole('menuitem')
expect(items.map(o => o.textContent)).toEqual(['Read Only', 'Workspace Write', 'Full access'])
@@ -38,15 +38,24 @@ describe('TodoPanel', () => {
expect(container.innerHTML).toBe('')
})
it('starts collapsed with the progress summary visible', () => {
it('starts collapsed with the per-status count summary visible', () => {
render(<TodoPanel todos={LIST} t={t} />)
expect(screen.getByTestId('todo-panel')).toBeTruthy()
expect(screen.getByText('任务清单')).toBeTruthy()
expect(screen.getByText('1/3 项任务 · 1 进行中')).toBeTruthy()
expect(screen.getByText('任务')).toBeTruthy()
expect(screen.getByText('1 已完成 · 1 进行中 · 1 待处理')).toBeTruthy()
expect(screen.getByRole('button', { expanded: false })).toBeTruthy()
expect(screen.queryByRole('list')).toBeNull()
})
it('omits the completed segment while nothing is done yet', () => {
render(<TodoPanel todos={[
{ content: '写组件', status: 'in_progress' },
{ content: '补测试', status: 'pending' },
]} t={t} />)
expect(screen.getByText('1 进行中 · 1 待处理')).toBeTruthy()
expect(screen.queryByText(/已完成/)).toBeNull()
})
it('expands to show one row per item with its status glyph', () => {
render(<TodoPanel todos={LIST} t={t} />)
fireEvent.click(screen.getByRole('button', { expanded: false }))
@@ -65,17 +74,18 @@ describe('TodoPanel', () => {
fireEvent.click(header)
expect(screen.queryByRole('list')).toBeNull()
// Collapsed header is title + progress only (no in-progress content hint).
expect(screen.getByText('1/3 项任务 · 1 进行中')).toBeTruthy()
expect(screen.getByText('1 已完成 · 1 进行中 · 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', () => {
it('an all-completed list collapses the summary to the done count alone', () => {
render(<TodoPanel todos={[{ content: '都完了', status: 'completed' }]} t={t} />)
expect(screen.getByRole('button', { expanded: false })).toBeTruthy()
expect(screen.queryByText('都完了')).toBeNull()
expect(screen.getByText('1/1 项任务 · 0 项进行中')).toBeTruthy()
expect(screen.getByText('1 已完成')).toBeTruthy()
expect(screen.queryByText(/进行中|待处理/)).toBeNull()
})
})
@@ -93,7 +103,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 项任务 · 1 进行中')).toBeTruthy()
expect(screen.getByText('1 已完成 · 1 进行中 · 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()