Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.md # docs/core-data-structures/core.zh.md # docs/core-data-structures/llm-streaming.i18n.yaml # docs/core-data-structures/llm-streaming.md # docs/core-data-structures/llm-streaming.zh.md # docs/event-producer-consumer.md # docs/module-graph.md # docs/subsystems/attachment.i18n.yaml # docs/subsystems/attachment.md # docs/subsystems/attachment.zh.md # docs/subsystems/core.i18n.yaml # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/README.i18n.yaml # packages/README.md # packages/README.zh.md # packages/client/runtime/package.json # packages/client/ui-conversation/package.json # packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx # packages/client/ui-conversation/src/client/chat/ChatView.tsx # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/src/client/index.ts # packages/client/ui-conversation/tests/input-bar.spec.tsx # packages/compact/compact-basic/README.i18n.yaml # packages/compact/compact-basic/README.md # packages/compact/compact-basic/README.zh.md # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/index.ts # packages/host/apiproxy/src/api/sessions.ts # packages/host/apiproxy/src/index.ts # packages/host/apiproxy/tests/api-proxy-models.spec.ts # packages/self-modification/tool-cordis/src/api-catalog.ts # pnpm-lock.yaml # scripts/type-equiv.manifest.json
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
// @vitest-environment jsdom
|
||||
// InputBar behavior over the machine wiring: Enter-send semantics (IME guard,
|
||||
// Shift newline, busy Enter policy, Ctrl/Meta steering, repeat suppression), running
|
||||
// semantics (input stays free; primary turns stop), the machine pending lock,
|
||||
// semantics (input stays free; continuable children keep Send beside Stop), the machine pending lock,
|
||||
// decoration backdrop, error/notice strips, and the focus-keeping mousedown.
|
||||
|
||||
import { afterEach, describe, expect, it, onTestFinished, 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 { createSnapshotStore, EMPTY_CHAT_SNAPSHOT } 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'
|
||||
@@ -37,7 +37,8 @@ const SID = 's1' as SessionId
|
||||
|
||||
function snapshotOf(overrides: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [], codeDispatches: new Map(),
|
||||
sessionId: SID, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
|
||||
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
|
||||
promptError: null, blank: false, subagent: null, lastAgentError: null,
|
||||
@@ -155,11 +156,15 @@ function bench(over?: BenchOptions) {
|
||||
}
|
||||
const view = render(<InputBar {...props} />)
|
||||
const textarea = view.container.querySelector('textarea')!
|
||||
const stopping = over?.running === true && over.subagent === undefined
|
||||
const primaryStops = over?.running === true && over.subagent === undefined
|
||||
const button = view.container.querySelector<HTMLButtonElement>(
|
||||
`button[aria-label="${stopping ? '停止生成' : '发送消息'}"]`,
|
||||
`button[aria-label="${primaryStops ? '停止生成' : '发送消息'}"]`,
|
||||
)!
|
||||
return { view, textarea, button, props, sink, shell, wiring: shell, session, stop, removeImage, slotCalls, menuLauncher }
|
||||
const interruptButton = view.container.querySelector<HTMLButtonElement>('button[aria-label="停止生成"]')
|
||||
return {
|
||||
view, textarea, button, interruptButton, props, sink, shell, wiring: shell, session, stop, removeImage, slotCalls,
|
||||
menuLauncher,
|
||||
}
|
||||
}
|
||||
|
||||
describe('image draft rail', () => {
|
||||
@@ -288,10 +293,10 @@ describe('Enter semantics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('running and lock semantics (queue cut 1)', () => {
|
||||
describe('running and lock semantics', () => {
|
||||
it('running keeps the input free (typing + Enter queue) while the primary turns stop', () => {
|
||||
const { textarea, button, stop, sink } = bench({ running: true, draft: '排队消息' })
|
||||
expect(textarea.disabled).toBe(false) // running no longer locks
|
||||
expect(textarea.disabled).toBe(false)
|
||||
fireEvent.change(textarea, { target: { value: '排队消息2' } })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('排队消息2', [], 'queue')
|
||||
@@ -316,8 +321,8 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
expect(ctrl.sink).toHaveBeenCalledWith('also queue', [], 'queue')
|
||||
})
|
||||
|
||||
it('running subagent primary admits a follow-up instead of exposing Stop', () => {
|
||||
const { button, sink, stop } = bench({
|
||||
it('running continuable subagent keeps Send beside an independent Stop', () => {
|
||||
const { button, interruptButton, textarea, sink, stop } = bench({
|
||||
running: true,
|
||||
draft: '后续消息',
|
||||
subagent: {
|
||||
@@ -330,22 +335,53 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
},
|
||||
})
|
||||
expect(button.getAttribute('aria-label')).toBe('发送消息')
|
||||
expect(interruptButton).not.toBeNull()
|
||||
expect(textarea.disabled).toBe(false)
|
||||
fireEvent.click(button)
|
||||
expect(sink).toHaveBeenCalledWith('后续消息', [], 'queue')
|
||||
expect(stop).not.toHaveBeenCalled()
|
||||
fireEvent.click(interruptButton!)
|
||||
expect(stop).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
const empty = bench({
|
||||
it('parent-offline running continuable locks Send but keeps independent Stop usable', () => {
|
||||
const { button, interruptButton, textarea, stop, view } = bench({
|
||||
running: true,
|
||||
draft: '',
|
||||
subagent: {
|
||||
address: {
|
||||
parentSessionId: 'parent' as SessionId,
|
||||
childSessionId: SID,
|
||||
mode: 'continuable',
|
||||
},
|
||||
parentAvailable: false,
|
||||
},
|
||||
})
|
||||
expect(textarea.disabled).toBe(true)
|
||||
expect(textarea.placeholder).toBe('父会话已离线,无法继续发送;仍可停止当前运行')
|
||||
expect((view.getByLabelText('命令') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect(button.getAttribute('aria-label')).toBe('发送消息')
|
||||
expect(button.disabled).toBe(true)
|
||||
expect(interruptButton?.disabled).toBe(false)
|
||||
fireEvent.click(interruptButton!)
|
||||
expect(stop).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('running one-shot subagent never exposes Stop', () => {
|
||||
const { button, interruptButton, stop } = bench({
|
||||
running: true,
|
||||
draft: '不可停止',
|
||||
subagent: {
|
||||
address: {
|
||||
parentSessionId: 'parent' as SessionId,
|
||||
childSessionId: SID,
|
||||
mode: 'one-shot',
|
||||
},
|
||||
parentAvailable: true,
|
||||
},
|
||||
})
|
||||
expect(empty.button.disabled).toBe(true)
|
||||
expect(button.getAttribute('aria-label')).toBe('发送消息')
|
||||
expect(interruptButton).toBeNull()
|
||||
expect(stop).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps both running subagent Enter gestures on Queue transport', () => {
|
||||
@@ -460,8 +496,8 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
// scrollport element holds both layers.
|
||||
expect(scroll.contains(textarea)).toBe(true)
|
||||
expect(scroll.contains(backdrop)).toBe(true)
|
||||
// The glyph layer carries the draft and nothing else: with one scrollport
|
||||
// it no longer pads its own height to match a second box's scroll extent.
|
||||
// The glyph layer carries the draft and nothing else — no height padding
|
||||
// to a second box's scroll extent.
|
||||
expect(backdrop.textContent).toBe('line\n'.repeat(40))
|
||||
})
|
||||
|
||||
@@ -690,7 +726,7 @@ describe('decorations', () => {
|
||||
expect(shell.snapshot.draft).toBe('参考 \uFFFC 内容')
|
||||
})
|
||||
|
||||
it('a lexicon-matched plain token renders the text-ref mark (decision 21)', () => {
|
||||
it('a lexicon-matched plain token renders the text-ref mark', () => {
|
||||
const lexicon = new Map<'/' | '@', readonly string[]>([['/', ['fixture-demo']]])
|
||||
const { view, shell } = bench({ lexicon })
|
||||
act(() => { shell.setDraft('use /fixture-demo now') })
|
||||
@@ -702,7 +738,7 @@ describe('decorations', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertText (decision 21 scoped event body)', () => {
|
||||
describe('insertText (scoped event body)', () => {
|
||||
it('splices plain text over the span and reports success as true', () => {
|
||||
const { shell } = bench({ draft: '/fix' })
|
||||
const ok = shell.insertText('/fixture-demo ', { start: 0, end: 4, draftRev: shell.snapshot.draftRev })
|
||||
@@ -752,7 +788,7 @@ describe('strips and variants', () => {
|
||||
})
|
||||
|
||||
describe('command launcher chrome and control seats', () => {
|
||||
it('renders the command launcher; the Access chip is absent without the permissions projection; plan/model seats render EMPTY without entries (B ruling)', () => {
|
||||
it('renders the command launcher; the Access chip is absent without the permissions projection; plan/model seats render EMPTY without entries', () => {
|
||||
const { view, slotCalls } = bench()
|
||||
expect(view.getByLabelText('命令')).toBeTruthy()
|
||||
// Capability absent (no projection value): the chip renders nothing.
|
||||
|
||||
Reference in New Issue
Block a user