Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/config-catalog.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/llm-streaming.i18n.yaml # docs/module-graph.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/README.i18n.yaml # packages/client/connection/src/client/fixture.ts # packages/client/connection/src/index.ts # packages/client/runtime/README.i18n.yaml # packages/client/runtime/README.md # packages/client/runtime/README.zh.md # packages/client/runtime/src/client/sessions/conversation.ts # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-conversation/src/client/chat/ChatView.tsx # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/src/client/contract/slots.ts # packages/client/ui-trajectory/tests/views.spec.tsx # packages/compact/compact-basic/README.i18n.yaml # packages/cordis/tool-cordis/src/api-catalog.ts # 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/fetch-carrier.spec.ts # packages/llm/llm-deepseek/src/adapter.ts # packages/llm/llm-deepseek/tests/adapter.spec.ts # packages/llm/llm-deepseek/tests/serialize.spec.ts # packages/llm/llm-pi-ai/README.i18n.yaml # packages/llm/llm-pi-ai/src/adapter.ts # packages/llm/llm-pi-ai/src/index.ts # packages/llm/llm-pi-ai/tests/adapter.spec.ts # packages/llm/llm/src/types.ts # packages/ui/tui/README.i18n.yaml # packages/ui/tui/src/index.ts # packages/ui/tui/tests/tui.spec.ts
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
// hero (blank session) and active phases — same textarea DOM node, machine-
|
||||
// owned draft, and the hero workspace picker (switching = retargetWorkspace).
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
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 type {
|
||||
@@ -48,14 +48,18 @@ const workspaceState = (items: readonly WorkspaceView[]): WorkspaceListState =>
|
||||
function conversationSnapshot(overrides: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
|
||||
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
|
||||
pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false,
|
||||
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
|
||||
promptError: null, blank: false, lastAgentError: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function mount(snapshot: ConversationSnapshot, workspaceRows: WorkspaceView[] = [{ ...workspace('one'), sessionIds: [SID] }]) {
|
||||
function mount(
|
||||
snapshot: ConversationSnapshot,
|
||||
workspaceRows: WorkspaceView[] = [{ ...workspace('one'), sessionIds: [SID] }],
|
||||
retargetWorkspace = vi.fn(async (_workspaceId: WorkspaceId) => {}),
|
||||
) {
|
||||
const root = sid('root')
|
||||
const sessions = createSnapshotStore<SessionListState>({
|
||||
ids: [root, SID],
|
||||
@@ -76,7 +80,6 @@ function mount(snapshot: ConversationSnapshot, workspaceRows: WorkspaceView[] =
|
||||
const inputActions = wiring.actions
|
||||
const stop = vi.fn()
|
||||
const open = vi.fn()
|
||||
const retargetWorkspace = vi.fn()
|
||||
const slotCalls: string[] = []
|
||||
let pickerOwner: unknown
|
||||
const renderSlot = ((key: string, owner: object, opts?: { only?: string }) => {
|
||||
@@ -162,7 +165,13 @@ describe('ConversationRoot resident composer', () => {
|
||||
})
|
||||
|
||||
it('hero phase: same textarea, hero chrome, no header, picker switches the workspace', () => {
|
||||
const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true }))
|
||||
const b = mount(
|
||||
conversationSnapshot({ composerPhase: 'blank', blank: true }),
|
||||
[
|
||||
{ ...workspace('one'), sessionIds: [SID] },
|
||||
{ ...workspace('second'), title: 'Selected Folder' },
|
||||
],
|
||||
)
|
||||
// Hero chrome present, view ring absent.
|
||||
expect(b.view.getByText("Let's start building")).toBeTruthy()
|
||||
expect(b.view.queryByTestId('view-chat')).toBeNull()
|
||||
@@ -177,8 +186,9 @@ describe('ConversationRoot resident composer', () => {
|
||||
fireEvent.click(b.view.getByRole('button', { name: 'Choose workspace' }))
|
||||
const owner = b.pickerOwner() as { open: boolean; onPick(id: WorkspaceId): void }
|
||||
expect(owner.open).toBe(true)
|
||||
owner.onPick(wid('second'))
|
||||
act(() => { owner.onPick(wid('second')) })
|
||||
expect(b.retargetWorkspace).toHaveBeenCalledWith(wid('second'))
|
||||
expect(b.view.getByText('Selected Folder')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('textarea DOM identity survives the hero → active flip', () => {
|
||||
@@ -195,6 +205,24 @@ describe('ConversationRoot resident composer', () => {
|
||||
expect(b.view.getByTestId('view-chat')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('rolls the pending workspace label back when switching fails', async () => {
|
||||
const selectWorkspace = vi.fn(async () => { throw new Error('connect failed') })
|
||||
const b = mount(
|
||||
conversationSnapshot({ composerPhase: 'blank', blank: true }),
|
||||
[
|
||||
{ ...workspace('one'), sessionIds: [SID] },
|
||||
{ ...workspace('second'), title: 'Selected Folder' },
|
||||
],
|
||||
selectWorkspace,
|
||||
)
|
||||
fireEvent.click(b.view.getByRole('button', { name: 'Choose workspace' }))
|
||||
const owner = b.pickerOwner() as { onPick(id: WorkspaceId): void }
|
||||
await act(async () => { owner.onPick(wid('second')); await Promise.resolve() })
|
||||
expect(selectWorkspace).toHaveBeenCalledWith(wid('second'))
|
||||
expect(b.view.queryByText('Selected Folder')).toBeNull()
|
||||
expect(b.view.getByText('one')).toBeTruthy()
|
||||
})
|
||||
|
||||
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' })
|
||||
|
||||
Reference in New Issue
Block a user