| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- /**
- * Queue snapshot semantics: authoritative replacement after every host-side
- * change, reconnect re-baselining, pre-instantiation buffering, editable-text
- * projection, and snapshot reference stability.
- */
- import { describe, expect, it } from 'vitest'
- import { createUserMessage } from '@deepseek-ai/dsh-llm'
- import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
- import type {
- InboxItemId, MuxFrame, RpcId, SessionId,
- } from '@deepseek-ai/dsh-client-connection/client'
- import { Session } from '../src/client/sessions/session.ts'
- import { SessionManager } from '../src/client/sessions/manager.ts'
- import { FakeApiClient } from './fake-api.ts'
- const SID = 'fk-q1' as SessionId
- const text = (value: string): ContentBlock[] => [{ type: 'text', text: value }]
- const rid = (id: string): RpcId => id as RpcId
- const iid = (id: string): InboxItemId => id as InboxItemId
- interface QueueFixture {
- id: string
- body: string
- content?: ContentBlock[]
- }
- /** Build one authoritative queue snapshot. */
- function queueFrame(items: QueueFixture[]): MuxFrame {
- return {
- type: 'session/queue',
- sessionId: SID,
- items: items.map(item => ({
- id: iid(item.id),
- message: createUserMessage({
- content: item.content ?? text(item.body),
- source: { kind: 'user', rpcId: rid(`rpc-${item.id}`) } as never,
- }),
- })),
- }
- }
- function makeSession(): Session {
- return new Session(SID, new FakeApiClient())
- }
- describe('queue snapshot intake', () => {
- it('projects stable ids, flat previews, and complete text', () => {
- const session = makeSession()
- session.handleMuxEnvelope(rid('env-1'), queueFrame([
- { id: 'q-1', body: '第一条 排队\n消息' },
- ]))
- expect(session.getSnapshot().queue).toEqual([
- { id: 'q-1', preview: '第一条 排队 消息', text: '第一条 排队\n消息' },
- ])
- })
- it('marks mixed-content messages non-editable while retaining their preview', () => {
- const session = makeSession()
- session.handleMuxEnvelope(rid('env-2'), queueFrame([{
- id: 'q-image',
- body: '',
- content: [{ type: 'text', text: 'hi' }, { type: 'image', data: 'x' } as never],
- }]))
- expect(session.getSnapshot().queue).toEqual([
- { id: 'q-image', preview: 'hi [image]', text: null },
- ])
- })
- it('caps previews at 200 code points and preserves the full editable text', () => {
- const session = makeSession()
- const body = '长'.repeat(201)
- session.handleMuxEnvelope(rid('env-3'), queueFrame([{ id: 'q-cap', body }]))
- const row = session.getSnapshot().queue[0]
- expect(Array.from(row?.preview ?? '')).toHaveLength(201)
- expect(row?.preview.endsWith('…')).toBe(true)
- expect(row?.text).toBe(body)
- })
- it('replaces content, order, and membership from each authoritative frame', () => {
- const session = makeSession()
- session.handleMuxEnvelope(rid('env-4'), queueFrame([
- { id: 'q-1', body: 'one' },
- { id: 'q-2', body: 'two' },
- ]))
- session.handleMuxEnvelope(rid('env-5'), queueFrame([
- { id: 'q-2', body: 'two edited' },
- ]))
- expect(session.getSnapshot().queue).toEqual([
- { id: 'q-2', preview: 'two edited', text: 'two edited' },
- ])
- session.handleMuxEnvelope(rid('env-6'), queueFrame([]))
- expect(session.getSnapshot().queue).toEqual([])
- })
- it('keeps the queue array reference stable across unrelated snapshot swaps', () => {
- const session = makeSession()
- session.handleMuxEnvelope(rid('env-7'), queueFrame([{ id: 'q-stable', body: '稳定' }]))
- const before = session.getSnapshot().queue
- session.handleAgentError('unrelated')
- expect(session.getSnapshot().queue).toBe(before)
- })
- })
- describe('queue operation transport', () => {
- it('addresses the session.updateQueue RPC without optimistic local mutation', async () => {
- const api = new FakeApiClient()
- const session = new Session(SID, api)
- session.handleMuxEnvelope(rid('env-op'), queueFrame([{ id: 'q-op', body: 'pending' }]))
- const before = session.getSnapshot().queue
- await expect(session.updateQueue(iid('q-op'), { kind: 'edit', content: text('next') }))
- .resolves.toEqual({ ok: true, value: { accepted: true } })
- expect(api.callsOf('session.updateQueue')).toEqual([{
- sessionId: SID,
- itemId: 'q-op',
- action: { kind: 'edit', content: text('next') },
- }])
- expect(session.getSnapshot().queue).toBe(before)
- })
- })
- describe('queue reconnect semantics', () => {
- it('session/subscribed clears stale state before the fresh snapshot lands', () => {
- const session = makeSession()
- session.handleMuxEnvelope(rid('e1'), queueFrame([{ id: 'q-old', body: '旧连接' }]))
- session.handleMuxEnvelope(rid('e2'), { type: 'session/subscribed', sessionId: SID, lastSeq: 10 })
- expect(session.getSnapshot().queue).toEqual([])
- session.handleMuxEnvelope(rid('e3'), queueFrame([{ id: 'q-new', body: '新基线' }]))
- expect(session.getSnapshot().queue.map(row => row.id)).toEqual(['q-new'])
- })
- it('resync does not clear a baseline that raced ahead of the host connection signal', async () => {
- const session = makeSession()
- session.handleMuxEnvelope(rid('e1'), { type: 'session/subscribed', sessionId: SID, lastSeq: 5 })
- session.handleMuxEnvelope(rid('e2'), queueFrame([{ id: 'q-fresh', body: '新基线' }]))
- await session.resync()
- expect(session.getSnapshot().queue.map(row => row.id)).toEqual(['q-fresh'])
- })
- it('running-status changes never guess at queue retirement', () => {
- const session = makeSession()
- session.handleMuxEnvelope(rid('e1'), queueFrame([{ id: 'q-live', body: '保留' }]))
- session.handleRunning(true)
- session.handleRunning(false)
- expect(session.getSnapshot().queue.map(row => row.id)).toEqual(['q-live'])
- })
- })
- describe('manager buffering of queue snapshots', () => {
- it('replays only the latest snapshot for an uninstantiated session', () => {
- const manager = new SessionManager(new FakeApiClient())
- manager.handleMuxEnvelope({ rpcId: rid('b1'), payload: queueFrame([{ id: 'q-old', body: '旧' }]) })
- manager.handleMuxEnvelope({ rpcId: rid('b2'), payload: queueFrame([{ id: 'q-new', body: '新' }]) })
- expect(manager.get(SID).getSnapshot().queue.map(row => row.id)).toEqual(['q-new'])
- })
- it('subscribed drops the prior-generation snapshot while preserving answerable frames', () => {
- const manager = new SessionManager(new FakeApiClient())
- manager.handleMuxEnvelope({ rpcId: rid('g1a'), payload: queueFrame([{ id: 'q-g1', body: '第一代' }]) })
- manager.handleMuxEnvelope({
- rpcId: rid('g1b'),
- payload: { type: 'approval/requested', sessionId: SID, approvalId: 'ap-1' as never, toolName: 'bash' },
- })
- manager.handleMuxEnvelope({
- rpcId: rid('g2a'),
- payload: { type: 'session/subscribed', sessionId: SID, lastSeq: 3 },
- })
- manager.handleMuxEnvelope({ rpcId: rid('g2b'), payload: queueFrame([{ id: 'q-g2', body: '第二代' }]) })
- const snapshot = manager.get(SID).getSnapshot()
- expect(snapshot.queue.map(row => row.id)).toEqual(['q-g2'])
- expect(snapshot.pending.map(pending => pending.kind)).toEqual(['approval'])
- })
- })
|