`CommandService.list` and `execute` carry the wire contract directly through `@Remote`, and the Client assembly mounts the generated commands contribution. The legacy API Proxy route, its schemas, the map rows, the generated client methods and the fixture's command domain are removed, so the catalog and the admission call have one owner again. `Session.command()` keeps a result-shaped public face for parity with the prompt, cancel and attachment neighbours it sits beside, and reads the generated namespace through one `SessionRemotes` parameter. The Session cluster declares that face against the owning business package rather than the generated contribution: the Host compiler aggregate builds this package, and it runs before any contribution is emitted. Migrated calls lose the `title-invalid` class of protocol-only error codes and report `internal`; no production caller branched on them.
98 lines
4.8 KiB
TypeScript
98 lines
4.8 KiB
TypeScript
/**
|
|
* PartialAccumulator: six-variant chunk folding, sparse-index compaction, and
|
|
* the block/snapshot reference discipline (a delta swaps only that block).
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { StreamChunk } from '@deepseek-ai/dsh-api-remotes/client'
|
|
import { PartialAccumulator } from '../src/client/sessions/partial.ts'
|
|
|
|
const chunk = (c: Record<string, unknown>): StreamChunk => c as unknown as StreamChunk
|
|
|
|
describe('PartialAccumulator', () => {
|
|
it('builds empty blocks per block-start type, unknown type falls to other', () => {
|
|
const acc = new PartialAccumulator(1, 0)
|
|
acc.push(chunk({ type: 'block-start', index: 0, blockType: 'text' }))
|
|
acc.push(chunk({ type: 'block-start', index: 1, blockType: 'reasoning' }))
|
|
acc.push(chunk({ type: 'block-start', index: 2, blockType: 'tool-call' }))
|
|
acc.push(chunk({ type: 'block-start', index: 3, blockType: 'no-such' }))
|
|
expect(acc.toPartial().blocks).toEqual([
|
|
{ kind: 'text', text: '' },
|
|
{ kind: 'reasoning', text: '' },
|
|
{ kind: 'tool-call', callId: '', name: '', argsRaw: '' },
|
|
{ kind: 'other', block: null },
|
|
])
|
|
})
|
|
|
|
it('accumulates text deltas, starting from empty when prev is missing or another kind', () => {
|
|
const acc = new PartialAccumulator(1, 0)
|
|
acc.push(chunk({ type: 'text-delta', index: 0, text: '无 start ' })) // prev missing
|
|
acc.push(chunk({ type: 'text-delta', index: 0, text: '也累积' }))
|
|
expect(acc.toPartial().blocks).toEqual([{ kind: 'text', text: '无 start 也累积' }])
|
|
acc.push(chunk({ type: 'reasoning-delta', index: 0, text: '换型重起' })) // prev is text → restart
|
|
expect(acc.toPartial().blocks).toEqual([{ kind: 'reasoning', text: '换型重起' }])
|
|
})
|
|
|
|
it('accumulates reasoning deltas on the reasoning lane', () => {
|
|
const acc = new PartialAccumulator(1, 0)
|
|
acc.push(chunk({ type: 'block-start', index: 0, blockType: 'reasoning' }))
|
|
acc.push(chunk({ type: 'reasoning-delta', index: 0, text: '思' }))
|
|
acc.push(chunk({ type: 'reasoning-delta', index: 0, text: '考' }))
|
|
expect(acc.toPartial().blocks).toEqual([{ kind: 'reasoning', text: '思考' }])
|
|
})
|
|
|
|
it('continues from a materialized history prefix', () => {
|
|
const acc = new PartialAccumulator(1, 0, [{ kind: 'text', text: '已有' }])
|
|
acc.push(chunk({ type: 'text-delta', index: 0, text: '增量' }))
|
|
expect(acc.toPartial().blocks).toEqual([{ kind: 'text', text: '已有增量' }])
|
|
})
|
|
|
|
it('folds tool-call deltas: first id pins callId, late name overrides, argsRaw concatenates', () => {
|
|
const acc = new PartialAccumulator(1, 0)
|
|
acc.push(chunk({ type: 'tool-call-delta', index: 0, id: 'c1', argumentsDelta: '{"a"' }))
|
|
acc.push(chunk({ type: 'tool-call-delta', index: 0, id: 'c2-late', name: 'echo', argumentsDelta: ':1}' }))
|
|
expect(acc.toPartial().blocks).toEqual([
|
|
{ kind: 'tool-call', callId: 'c1', name: 'echo', argsRaw: '{"a":1}' },
|
|
])
|
|
})
|
|
|
|
it('replaces the accumulated block wholesale on block-end', () => {
|
|
const acc = new PartialAccumulator(1, 0)
|
|
acc.push(chunk({ type: 'text-delta', index: 0, text: '中间态' }))
|
|
acc.push(chunk({ type: 'block-end', index: 0, block: { type: 'text', text: '定稿全文' } }))
|
|
expect(acc.toPartial().blocks).toEqual([{ kind: 'text', text: '定稿全文' }])
|
|
})
|
|
|
|
it('returns false (no notification) for usage/finish/unknown variants and keeps blocks', () => {
|
|
const acc = new PartialAccumulator(1, 0)
|
|
acc.push(chunk({ type: 'text-delta', index: 0, text: 'x' }))
|
|
const before = acc.toPartial()
|
|
expect(acc.push(chunk({ type: 'usage', usage: {} }))).toBe(false)
|
|
expect(acc.push(chunk({ type: 'finish', reason: 'stop' }))).toBe(false)
|
|
expect(acc.push(chunk({ type: 'future-variant' }))).toBe(false)
|
|
expect(acc.toPartial()).toBe(before) // unchanged: same snapshot reference
|
|
})
|
|
|
|
it('compacts sparse indexes into a dense render-order array', () => {
|
|
const acc = new PartialAccumulator(1, 0)
|
|
acc.push(chunk({ type: 'block-start', index: 2, blockType: 'text' }))
|
|
acc.push(chunk({ type: 'text-delta', index: 2, text: '先到的高位' }))
|
|
acc.push(chunk({ type: 'block-start', index: 0, blockType: 'reasoning' }))
|
|
const { blocks } = acc.toPartial()
|
|
expect(blocks).toHaveLength(2) // no undefined holes
|
|
expect(blocks[0]).toEqual({ kind: 'reasoning', text: '' })
|
|
expect(blocks[1]).toEqual({ kind: 'text', text: '先到的高位' })
|
|
})
|
|
|
|
it('keeps the snapshot reference stable without changes and swaps it once per mutation', () => {
|
|
const acc = new PartialAccumulator(3, 1)
|
|
const first = acc.toPartial()
|
|
expect(first).toMatchObject({ turn: 3, step: 1, blocks: [] })
|
|
expect(acc.toPartial()).toBe(first)
|
|
acc.push(chunk({ type: 'text-delta', index: 0, text: 'a' }))
|
|
const second = acc.toPartial()
|
|
expect(second).not.toBe(first)
|
|
expect(acc.toPartial()).toBe(second)
|
|
})
|
|
})
|