Files
deepseek-harness/packages/api/session-controller/tests/summary-list.host.spec.ts
T

146 lines
6.8 KiB
TypeScript

/**
* Session-list summary payloads: a `session.list` row carries every registered
* wire projection except the units that declare `summary: false`
* (`turnOutline`, `sessionStats`, `todos`, `imageLimits`), while an opened Session's
* `session/follow` opening snapshot keeps serving them.
*/
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionFollowFrame } from '@deepseek-ai/dsh-api-session-controller/types'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import {
apply as sessionStatsApply, inject as sessionStatsInject, name as sessionStatsName,
} from '@deepseek-ai/dsh-session-stats'
import {
apply as turnOutlineApply, inject as turnOutlineInject, name as turnOutlineName,
} from '@deepseek-ai/dsh-session-turn-outline'
import {
apply as toolTodoApply, Config as toolTodoConfig, inject as toolTodoInject, name as toolTodoName,
} from '@deepseek-ai/dsh-tool-todo'
import { createSessionTestRemote } from './test-remote.ts'
const SUMMARY_EXCLUDED_KEYS = ['turnOutline', 'sessionStats', 'todos', 'imageLimits'] as const
const ownedContexts = new Set<Context>()
afterEach(async () => {
await Promise.all([...ownedContexts].map(ctx => ctx.fiber.dispose()))
ownedContexts.clear()
})
const remote = (ctx: Context) => createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
cwd: '/tmp',
})
/** Read and close one snapshot-first follow generation. */
async function opening(
ctx: Context,
sessionId: SessionId,
): Promise<Extract<SessionFollowFrame, { type: 'snapshot' }>> {
const abort = new AbortController()
const iterator = (await remote(ctx)).follow({ address: { kind: 'session', sessionId } }, abort.signal)[Symbol.asyncIterator]()
const first = await iterator.next()
abort.abort()
await iterator.return?.()
if (first.done || first.value.type !== 'snapshot') throw new Error('follow did not open with a snapshot')
return first.value
}
/**
* Mount the registry plus the four summary-excluded units and one ordinary
* wire unit. The tool plugin needs a tool registry; the list path never
* executes a tool, so a registration sink is enough.
*/
async function harness(): Promise<{ ctx: Context; sessionContext: Context }> {
const ctx = new Context()
ownedContexts.add(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(SessionProjectionRegistry)
ctx.provide('tools', { register: () => () => {} } as never)
await ctx.sessionProjections.register(agentPresetProjectionDefinition)
await ctx.plugin({ name: turnOutlineName, inject: turnOutlineInject, apply: turnOutlineApply })
await ctx.plugin({ name: sessionStatsName, inject: sessionStatsInject, apply: sessionStatsApply })
await ctx.plugin({
name: toolTodoName,
inject: toolTodoInject,
Config: toolTodoConfig,
apply: toolTodoApply,
}, { allowParallelInProgress: false })
// Build the remote (and with it the list owner that registers
// `sessionListMetadata`) before any Session exists, so every registered unit
// seeds a cell at `session/created`.
await remote(ctx)
return { ctx, sessionContext: ctx }
}
/** One Session with a closed turn, a tool-free todo write, and a preset. */
function seedSession(ctx: Context): { id: SessionId } {
const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace', agentPreset: 'standard' } })
session.append('turn/start', { turn: 1 })
session.append('todo/write', { todos: [{ content: 'ship it', status: 'in_progress' }] })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
return { id: session.id }
}
describe('session.list summary payload', () => {
it('carries every registered wire key except the summary-excluded units', async () => {
const { ctx } = await harness()
const { id } = seedSession(ctx)
const session = ctx.sessions.get(id)
if (session === undefined) throw new Error('session was not created')
const everyWireKey = Object.keys(ctx.sessionProjections.snapshot(session).values)
// The fixture must actually exercise the excluded units.
for (const key of SUMMARY_EXCLUDED_KEYS) expect(everyWireKey).toContain(key)
expect([...ctx.sessionProjections.summaryKeys()]).not.toContain('turnOutline')
const response = await (await remote(ctx)).list({})
if (!response.ok) throw new Error('unreachable')
const row = response.value.items.find(item => item.sessionId === id)
const served = Object.keys(row?.projections?.values ?? {})
expect(served.sort()).toEqual(everyWireKey.filter(key => !SUMMARY_EXCLUDED_KEYS.includes(
key as (typeof SUMMARY_EXCLUDED_KEYS)[number],
)).sort())
for (const key of SUMMARY_EXCLUDED_KEYS) expect(served).not.toContain(key)
// An ordinary wire unit is still there with its value.
expect(row?.projections?.values.sessionListMetadata).toMatchObject({ blank: false })
})
it('keeps the excluded units on an opened Session\'s follow snapshot', async () => {
const { ctx } = await harness()
const { id } = seedSession(ctx)
const session = ctx.sessions.get(id)
if (session === undefined) throw new Error('session was not created')
const everyValue = ctx.sessionProjections.snapshot(session).values
const snapshot = await opening(ctx, id)
for (const key of SUMMARY_EXCLUDED_KEYS) {
expect(snapshot.projections.values).toHaveProperty(key)
expect(snapshot.projections.values[key as keyof typeof snapshot.projections.values])
.toEqual(everyValue[key as keyof typeof everyValue])
}
expect(snapshot.projections.values.todos).toEqual([{ content: 'ship it', status: 'in_progress' }])
expect(snapshot.projections.values.imageLimits).toEqual(ctx.attachments.imageLimits)
})
it('drops the excluded units from the list while a Session is still attached', async () => {
const { ctx } = await harness()
const { id } = seedSession(ctx)
// Two consecutive pulls: a refresh must not reintroduce a dropped unit.
for (const attempt of [0, 1]) {
const response = await (await remote(ctx)).list({})
if (!response.ok) throw new Error('unreachable')
const row = response.value.items.find(item => item.sessionId === id)
expect(row?.projections?.values, `attempt ${String(attempt)}`).not.toHaveProperty('turnOutline')
expect(row?.projections?.values, `attempt ${String(attempt)}`).not.toHaveProperty('sessionStats')
expect(row?.projections?.values, `attempt ${String(attempt)}`).not.toHaveProperty('todos')
expect(row?.projections?.values, `attempt ${String(attempt)}`).not.toHaveProperty('imageLimits')
}
})
})