| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- import { describe, expect, it } from 'vitest'
- import type { HistoryEntry } from '@deepseek-ai/dsh-client-connection/client'
- import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
- import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
- import { inspectRequests } from '../src/client/sessions/request-inspection.ts'
- const at = (seq: number, type: string, data: unknown): SessionEvent =>
- ({ seq, time: 1_700_000_000_000 + seq, type, data }) as SessionEvent
- const entriesOf = (events: readonly SessionEvent[]): HistoryEntry[] =>
- events.map(event => ({ event }))
- describe('inspectRequests', () => {
- it('projects ordinary and compaction calls into one chronological request stream', () => {
- const events = [
- at(0, 'step/start', { turn: 1, step: 1 }),
- at(1, 'request/header', {
- reason: 'initial',
- header: {
- config: { provider: 'fake', model: 'model' },
- system: 'system',
- tools: [{
- name: 'read',
- description: 'Read a file.',
- parameters: { type: 'object' },
- }],
- },
- }),
- at(2, 'tool/call', {
- turn: 1,
- step: 1,
- callId: 'call-1',
- name: 'read',
- arguments: '{}',
- }),
- at(3, 'assistant/message', {
- turn: 1,
- step: 1,
- message: createAssistantMessage({
- content: [{ type: 'text', text: 'done' }],
- source: { provider: 'fake', model: 'model' },
- }),
- usage: { inputTokens: 5, outputTokens: 2 },
- }),
- at(4, 'step/end', { turn: 1, step: 1 }),
- at(5, 'compact/start', { turn: 1 }),
- at(6, 'compact/summary', {
- summary: [{ type: 'text', text: 'summary' }],
- rawOutput: [
- { type: 'reasoning', text: 'thought' },
- { type: 'text', text: 'summary' },
- ],
- provider: 'fake',
- model: 'compact-model',
- usage: { inputTokens: 8, outputTokens: 3 },
- }),
- at(7, 'user/message', createUserMessage({
- content: [{ type: 'text', text: 'checkpoint' }],
- source: { kind: 'plugin', plugin: 'compact' },
- })),
- at(8, 'compact/end', { turn: 1 }),
- ]
- const snapshot = inspectRequests(entriesOf(events))
- expect(snapshot.requests).toMatchObject([
- {
- purpose: 'assistant',
- startSeq: 0,
- resultSeq: 3,
- status: 'complete',
- prompt: {
- config: { provider: 'fake', model: 'model' },
- system: 'system',
- },
- promptChange: { seq: 1, kind: 'initial' },
- },
- {
- purpose: 'compaction',
- startSeq: 5,
- resultSeq: 6,
- replacementSeq: 7,
- status: 'complete',
- summary: [{ type: 'text', text: 'summary' }],
- },
- ])
- expect(snapshot.callSchemas.get('call-1')?.name).toBe('read')
- })
- it('does not promote a truncated resume or change header to the initial prompt', () => {
- for (const reason of ['resume', 'change'] as const) {
- const snapshot = inspectRequests(entriesOf([
- at(10, 'step/start', { turn: 3, step: 1 }),
- at(11, 'request/header', {
- reason,
- header: {
- config: { provider: 'fake', model: 'model' },
- system: 'tail-window prompt',
- },
- }),
- ]))
- expect(snapshot.requests[0]).toMatchObject({
- purpose: 'assistant',
- prompt: { system: 'tail-window prompt' },
- })
- expect(snapshot.requests[0]).not.toHaveProperty('promptChange')
- }
- })
- it('classifies a prompt change once the preceding header is loaded', () => {
- const snapshot = inspectRequests(entriesOf([
- at(0, 'step/start', { turn: 1, step: 1 }),
- at(1, 'request/header', {
- reason: 'initial',
- header: {
- config: { provider: 'fake', model: 'model' },
- system: 'before',
- },
- }),
- at(2, 'step/start', { turn: 1, step: 2 }),
- at(3, 'request/header', {
- reason: 'change',
- header: {
- config: { provider: 'fake', model: 'model' },
- system: 'after',
- },
- }),
- ]))
- expect(snapshot.requests[1]).toMatchObject({
- promptChange: {
- seq: 3,
- kind: 'system',
- previous: { system: 'before' },
- },
- })
- })
- it('preserves a standalone compaction owner without widening assistant turns', () => {
- const snapshot = inspectRequests(entriesOf([
- at(0, 'compact/start', { turn: null }),
- at(1, 'compact/summary', {
- summary: [{ type: 'text', text: 'standalone summary' }],
- provider: 'fake',
- model: 'compact-model',
- }),
- at(2, 'compact/end', { turn: null }),
- at(3, 'step/start', { turn: 2, step: 1 }),
- ]))
- const [compaction, assistant] = snapshot.requests
- expect(compaction).toMatchObject({
- purpose: 'compaction',
- turn: null,
- step: 0,
- status: 'complete',
- })
- expect(assistant).toMatchObject({
- purpose: 'assistant',
- turn: 2,
- step: 1,
- status: 'running',
- })
- if (assistant?.purpose === 'assistant') {
- const turn: number = assistant.turn
- expect(turn).toBe(2)
- }
- })
- it('interrupts an orphaned compaction at end-seed before projecting a new attempt', () => {
- const snapshot = inspectRequests(entriesOf([
- at(0, 'compact/start', { turn: null }),
- at(1, 'session/end-seed', {}),
- at(2, 'compact/start', { turn: null }),
- at(3, 'compact/summary', {
- summary: [{ type: 'text', text: 'replacement summary' }],
- provider: 'fake',
- model: 'compact-model',
- }),
- at(4, 'compact/end', { turn: null }),
- ]))
- expect(snapshot.requests).toMatchObject([
- {
- purpose: 'compaction',
- startSeq: 0,
- status: 'error',
- completedAt: 1_700_000_000_001,
- error: 'Compaction was interrupted before completion.',
- },
- {
- purpose: 'compaction',
- startSeq: 2,
- status: 'complete',
- completedAt: 1_700_000_000_004,
- summary: [{ type: 'text', text: 'replacement summary' }],
- },
- ])
- })
- it('captures schemas for nested tool dispatches from the active request header', () => {
- const snapshot = inspectRequests(entriesOf([
- at(0, 'request/header', {
- reason: 'initial',
- header: {
- config: { provider: 'fake', model: 'model' },
- tools: [{
- name: 'read',
- description: 'Read a file.',
- parameters: { type: 'object' },
- }],
- },
- }),
- at(1, 'tool/code-dispatch-start', {
- parentCallId: 'parent',
- subCallId: 'nested',
- name: 'read',
- arguments: {},
- }),
- ]))
- expect(snapshot.callSchemas.get('nested')?.name).toBe('read')
- })
- it('keeps chunk-reported usage through request failure and prefers it to message fallback', () => {
- const chunkUsage = { inputTokens: 21, outputTokens: 3 }
- const retryUsage = {
- inputTokens: 5,
- outputTokens: 2,
- cacheReadTokens: 8,
- reasoningTokens: 1,
- }
- const snapshot = inspectRequests(entriesOf([
- at(0, 'step/start', { turn: 1, step: 1 }),
- at(1, 'assistant/chunk', {
- turn: 1,
- step: 1,
- chunk: { type: 'usage', usage: chunkUsage },
- }),
- at(2, 'llm/retry', {
- turn: 1,
- step: 1,
- retry: 1,
- maxRetries: 2,
- delayMs: 100,
- failure: { message: 'rate limited' },
- }),
- at(3, 'assistant/chunk', {
- turn: 1,
- step: 1,
- chunk: { type: 'usage', usage: retryUsage },
- }),
- at(4, 'assistant/message', {
- turn: 1,
- step: 1,
- message: createAssistantMessage({
- content: [{ type: 'text', text: 'recovered' }],
- source: { provider: 'fake', model: 'model' },
- }),
- usage: { inputTokens: 1, outputTokens: 1 },
- }),
- ]))
- expect(snapshot.requests[0]).toMatchObject({
- status: 'complete',
- usage: {
- inputTokens: 26,
- outputTokens: 5,
- cacheReadTokens: 8,
- reasoningTokens: 1,
- },
- })
- })
- it('keeps provider credential fragments out of projected request errors', () => {
- const snapshot = inspectRequests(entriesOf([
- at(0, 'step/start', { turn: 1, step: 1 }),
- at(1, 'turn/end', {
- turn: 1, reason: { kind: 'error', error: {
- code: 'AUTH',
- message: 'Authentication Fails, Your api key: sk-preview-secret is invalid',
- },
- },
- }),
- at(2, 'step/start', { turn: 2, step: 1 }),
- at(3, 'turn/end', {
- turn: 2, reason: { kind: 'error', error: { message: 'plugin exploded', code: 'UNKNOWN' } },
- }),
- ]))
- expect(snapshot.requests).toMatchObject([
- { status: 'error', error: 'API key is invalid' },
- { status: 'error', error: 'plugin exploded' },
- ])
- })
- it('treats a scrubbed durable-fixture tool catalog as unavailable', () => {
- const snapshot = inspectRequests(entriesOf([
- at(0, 'step/start', { turn: 1, step: 1 }),
- at(1, 'request/header', {
- reason: 'initial',
- header: {
- config: { provider: 'fake', model: 'model' },
- tools: '{{tools}}',
- },
- }),
- at(2, 'tool/call', {
- turn: 1,
- step: 1,
- callId: 'call-1',
- name: 'read',
- arguments: '{}',
- }),
- ]))
- expect(snapshot.callSchemas).toEqual(new Map())
- const [request] = snapshot.requests
- expect(request?.purpose === 'assistant' ? request.prompt?.tools : undefined).toEqual([])
- })
- })
|