|
@@ -1,5 +1,6 @@
|
|
|
// @vitest-environment jsdom
|
|
// @vitest-environment jsdom
|
|
|
|
|
|
|
|
|
|
+import type { InboxState } from '@deepseek-ai/dsh-agent/types'
|
|
|
import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
|
|
import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
@@ -60,10 +61,13 @@ beforeEach(() => {
|
|
|
const SID = 's1' as SessionId
|
|
const SID = 's1' as SessionId
|
|
|
type RoutedChatNodeOwner = ChatNodeOwnerProps & { readonly node: ChatNode }
|
|
type RoutedChatNodeOwner = ChatNodeOwnerProps & { readonly node: ChatNode }
|
|
|
|
|
|
|
|
-function sessionSnapshot(overrides: Partial<SessionSnapshot> = {}): SessionSnapshot {
|
|
|
|
|
|
|
+interface TestSessionSnapshot extends SessionSnapshot {
|
|
|
|
|
+ readonly testInbox?: InboxState
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function sessionSnapshot(overrides: Partial<TestSessionSnapshot> = {}): TestSessionSnapshot {
|
|
|
return {
|
|
return {
|
|
|
sessionId: SID,
|
|
sessionId: SID,
|
|
|
- queue: [],
|
|
|
|
|
pendingSubmissions: [],
|
|
pendingSubmissions: [],
|
|
|
running: false,
|
|
running: false,
|
|
|
removed: false,
|
|
removed: false,
|
|
@@ -82,11 +86,11 @@ function sessionSnapshot(overrides: Partial<SessionSnapshot> = {}): SessionSnaps
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Scripted Session source: set() swaps the top-level object like the real Controller binding. */
|
|
/** Scripted Session source: set() swaps the top-level object like the real Controller binding. */
|
|
|
-function makeSessionSource(init: Partial<SessionSnapshot> = {}) {
|
|
|
|
|
|
|
+function makeSessionSource(init: Partial<TestSessionSnapshot> = {}) {
|
|
|
let snap = sessionSnapshot(init)
|
|
let snap = sessionSnapshot(init)
|
|
|
const subs = new Set<() => void>()
|
|
const subs = new Set<() => void>()
|
|
|
return {
|
|
return {
|
|
|
- set: (next: Partial<SessionSnapshot>) => {
|
|
|
|
|
|
|
+ set: (next: Partial<TestSessionSnapshot>) => {
|
|
|
snap = { ...snap, ...next }
|
|
snap = { ...snap, ...next }
|
|
|
for (const fn of [...subs]) fn()
|
|
for (const fn of [...subs]) fn()
|
|
|
},
|
|
},
|
|
@@ -103,7 +107,7 @@ function makeSessionSource(init: Partial<SessionSnapshot> = {}) {
|
|
|
type ChatSlice = Partial<LegacyConversationSlice> & {
|
|
type ChatSlice = Partial<LegacyConversationSlice> & {
|
|
|
readonly turnUsages?: NonNullable<Parameters<typeof chatSnapshotFixture>[0]>['turnUsages']
|
|
readonly turnUsages?: NonNullable<Parameters<typeof chatSnapshotFixture>[0]>['turnUsages']
|
|
|
}
|
|
}
|
|
|
-type HarnessUpdate = ChatSlice & Partial<SessionSnapshot> & { readonly chat?: ChatSnapshot }
|
|
|
|
|
|
|
+type HarnessUpdate = ChatSlice & Partial<TestSessionSnapshot> & { readonly chat?: ChatSnapshot }
|
|
|
|
|
|
|
|
/** Scripted Chat target source, independent from Session lifecycle state. */
|
|
/** Scripted Chat target source, independent from Session lifecycle state. */
|
|
|
function makeChatSource(init: ChatSlice = {}, snapshot?: ChatSnapshot) {
|
|
function makeChatSource(init: ChatSlice = {}, snapshot?: ChatSnapshot) {
|
|
@@ -226,7 +230,7 @@ function bindKeyedSnapshotSelector<Value>(
|
|
|
|
|
|
|
|
function makeHarness(
|
|
function makeHarness(
|
|
|
init: HarnessUpdate = {},
|
|
init: HarnessUpdate = {},
|
|
|
- sessionOverrides: Partial<SessionSnapshot> = {},
|
|
|
|
|
|
|
+ sessionOverrides: Partial<TestSessionSnapshot> = {},
|
|
|
chatSnapshot?: ChatSnapshot,
|
|
chatSnapshot?: ChatSnapshot,
|
|
|
) {
|
|
) {
|
|
|
const {
|
|
const {
|
|
@@ -242,6 +246,7 @@ function makeHarness(
|
|
|
...(turnUsages === undefined ? {} : { turnUsages }),
|
|
...(turnUsages === undefined ? {} : { turnUsages }),
|
|
|
}
|
|
}
|
|
|
const session = makeSessionSource({ ...sessionInit, ...sessionOverrides })
|
|
const session = makeSessionSource({ ...sessionInit, ...sessionOverrides })
|
|
|
|
|
+ const useTestSession = bindSnapshotSelector(session.source)
|
|
|
const chatSource = makeChatSource(chatSlice, initialChat ?? chatSnapshot)
|
|
const chatSource = makeChatSource(chatSlice, initialChat ?? chatSnapshot)
|
|
|
const useChatNode = bindKeyedSnapshotSelector(
|
|
const useChatNode = bindKeyedSnapshotSelector(
|
|
|
key => chatSource.source.getSnapshot().nodes.source(key),
|
|
key => chatSource.source.getSnapshot().nodes.source(key),
|
|
@@ -378,7 +383,10 @@ function makeHarness(
|
|
|
createSnapshotStore<SessionPendingInteractionSnapshot>(new Map()),
|
|
createSnapshotStore<SessionPendingInteractionSnapshot>(new Map()),
|
|
|
),
|
|
),
|
|
|
useWorkspaces: emptyWorkspaces(),
|
|
useWorkspaces: emptyWorkspaces(),
|
|
|
- useProjection: () => outlineValue,
|
|
|
|
|
|
|
+ useProjection: (key: string) => {
|
|
|
|
|
+ const inbox = useTestSession(snapshot => snapshot.testInbox)
|
|
|
|
|
+ return key === 'inbox' ? inbox : outlineValue
|
|
|
|
|
+ },
|
|
|
useInput: (() => { throw new Error('unused') }),
|
|
useInput: (() => { throw new Error('unused') }),
|
|
|
inputActions: {
|
|
inputActions: {
|
|
|
setDraft: () => {},
|
|
setDraft: () => {},
|
|
@@ -949,23 +957,23 @@ describe('ChatView', () => {
|
|
|
})
|
|
})
|
|
|
const pending = {
|
|
const pending = {
|
|
|
id: 'steer-occurrence' as never,
|
|
id: 'steer-occurrence' as never,
|
|
|
- messageId: 'steer-message' as never,
|
|
|
|
|
- placement: 'steering' as const,
|
|
|
|
|
|
|
+ role: 'user' as const,
|
|
|
|
|
+ source: { kind: 'user' as const },
|
|
|
content: [{ type: 'text' as const, text: 'interrupt now' }],
|
|
content: [{ type: 'text' as const, text: 'interrupt now' }],
|
|
|
preview: 'interrupt now',
|
|
preview: 'interrupt now',
|
|
|
text: 'interrupt now',
|
|
text: 'interrupt now',
|
|
|
}
|
|
}
|
|
|
const queued = {
|
|
const queued = {
|
|
|
id: 'queued-occurrence' as never,
|
|
id: 'queued-occurrence' as never,
|
|
|
- messageId: 'queued-message' as never,
|
|
|
|
|
- placement: 'queued' as const,
|
|
|
|
|
|
|
+ role: 'user' as const,
|
|
|
|
|
+ source: { kind: 'user' as const },
|
|
|
content: [{ type: 'text' as const, text: 'later' }],
|
|
content: [{ type: 'text' as const, text: 'later' }],
|
|
|
preview: 'later',
|
|
preview: 'later',
|
|
|
text: 'later',
|
|
text: 'later',
|
|
|
}
|
|
}
|
|
|
const h = makeHarness(
|
|
const h = makeHarness(
|
|
|
{ nodes: [assistant(1, 'working')] },
|
|
{ nodes: [assistant(1, 'working')] },
|
|
|
- { queue: [queued, pending], running: true },
|
|
|
|
|
|
|
+ { testInbox: { 'next-turn': [queued], 'next-step': [pending] }, running: true },
|
|
|
)
|
|
)
|
|
|
const view = render(<h.ChatView {...h.props} />)
|
|
const view = render(<h.ChatView {...h.props} />)
|
|
|
|
|
|
|
@@ -980,12 +988,12 @@ describe('ChatView', () => {
|
|
|
& Node.DOCUMENT_POSITION_FOLLOWING).not.toBe(0)
|
|
& Node.DOCUMENT_POSITION_FOLLOWING).not.toBe(0)
|
|
|
|
|
|
|
|
act(() => {
|
|
act(() => {
|
|
|
- h.setSession({ queue: [queued] })
|
|
|
|
|
|
|
+ h.setSession({ testInbox: { 'next-turn': [queued], 'next-step': [] } })
|
|
|
h.setChat({
|
|
h.setChat({
|
|
|
nodes: [
|
|
nodes: [
|
|
|
assistant(1, 'working'),
|
|
assistant(1, 'working'),
|
|
|
{
|
|
{
|
|
|
- kind: 'steering', messageId: pending.messageId,
|
|
|
|
|
|
|
+ kind: 'steering', messageId: pending.id,
|
|
|
seq: 2, time: 2_000,
|
|
seq: 2, time: 2_000,
|
|
|
content: [{ type: 'text', text: 'interrupt now' }], source: null,
|
|
content: [{ type: 'text', text: 'interrupt now' }], source: null,
|
|
|
},
|
|
},
|
|
@@ -1017,8 +1025,8 @@ describe('ChatView', () => {
|
|
|
it('keeps a later pending occurrence visible when it reuses a durable MessageId', () => {
|
|
it('keeps a later pending occurrence visible when it reuses a durable MessageId', () => {
|
|
|
const pending = {
|
|
const pending = {
|
|
|
id: 'steer-occurrence-later' as never,
|
|
id: 'steer-occurrence-later' as never,
|
|
|
- messageId: 'shared-steer-message' as never,
|
|
|
|
|
- placement: 'steering' as const,
|
|
|
|
|
|
|
+ role: 'user' as const,
|
|
|
|
|
+ source: { kind: 'user' as const },
|
|
|
content: [{ type: 'text' as const, text: 'same steering' }],
|
|
content: [{ type: 'text' as const, text: 'same steering' }],
|
|
|
preview: 'same steering',
|
|
preview: 'same steering',
|
|
|
text: 'same steering',
|
|
text: 'same steering',
|
|
@@ -1028,7 +1036,7 @@ describe('ChatView', () => {
|
|
|
kind: 'user', seq: 2, time: 2_000,
|
|
kind: 'user', seq: 2, time: 2_000,
|
|
|
content: pending.content, source: null,
|
|
content: pending.content, source: null,
|
|
|
}],
|
|
}],
|
|
|
- }, { queue: [pending], running: true })
|
|
|
|
|
|
|
+ }, { testInbox: { 'next-turn': [], 'next-step': [pending] }, running: true })
|
|
|
const view = render(<h.ChatView {...h.props} />)
|
|
const view = render(<h.ChatView {...h.props} />)
|
|
|
|
|
|
|
|
expect(view.getAllByText('same steering')).toHaveLength(2)
|
|
expect(view.getAllByText('same steering')).toHaveLength(2)
|
|
@@ -1094,15 +1102,12 @@ describe('ChatView', () => {
|
|
|
|
|
|
|
|
act(() => {
|
|
act(() => {
|
|
|
h.setSession({
|
|
h.setSession({
|
|
|
- queue: [{
|
|
|
|
|
|
|
+ testInbox: { 'next-turn': [], 'next-step': [{
|
|
|
id: 'steer-occurrence' as never,
|
|
id: 'steer-occurrence' as never,
|
|
|
- messageId: 'steer-message' as never,
|
|
|
|
|
- placement: 'steering',
|
|
|
|
|
- rpcId: 'req-steer' as never,
|
|
|
|
|
content: [{ type: 'text', text: '带图纠偏' }],
|
|
content: [{ type: 'text', text: '带图纠偏' }],
|
|
|
- preview: '带图纠偏',
|
|
|
|
|
- text: '带图纠偏',
|
|
|
|
|
- }],
|
|
|
|
|
|
|
+
|
|
|
|
|
+ role: 'user', source: { kind: 'user', rpcId: 'req-steer' as never },
|
|
|
|
|
+ }] },
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
expect(view.getAllByText('带图纠偏')).toHaveLength(1)
|
|
expect(view.getAllByText('带图纠偏')).toHaveLength(1)
|
|
@@ -1127,15 +1132,12 @@ describe('ChatView', () => {
|
|
|
expect(view.queryByText('排队中')).toBeNull()
|
|
expect(view.queryByText('排队中')).toBeNull()
|
|
|
act(() => {
|
|
act(() => {
|
|
|
h.setSession({
|
|
h.setSession({
|
|
|
- queue: [{
|
|
|
|
|
|
|
+ testInbox: { 'next-step': [], 'next-turn': [{
|
|
|
id: 'q-occurrence' as never,
|
|
id: 'q-occurrence' as never,
|
|
|
- messageId: 'q-message' as never,
|
|
|
|
|
- placement: 'queued' as const,
|
|
|
|
|
- rpcId: 'req-q' as never,
|
|
|
|
|
content: [{ type: 'text' as const, text: '排队中' }],
|
|
content: [{ type: 'text' as const, text: '排队中' }],
|
|
|
- preview: '排队中',
|
|
|
|
|
- text: '排队中',
|
|
|
|
|
- }],
|
|
|
|
|
|
|
+
|
|
|
|
|
+ role: 'user', source: { kind: 'user', rpcId: 'req-q' as never },
|
|
|
|
|
+ }] },
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
// The queued occurrence and its local predecessor both belong to the
|
|
// The queued occurrence and its local predecessor both belong to the
|
|
@@ -2152,14 +2154,12 @@ describe('ChatView', () => {
|
|
|
expect(status.textContent).toMatch(/^深度求索中\.\.\.2分0\d秒$/)
|
|
expect(status.textContent).toMatch(/^深度求索中\.\.\.2分0\d秒$/)
|
|
|
expect(status.querySelector('[aria-hidden="true"]')).not.toBeNull()
|
|
expect(status.querySelector('[aria-hidden="true"]')).not.toBeNull()
|
|
|
act(() => {
|
|
act(() => {
|
|
|
- h.setSession({ queue: [{
|
|
|
|
|
|
|
+ h.setSession({ testInbox: { 'next-turn': [], 'next-step': [{
|
|
|
id: 'steering-occurrence' as never,
|
|
id: 'steering-occurrence' as never,
|
|
|
- messageId: 'steering-message' as never,
|
|
|
|
|
- placement: 'steering',
|
|
|
|
|
content: [{ type: 'text', text: 'also' }],
|
|
content: [{ type: 'text', text: 'also' }],
|
|
|
- preview: 'also',
|
|
|
|
|
- text: 'also',
|
|
|
|
|
- }] })
|
|
|
|
|
|
|
+
|
|
|
|
|
+ role: 'user', source: { kind: 'user' },
|
|
|
|
|
+ }] } })
|
|
|
})
|
|
})
|
|
|
expect(status.textContent).toMatch(/^深度求索中\.\.\.2分0\d秒$/)
|
|
expect(status.textContent).toMatch(/^深度求索中\.\.\.2分0\d秒$/)
|
|
|
})
|
|
})
|