Merge remote-tracking branch 'origin/master' into feat/todo-multi-in-progress
# Conflicts: # packages/todo/tool-todo/README.i18n.yaml
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
// Test-local programmable IApiClient fake (NOT the fixture: fixture is a demo
|
||||
// data source on a real clock; behavior tests need per-case responses and
|
||||
// deferred-controlled timing). Streams are hand pumps: pushMux/pushHost.
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import type {
|
||||
CommandDescriptor, CommandExecuteResult, HostFrame, IApiClient, ModelTarget, MuxFrame,
|
||||
CommandDescriptor, HostFrame, IApiClient, ModelTarget, MuxFrame,
|
||||
RpcRequest, RpcResponse, SessionId, SessionModels, SkillEntry,
|
||||
} from '../src/client/api.ts'
|
||||
import { RpcId } from '../src/client/api.ts'
|
||||
@@ -110,10 +111,12 @@ export class FakeApiClient implements IApiClient {
|
||||
|
||||
// Payloads stay `unknown` (lint-lane note above); response rows are the real
|
||||
// wire shapes so cases can program catalogs and skill lists without casts.
|
||||
onCommandList: (payload: unknown) => Promise<RpcResponse<{ commands: CommandDescriptor[] }>> = () => Promise.resolve(ok({ commands: [] }))
|
||||
onCommandExecute: (payload: unknown) => Promise<RpcResponse<{ matched: boolean; result?: CommandExecuteResult }>> =
|
||||
() => Promise.resolve(ok({ matched: false }))
|
||||
onSkillList: (payload: unknown) => Promise<RpcResponse<{ skills: SkillEntry[] }>> = () => Promise.resolve(ok({ skills: [] }))
|
||||
onCommandList: (payload: unknown) => Promise<RpcResponse<{ commands: CommandDescriptor[] }>>
|
||||
= () => Promise.resolve(ok({ commands: [] }))
|
||||
onCommandExecute: (payload: unknown) => Promise<RpcResponse<{ matched: boolean; commandId?: CommandId }>>
|
||||
= () => Promise.resolve(ok({ matched: false }))
|
||||
onSkillList: (payload: unknown) => Promise<RpcResponse<{ skills: SkillEntry[] }>>
|
||||
= () => Promise.resolve(ok({ skills: [] }))
|
||||
|
||||
readonly commands: IApiClient['commands'] = {
|
||||
list: (payload: unknown) => this.record('command.list', payload, this.onCommandList(payload)),
|
||||
|
||||
@@ -36,20 +36,37 @@ describe('createFixtureApi commands/skills', () => {
|
||||
expect(response.result).toMatchObject({ ok: false, error: { code: 'session-not-found' } })
|
||||
})
|
||||
|
||||
it('executes a known command line and reports matched with a result', async () => {
|
||||
it('executes a known command line: pure admission plus a mux-broadcast lifecycle pair', async () => {
|
||||
const api = createFixtureApi()
|
||||
const frames: unknown[] = []
|
||||
const abort = new AbortController()
|
||||
const stream = api.events.mux(req({}), abort.signal)
|
||||
const pump = (async () => {
|
||||
for await (const frame of stream) {
|
||||
frames.push(frame.payload)
|
||||
if (frames.filter(f => (f as { type: string }).type === 'session/event').length >= 2) abort.abort()
|
||||
}
|
||||
})()
|
||||
const response = await api.commands.execute(req({ sessionId: sid('fx-alpha'), line: '/echo hello world' }), signal)
|
||||
if (!response.result.ok) throw new Error('execute failed')
|
||||
expect(response.result.value.matched).toBe(true)
|
||||
expect(response.result.value.result).toEqual({ kind: 'success', text: 'hello world' })
|
||||
expect(response.result.value).toMatchObject({ matched: true })
|
||||
expect(response.result.value.commandId).toBeTruthy()
|
||||
await pump
|
||||
const events = frames
|
||||
.filter((f): f is { type: string; event: { type: string; data: Record<string, unknown> } } => (f as { type: string }).type === 'session/event')
|
||||
.map(f => f.event)
|
||||
expect(events).toMatchObject([
|
||||
{ type: 'command/run', data: { name: 'echo', args: ' hello world', source: { kind: 'user' } } },
|
||||
{ type: 'command/done', data: { kind: 'success', text: 'hello world' } },
|
||||
])
|
||||
expect(events[0]?.data.commandId).toBe(events[1]?.data.commandId)
|
||||
})
|
||||
|
||||
it('addresses execute to the session (result text carries the id)', async () => {
|
||||
it('addresses execute to the session; an unknown session errs', async () => {
|
||||
const api = createFixtureApi()
|
||||
const hit = await api.commands.execute(req({ sessionId: sid('fx-alpha'), line: '/goal-fixture ship' }), signal)
|
||||
if (!hit.result.ok) throw new Error('execute failed')
|
||||
expect(hit.result.value.matched).toBe(true)
|
||||
expect(hit.result.value.result?.text).toContain('fx-alpha')
|
||||
|
||||
const missing = await api.commands.execute(req({ sessionId: sid('fx-nope'), line: '/goal-fixture ship' }), signal)
|
||||
expect(missing.result).toMatchObject({ ok: false, error: { code: 'session-not-found' } })
|
||||
@@ -60,8 +77,8 @@ describe('createFixtureApi commands/skills', () => {
|
||||
for (const line of ['/nope', 'plain text', '/']) {
|
||||
const response = await api.commands.execute(req({ sessionId: sid('fx-alpha'), line }), signal)
|
||||
if (!response.result.ok) throw new Error('execute failed')
|
||||
expect(response.result.value.matched).toBe(false)
|
||||
expect(response.result.value.result).toBeUndefined()
|
||||
// Pure admission value: the matched bit is the whole response shape.
|
||||
expect(response.result.value).toEqual({ matched: false })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -65,13 +65,11 @@ describe('createFixtureApi', () => {
|
||||
const clamped = await api.sessions.history(req({ sessionId: sid('fx-alpha'), beforeSeq: -5, maxMessages: 10 }))
|
||||
if (!clamped.result.ok) throw new Error('clamped failed')
|
||||
expect(clamped.result.value.events).toEqual([])
|
||||
// Unknown session: empty page, not an error (history of a bare id).
|
||||
// Unknown session: empty page, not an error (history of a bare id). The
|
||||
// tail block still rides it — empty-log cut at -1, the host convention.
|
||||
const empty = await api.sessions.history(req({ sessionId: sid('no-such'), maxMessages: 10 }))
|
||||
if (!empty.result.ok) throw new Error('empty failed')
|
||||
expect(empty.result.value).toEqual({
|
||||
events: [],
|
||||
hasMore: false,
|
||||
})
|
||||
expect(empty.result.value).toEqual({ events: [], hasMore: false, projections: { asOfSeq: -1, values: {} } })
|
||||
})
|
||||
|
||||
it('serves grouped models and keeps a selected target for later history and fixture requests', async () => {
|
||||
@@ -217,11 +215,13 @@ describe('createFixtureApi', () => {
|
||||
const second = await openOnce()
|
||||
expect(first[0]?.payload).toMatchObject({ type: 'session/subscribed', sessionId: 'fx-alpha' })
|
||||
expect((first[0]?.payload as { lastSeq: number }).lastSeq).toBeGreaterThan(0)
|
||||
expect(first[1]?.payload).toMatchObject({ type: 'session/title', sessionId: 'fx-alpha', title: 'Fixture 历史会话' })
|
||||
expect(first[2]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
|
||||
expect(second[2]?.rpcId).toBe(first[2]?.rpcId) // stable rpcId across replays (host replay semantics)
|
||||
expect(first[3]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
|
||||
expect(second[3]?.rpcId).toBe(first[3]?.rpcId)
|
||||
// Projection baseline frames follow the subscribed frame (title + todos units).
|
||||
expect(first[1]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'title', value: 'Fixture 历史会话' })
|
||||
expect(first[2]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'todos' })
|
||||
expect(first[3]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
|
||||
expect(second[3]?.rpcId).toBe(first[3]?.rpcId) // stable rpcId across replays (host replay semantics)
|
||||
expect(first[4]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
|
||||
expect(second[4]?.rpcId).toBe(first[4]?.rpcId)
|
||||
})
|
||||
|
||||
it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => {
|
||||
@@ -623,11 +623,11 @@ describe('createFixtureApi', () => {
|
||||
hooks.appendTitle('fx-alpha', 'Fixture 修订标题')
|
||||
await vi.waitFor(() => {
|
||||
expect(seen.some(f => f.type === 'session/event' && JSON.stringify(f.event.data).includes('正常直播'))).toBe(true)
|
||||
expect(seen.some(f => f.type === 'session/title' && f.title === 'Fixture 修订标题')).toBe(true)
|
||||
expect(seen.some(f => f.type === 'session/projection' && f.key === 'title' && f.value === 'Fixture 修订标题')).toBe(true)
|
||||
})
|
||||
expect(seen.some(f => f.type === 'session/event' && JSON.stringify(f.event.data).includes('静默丢帧'))).toBe(false)
|
||||
const rawTitleIndex = seen.findIndex(f => f.type === 'session/event' && (f.event as { type: string }).type === 'session/title')
|
||||
const titleControlIndex = seen.findIndex(f => f.type === 'session/title' && f.title === 'Fixture 修订标题')
|
||||
const titleControlIndex = seen.findIndex(f => f.type === 'session/projection' && f.key === 'title' && f.value === 'Fixture 修订标题')
|
||||
expect(titleControlIndex).toBe(rawTitleIndex + 1)
|
||||
// But history serves the silent event (the client's repull finds it).
|
||||
const repull = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 5 }))
|
||||
|
||||
Reference in New Issue
Block a user