| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- import { Context, Service } from '@deepseek-ai/cordis'
- import { describe, expect, it, vi } from 'vitest'
- import type { SessionId } from '@deepseek-ai/dsh-session/types'
- import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
- import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
- import type { TeamMemberView as TeamRosterMember, TeamTaskId } from '@deepseek-ai/dsh-experimental-agent-team/client'
- import type {} from '@deepseek-ai/dsh-experimental-agent-team/remote'
- import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
- import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'
- import { TeamAction, type TeamActionInjected } from '../src/client/TeamAction.tsx'
- import { inject, mountAgentTeamUi } from '../src/client/mount.ts'
- import { apply as nodeApply } from '../src/index.ts'
- const SESSION = 'team-session' as SessionId
- const CHILD = 'team-child' as SessionId
- const TASK_ID = 'task-1' as TeamTaskId
- const REMOTE: TypertRemoteContribution = {
- package: '@deepseek-ai/dsh-experimental-agent-team',
- descriptors: [],
- }
- async function bench(options: {
- addressed?: boolean
- conflict?: boolean
- registrationFailure?: boolean
- remoteFailure?: 'view' | 'update'
- refreshGate?: Promise<void>
- } = {}) {
- const ctx = new Context()
- const calls: { method: string; args: unknown[] }[] = []
- const answer = <T>(method: string, value: T) => (...args: unknown[]) => {
- calls.push({ method, args })
- return Promise.resolve({ ok: true as const, value })
- }
- const task = {
- id: 'task-1',
- revision: 1, subject: 'Task', description: 'Description', status: 'pending' as const,
- blockedBy: [], writeScopes: [], ready: true, writeScopeWarnings: [],
- }
- class RemoteService extends Service {
- readonly disposeMount = vi.fn(() => Promise.resolve())
- readonly mount = vi.fn((_contribution: unknown) => Promise.resolve(this.disposeMount))
- constructor(serviceCtx: Context) {
- super(serviceCtx, 'remote')
- }
- $mount(contribution: unknown): Promise<() => Promise<void>> {
- return this.mount(contribution)
- }
- }
- const remote = new RemoteService(ctx)
- const failure = {
- ok: false as const,
- error: new RemoteError('gateway/internal', 'offline', {}),
- }
- const view = {
- members: [{
- id: SESSION, name: 'lead', role: 'lead' as const, status: 'idle' as const, diagnostics: [],
- }], tasks: [task],
- }
- ctx.provide('remote.agentTeams', {
- view: (...args: unknown[]) => {
- calls.push({ method: 'agentTeams/view', args })
- return Promise.resolve(options.remoteFailure === 'view'
- ? failure
- : { ok: true as const, value: view })
- },
- createTask: answer('agentTeams/createTask', task),
- updateTask: (...args: unknown[]) => {
- calls.push({ method: 'agentTeams/updateTask', args })
- if (options.remoteFailure === 'update') return Promise.resolve(failure)
- return Promise.resolve(options.conflict
- ? {
- ok: true as const,
- value: {
- ok: false as const,
- error: {
- code: 'team-task-conflict' as const,
- message: 'stale',
- },
- },
- }
- : { ok: true as const, value: { ok: true as const, value: { ...task, revision: 2 } } })
- },
- })
- const navigation: unknown[] = []
- let current = options.addressed === true ? CHILD : SESSION
- ctx.provide('sessions', {
- list: { getSnapshot: () => ({ current }) },
- binding: (id: SessionId) => options.addressed === true && id === CHILD
- ? { session: { getSnapshot: () => ({
- subagent: {
- address: {
- parentSessionId: SESSION,
- childSessionId: CHILD,
- mode: 'continuable' as const,
- },
- },
- }) } }
- : undefined,
- refreshSubagents: (id: SessionId) => {
- navigation.push(['refresh', id])
- return options.refreshGate ?? Promise.resolve()
- },
- openSubagent: (address: unknown) => { navigation.push(['open', address]) },
- })
- ctx.provide('conversation', {})
- ctx.provide('locale', new LocaleRuntime(ctx))
- await ctx.plugin(SlotRegistry).await()
- const collapseHeader = ctx.slots.register({
- name: 'root',
- children: { 'conversation.session.header.actions': { kind: 'list', scope: 'session' } },
- } as never, () => null)
- if (options.registrationFailure === true) {
- vi.spyOn(ctx.slots, 'inject').mockImplementationOnce(() => { throw new Error('slot registration failed') })
- }
- const fiber = options.registrationFailure === true
- ? ctx.plugin({ apply() {} })
- : ctx.plugin({ inject: [...inject], apply: clientCtx => mountAgentTeamUi(clientCtx, REMOTE) })
- const activation: Promise<unknown> = options.registrationFailure === true
- ? mountAgentTeamUi(ctx, REMOTE).catch((error: unknown) => error)
- : fiber.await()
- if (options.registrationFailure !== true) {
- await activation
- } else {
- await fiber.await()
- }
- const entry = () => ctx.slots.entries('conversation.session.header.actions')
- .find(candidate => candidate.component === TeamAction)
- return {
- ctx,
- fiber,
- activation,
- calls,
- navigation,
- remote,
- entry,
- collapseHeader,
- select: (sessionId: SessionId) => { current = sessionId },
- }
- }
- describe('ui-team browser plugin', () => {
- it('registers one disposable header action with RPC-backed task operations', async () => {
- const b = await bench()
- expect(inject).toEqual(['sessions', 'remote', 'slots', 'locale'])
- expect(b.entry()).toMatchObject({
- options: { id: 'agent-team', order: 20 },
- locale: 'agent-team',
- })
- expect(b.remote.mount).toHaveBeenCalledOnce()
- expect(b.remote.mount).toHaveBeenCalledWith(REMOTE)
- const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)()
- expect((await actions.load(SESSION)).ok).toBe(true)
- expect((await actions.createTask(SESSION, {
- subject: 'Task', description: 'Description', blockedBy: [], writeScopes: [],
- })).ok).toBe(true)
- expect((await actions.updateTask(SESSION, {
- taskId: TASK_ID, expectedRevision: 1, action: 'complete',
- })).ok).toBe(true)
- expect((await actions.updateTask(SESSION, {
- taskId: TASK_ID, expectedRevision: 2, action: 'reassign', owner: 'worker',
- })).ok).toBe(true)
- expect(b.calls.map(call => call.method)).toEqual([
- 'agentTeams/view', 'agentTeams/createTask', 'agentTeams/updateTask', 'agentTeams/updateTask',
- ])
- expect(b.calls.at(-1)?.args[1]).toMatchObject({ owner: 'worker' })
- await actions.openTeammate(SESSION, {
- id: SESSION,
- name: 'lead',
- role: 'lead',
- status: 'idle',
- diagnostics: [],
- })
- expect(b.navigation).toEqual([])
- await b.fiber.dispose()
- expect(b.entry()).toBeUndefined()
- expect(b.remote.disposeMount).toHaveBeenCalledOnce()
- })
- it('unmounts the Remote contribution when later Client registration fails', async () => {
- const b = await bench({ registrationFailure: true })
- await expect(b.activation).resolves.toMatchObject({ message: 'slot registration failed' })
- expect(b.remote.mount).toHaveBeenCalledOnce()
- expect(b.remote.disposeMount).toHaveBeenCalledOnce()
- })
- it('returns the generated task business result without a Client transport wrapper', async () => {
- const b = await bench({ conflict: true })
- const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)()
- await expect(actions.updateTask(SESSION, {
- taskId: TASK_ID, expectedRevision: 1, action: 'delete',
- })).resolves.toEqual({
- ok: true,
- value: {
- ok: false,
- error: { code: 'team-task-conflict', message: 'stale' },
- },
- })
- })
- it('returns Remote carrier failures unchanged', async () => {
- const view = await bench({ remoteFailure: 'view' })
- const viewActions = (view.entry()!.inject as unknown as () => TeamActionInjected)()
- await expect(viewActions.load(SESSION)).resolves.toMatchObject({
- ok: false,
- error: { code: 'gateway/internal', message: 'offline' },
- })
- const update = await bench({ remoteFailure: 'update' })
- const updateActions = (update.entry()!.inject as unknown as () => TeamActionInjected)()
- await expect(updateActions.updateTask(SESSION, {
- taskId: TASK_ID, expectedRevision: 1, action: 'delete',
- })).resolves.toMatchObject({
- ok: false,
- error: { code: 'gateway/internal', message: 'offline' },
- })
- })
- it('refreshes the descriptor catalog before opening a continuable teammate address', async () => {
- const b = await bench()
- const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)()
- const member: TeamRosterMember = {
- id: CHILD,
- name: 'worker',
- role: 'teammate',
- status: 'inactive',
- diagnostics: [],
- }
- await actions.openTeammate(SESSION, member)
- expect(b.navigation).toEqual([
- ['refresh', SESSION],
- ['open', {
- parentSessionId: SESSION,
- childSessionId: CHILD,
- mode: 'continuable',
- }],
- ])
- })
- it('routes Team actions from an addressed teammate conversation back through its Lead', async () => {
- const b = await bench({ addressed: true })
- const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)()
- await actions.load(CHILD)
- await actions.openTeammate(CHILD, {
- id: CHILD,
- name: 'worker',
- role: 'teammate',
- status: 'inactive',
- diagnostics: [],
- })
- expect(b.calls[0]).toEqual({ method: 'agentTeams/view', args: [SESSION] })
- expect(b.navigation).toEqual([
- ['refresh', SESSION],
- ['open', {
- parentSessionId: SESSION,
- childSessionId: CHILD,
- mode: 'continuable',
- }],
- ])
- })
- it('does not open a teammate after navigation switches during catalog refresh', async () => {
- const refresh = Promise.withResolvers<undefined>()
- const b = await bench({ refreshGate: refresh.promise })
- const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)()
- const opening = actions.openTeammate(SESSION, {
- id: CHILD,
- name: 'worker',
- role: 'teammate',
- status: 'inactive',
- diagnostics: [],
- })
- expect(b.navigation).toEqual([['refresh', SESSION]])
- b.select('other-session' as SessionId)
- refresh.resolve(undefined)
- await opening
- expect(b.navigation).toEqual([['refresh', SESSION]])
- })
- it('re-registers after the conversation header slot is collapsed and declared again', async () => {
- const b = await bench()
- expect(b.entry()).toBeDefined()
- b.collapseHeader()
- expect(b.entry()).toBeUndefined()
- b.ctx.slots.register({
- name: 'root',
- children: { 'conversation.session.header.actions': { kind: 'list', scope: 'session' } },
- } as never, () => null)
- await Promise.resolve()
- expect(b.entry()).toBeDefined()
- })
- it('keeps the node half inert', () => {
- expect(() => { nodeApply() }).not.toThrow()
- })
- })
|