203 lines
6.2 KiB
TypeScript
203 lines
6.2 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { AssistantExpert } from '../../shared/assistant-contracts'
|
|
import type { SubagentEvent } from '../../shared/contracts'
|
|
import type {
|
|
AgentExecutionRequest,
|
|
AgentRuntime
|
|
} from '../agent/runtime'
|
|
import { SubagentService } from './subagent-service'
|
|
import { SubagentScheduler } from './subagent-scheduler'
|
|
|
|
const expert: AssistantExpert = {
|
|
id: '00000000-0000-4000-8000-000000000001',
|
|
name: '研究专家',
|
|
description: '',
|
|
systemInstructions: 'Separate evidence from assumptions.',
|
|
routingKeywords: ['研究'],
|
|
enabled: true,
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
updatedAt: '2026-01-01T00:00:00.000Z'
|
|
}
|
|
|
|
const parentRequest: AgentExecutionRequest = {
|
|
requestId: '00000000-0000-4000-8000-000000000010',
|
|
conversationId: 'conversation',
|
|
workMode: 'ask',
|
|
prompt: '研究这份材料'
|
|
}
|
|
|
|
function database() {
|
|
return {
|
|
createTask: vi.fn(() => ({})),
|
|
updateTaskStatus: vi.fn(),
|
|
appendTaskEvent: vi.fn()
|
|
}
|
|
}
|
|
|
|
describe('SubagentService', () => {
|
|
it('creates a linked child task and puts expert instructions in system context', async () => {
|
|
let executionRequest: AgentExecutionRequest | undefined
|
|
const expertOutput = '结果'.repeat(35_001)
|
|
const runtime = {
|
|
run: async function* (request: AgentExecutionRequest) {
|
|
executionRequest = request
|
|
yield {
|
|
requestId: request.requestId,
|
|
type: 'text',
|
|
delta: expertOutput
|
|
} as const
|
|
yield { requestId: request.requestId, type: 'done' } as const
|
|
},
|
|
releaseConversation: vi.fn(async () => undefined),
|
|
dispose: vi.fn(async () => undefined)
|
|
} as unknown as AgentRuntime
|
|
const db = database()
|
|
const service = new SubagentService(
|
|
runtime,
|
|
db as never,
|
|
new SubagentScheduler({ timeoutMs: 1_000 })
|
|
)
|
|
const events: SubagentEvent[] = []
|
|
const result = await service.run({
|
|
parentRequest,
|
|
expert,
|
|
routingMode: 'smart',
|
|
signal: new AbortController().signal,
|
|
onEvent: (event) => events.push(event)
|
|
})
|
|
|
|
expect(result.output).toBe(expertOutput)
|
|
expect(result.output.length).toBeGreaterThan(60_000)
|
|
expect(db.createTask).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
parentTaskId: parentRequest.requestId,
|
|
expertId: expert.id,
|
|
routingMode: 'smart',
|
|
status: 'queued'
|
|
})
|
|
)
|
|
expect(executionRequest?.prompt).toBe(parentRequest.prompt)
|
|
expect(executionRequest?.trustedInstructions).toContain(
|
|
expert.systemInstructions
|
|
)
|
|
expect(events.map((event) => event.state)).toEqual([
|
|
'queued',
|
|
'running',
|
|
'completed'
|
|
])
|
|
expect(events.at(-1)).toMatchObject({
|
|
state: 'completed',
|
|
output: expertOutput
|
|
})
|
|
await service.dispose()
|
|
})
|
|
|
|
it('allows expert tools through the parent policy and records their activity', async () => {
|
|
const events: SubagentEvent[] = []
|
|
const authorize = vi.fn(async () => 'once' as const)
|
|
let receivedAuthorize: unknown
|
|
const runtime = {
|
|
run: async function* (
|
|
request: AgentExecutionRequest,
|
|
signal: AbortSignal,
|
|
runtimeAuthorize: unknown
|
|
) {
|
|
void signal
|
|
receivedAuthorize = runtimeAuthorize
|
|
yield {
|
|
requestId: request.requestId,
|
|
type: 'text',
|
|
delta: '部分结果'
|
|
} as const
|
|
yield {
|
|
requestId: request.requestId,
|
|
type: 'tool',
|
|
callId: 'call',
|
|
name: 'unsafe',
|
|
state: 'running',
|
|
summary: 'unsafe'
|
|
} as const
|
|
yield { requestId: request.requestId, type: 'done' } as const
|
|
},
|
|
dispose: vi.fn(async () => undefined)
|
|
} as unknown as AgentRuntime
|
|
const db = database()
|
|
const service = new SubagentService(runtime, db as never)
|
|
await expect(service.run({
|
|
parentRequest: { ...parentRequest, workMode: 'execute' },
|
|
expert,
|
|
routingMode: 'manual',
|
|
signal: new AbortController().signal,
|
|
onEvent: (event) => events.push(event),
|
|
authorize
|
|
})).resolves.toMatchObject({ output: '部分结果' })
|
|
expect(receivedAuthorize).toBe(authorize)
|
|
expect(db.createTask).toHaveBeenCalledWith(
|
|
expect.objectContaining({ workMode: 'execute' })
|
|
)
|
|
expect(db.appendTaskEvent).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
'tool',
|
|
expect.objectContaining({ name: 'unsafe' })
|
|
)
|
|
expect(events.at(-1)).toMatchObject({
|
|
state: 'completed',
|
|
output: '部分结果'
|
|
})
|
|
await service.dispose()
|
|
})
|
|
|
|
it('uses an expert model profile and falls back to the default runtime', async () => {
|
|
const calls: string[] = []
|
|
const createRuntime = (label: string): AgentRuntime =>
|
|
({
|
|
run: async function* (request: AgentExecutionRequest) {
|
|
calls.push(label)
|
|
yield {
|
|
requestId: request.requestId,
|
|
type: 'text',
|
|
delta: label
|
|
} as const
|
|
yield { requestId: request.requestId, type: 'done' } as const
|
|
},
|
|
releaseConversation: vi.fn(async () => undefined),
|
|
dispose: vi.fn(async () => undefined)
|
|
}) as unknown as AgentRuntime
|
|
const defaultRuntime = createRuntime('default')
|
|
const profileRuntime = createRuntime('profile')
|
|
const profileId = '00000000-0000-4000-8000-000000000002'
|
|
const service = new SubagentService(
|
|
defaultRuntime,
|
|
database() as never,
|
|
new SubagentScheduler({ timeoutMs: 1_000 }),
|
|
new Map([[profileId, profileRuntime]])
|
|
)
|
|
|
|
const selected = await service.run({
|
|
parentRequest,
|
|
expert: { ...expert, modelProfileId: profileId },
|
|
routingMode: 'manual',
|
|
signal: new AbortController().signal,
|
|
onEvent: vi.fn()
|
|
})
|
|
const fallback = await service.run({
|
|
parentRequest: {
|
|
...parentRequest,
|
|
requestId: '00000000-0000-4000-8000-000000000011'
|
|
},
|
|
expert: {
|
|
...expert,
|
|
modelProfileId: '00000000-0000-4000-8000-000000000099'
|
|
},
|
|
routingMode: 'manual',
|
|
signal: new AbortController().signal,
|
|
onEvent: vi.fn()
|
|
})
|
|
|
|
expect(selected.output).toBe('profile')
|
|
expect(fallback.output).toBe('default')
|
|
expect(calls).toEqual(['profile', 'default'])
|
|
await service.dispose()
|
|
})
|
|
})
|