mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
95 lines
5.0 KiB
TypeScript
95 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { LiveSnapshot, TeamRun, TeamRunTask } from '@/api'
|
|
import { createAgentsLiveRouteHydrator, parseAgentsLiveRoute, reconcileAgentsLiveRoute } from '../agentsLiveRouteState'
|
|
|
|
const task = (id: string, conversationId: string | null): TeamRunTask => ({
|
|
id, teamId: '10', runId: '20', taskNumber: 1, subject: id, description: null, status: 'in_progress',
|
|
priority: 0, taskType: 'execution', assigneeAgentId: 'agent', ownerAgentId: null, blockedBy: null,
|
|
requireApproval: false, progressPercent: null, progressStep: null, result: null, reason: null,
|
|
conversationId, metadata: null, createTime: null, updateTime: null,
|
|
})
|
|
const run = (id: string, tasks: TeamRunTask[]): TeamRun => ({
|
|
id, teamId: '10', workspaceId: '1', leadAgentId: 'lead', leadConversationId: 'lead-conv',
|
|
originMessageId: null, title: id, objective: id, status: 'running', finalSummary: null, stopReason: null,
|
|
metadata: null, startedAt: null, completedAt: null, createTime: null, updateTime: null,
|
|
progress: { total: tasks.length, done: 0, failed: 0, inReview: 0, percent: 0 }, tasks,
|
|
})
|
|
const live = (...conversationIds: string[]): LiveSnapshot => ({
|
|
runs: conversationIds.map(conversationId => ({
|
|
conversationId, agentId: 1, agentName: conversationId, agentIcon: null, username: null,
|
|
currentPhase: 'tools', runningToolName: null, waitingReason: null, done: false, stopRequested: false,
|
|
firstTokenReceived: true, subscriberCount: 1, queueLen: 0, ageMs: 1, msSinceLastEvent: 1,
|
|
stuckReason: null, orphan: false, subagentCount: 0,
|
|
})),
|
|
subagents: [], timestamp: 1,
|
|
summary: { running: conversationIds.length, stuck: 0, orphan: 0, queued: 0, subagentsActive: 0 },
|
|
})
|
|
|
|
describe('agents live route state', () => {
|
|
it('parses the whole route and reconciles browser navigation A to B to none', () => {
|
|
const selectedRun = run('20', [task('A', 'worker-a'), task('B', 'worker-b')])
|
|
expect(parseAgentsLiveRoute({ view: 'live', teamRunId: '20', taskId: 'A' })).toEqual({
|
|
view: 'live', runId: '20', taskId: 'A', requiredRunId: '20',
|
|
})
|
|
expect(reconcileAgentsLiveRoute(parseAgentsLiveRoute({ view: 'live', teamRunId: '20', taskId: 'A' }), [selectedRun], live('worker-a', 'worker-b')).selectedTaskId).toBe('A')
|
|
expect(reconcileAgentsLiveRoute(parseAgentsLiveRoute({ view: 'live', teamRunId: '20', taskId: 'B' }), [selectedRun], live('worker-a', 'worker-b')).selectedTaskId).toBe('B')
|
|
expect(reconcileAgentsLiveRoute(parseAgentsLiveRoute({ view: 'live', teamRunId: '20' }), [selectedRun], live('worker-a', 'worker-b')).selectedTaskId).toBeNull()
|
|
})
|
|
|
|
it('preserves an ended task deep link and reports its worker as offline', () => {
|
|
const selectedRun = run('20', [task('A', 'worker-a'), task('offline', 'worker-offline')])
|
|
expect(reconcileAgentsLiveRoute(
|
|
parseAgentsLiveRoute({ view: 'live', teamRunId: '20', taskId: 'offline' }), [selectedRun], live('worker-a'),
|
|
)).toMatchObject({
|
|
selectedRunId: '20', selectedTaskId: 'offline',
|
|
selectedWorker: { taskId: 'offline', conversationId: 'worker-offline', online: false },
|
|
replaceQuery: null,
|
|
})
|
|
})
|
|
|
|
it('clears a task outside the selected run and clears both ids for an invalid run', () => {
|
|
const selectedRun = run('20', [task('A', 'worker-a')])
|
|
expect(reconcileAgentsLiveRoute(
|
|
parseAgentsLiveRoute({ view: 'live', teamRunId: '20', taskId: 'other' }), [selectedRun], live('worker-a'),
|
|
)).toMatchObject({ selectedRunId: '20', selectedTaskId: null, replaceQuery: { view: 'live', teamRunId: '20' } })
|
|
expect(reconcileAgentsLiveRoute(
|
|
parseAgentsLiveRoute({ view: 'live', teamRunId: 'missing', taskId: 'A' }), [selectedRun], live('worker-a'),
|
|
)).toMatchObject({ selectedRunId: null, selectedTaskId: null, replaceQuery: { view: 'live' } })
|
|
})
|
|
|
|
it('ignores an older route hydration after a newer route finishes', async () => {
|
|
const oldLoad = deferred<void>()
|
|
const newLoad = deferred<void>()
|
|
const refreshed: Array<string | null> = []
|
|
const replaced: unknown[] = []
|
|
const hydrator = createAgentsLiveRouteHydrator({
|
|
invalidatePoll: () => {},
|
|
ensureRun: (runId) => {
|
|
refreshed.push(runId)
|
|
return runId === 'old' ? oldLoad.promise : newLoad.promise
|
|
},
|
|
reconcile: route => ({
|
|
selectedRunId: route.runId, selectedTaskId: route.taskId, selectedWorker: null,
|
|
replaceQuery: route.runId === 'old' ? { view: 'live' } : null,
|
|
}),
|
|
replace: query => { replaced.push(query); return Promise.resolve() },
|
|
})
|
|
|
|
const oldHydration = hydrator.hydrate(parseAgentsLiveRoute({ view: 'live', teamRunId: 'old' }))
|
|
const newHydration = hydrator.hydrate(parseAgentsLiveRoute({ view: 'live', teamRunId: 'new' }))
|
|
newLoad.resolve()
|
|
await newHydration
|
|
oldLoad.resolve()
|
|
await oldHydration
|
|
|
|
expect(refreshed).toEqual(['old', 'new'])
|
|
expect(replaced).toEqual([])
|
|
})
|
|
})
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void
|
|
const promise = new Promise<T>(done => { resolve = done })
|
|
return { promise, resolve }
|
|
}
|