Files
deepseek-harness/packages/todo/tool-todo/tests/projection.spec.ts
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

125 lines
5.2 KiB
TypeScript

/**
* The `todos` projection provider (session-projection RFC knife 4 — the "a
* fourth domain is just its own registrations" acceptance probe): mounting
* tool-todo beside the registry serves the whole current list on the history
* tail page with a consistent asOfSeq (= last event seq); before any write the value is null; a
* composition without tool-todo has no `todos` key; unmounting tool-todo
* removes it (HMR safety). The carrier and framework are exercised unmodified.
*/
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session, TodoItem } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import type { RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
import { createApiProxy } from '@deepseek-ai/dsh-host-apiproxy'
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
let nextRpc = 1
function request<P>(payload: P): RpcRequest<P> {
return { rpcId: RpcId(`todo-proj-${String(nextRpc++)}`), payload }
}
interface Bench {
ctx: Context
session: Session
tailProjections(): Promise<{ asOfSeq: number; values: Record<string, unknown> } | undefined>
}
async function harness(withTodoTool: boolean): Promise<Bench> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(UserInteractionService)
await ctx.plugin(AgentRegistry)
await ctx.plugin(SessionProjectionRegistry)
if (withTodoTool) await ctx.plugin(ToolTodo, { allowParallelInProgress: true })
const session = ctx.sessions.create()
ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
const api = createApiProxy(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
return {
ctx,
session,
async tailProjections() {
const response = await api.sessions.history(request({ sessionId: session.id }))
if (!response.result.ok) throw new Error('history failed')
return response.result.value.projections
},
}
}
/** One paginable message so the tail page is non-degenerate. */
function seedMessage(session: Session): void {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'hi' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
}
describe('todos projection provider', () => {
it('serves null before the first todo/write', async () => {
const bench = await harness(true)
seedMessage(bench.session)
const projections = await bench.tailProjections()
expect(projections?.values).toEqual({ todos: null })
expect(projections?.asOfSeq).toBe(bench.session.seq - 1)
})
it('serves the latest whole list after writes, asOfSeq = last event seq', async () => {
const bench = await harness(true)
const session = bench.session
seedMessage(session)
const first: TodoItem[] = [{ content: 'a', status: 'pending' }]
const second: TodoItem[] = [
{ content: 'a', status: 'completed' },
{ content: 'b', status: 'in_progress' },
]
session.append('todo/write', { todos: first })
session.append('todo/write', { todos: second })
const projections = await bench.tailProjections()
// Last-wins: the latest snapshot, whole.
expect(projections?.values.todos).toEqual(second)
expect(projections?.asOfSeq).toBe(session.seq - 1)
})
it('clears the standing plan on the next turn/start (turn/end keeps it)', async () => {
const bench = await harness(true)
const session = bench.session
seedMessage(session)
const list: TodoItem[] = [{ content: 'done', status: 'completed' }]
session.append('todo/write', { todos: list })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
expect((await bench.tailProjections())?.values.todos).toEqual(list)
session.append('turn/start', { turn: 1 })
const cleared = await bench.tailProjections()
expect(cleared?.values.todos).toBeNull()
expect(cleared?.asOfSeq).toBe(session.seq - 1)
})
it('has no todos key when tool-todo is not composed', async () => {
const bench = await harness(false)
seedMessage(bench.session)
const projections = await bench.tailProjections()
expect(projections).toBeDefined()
expect('todos' in (projections?.values ?? {})).toBe(false)
})
it('drops the key when the tool-todo fiber unloads (HMR safety)', async () => {
const bench = await harness(false)
seedMessage(bench.session)
const fiber = await bench.ctx.plugin(ToolTodo, { allowParallelInProgress: true })
expect((await bench.tailProjections())?.values).toEqual({ todos: null })
await fiber.dispose()
expect('todos' in ((await bench.tailProjections())?.values ?? {})).toBe(false)
})
})