feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
import { QueryClient, type InfiniteData } from '@tanstack/vue-query'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
applyOpenClawCronDetail,
|
||||
mergeOpenClawCronPages,
|
||||
} from '../src/api/openClawCron'
|
||||
import {
|
||||
applyOpenClawRunOperation,
|
||||
type OpenClawRunCollectionDto,
|
||||
type OpenClawRunDto,
|
||||
type OpenClawRunOperationDto,
|
||||
} from '../src/api/openClawRuns'
|
||||
import { queryKeys } from '../src/api/queryClient'
|
||||
import {
|
||||
invalidateOpenClawCronEventQueries,
|
||||
invalidateOpenClawRunEventQueries,
|
||||
} from '../src/services/domainEvents'
|
||||
import type {
|
||||
OpenClawCollection,
|
||||
OpenClawCronJob,
|
||||
OpenClawCronJobDetail,
|
||||
} from '../src/types/openclaw'
|
||||
|
||||
function run(overrides: Partial<OpenClawRunDto> = {}): OpenClawRunDto {
|
||||
return {
|
||||
id: '98f927bb-3df8-44d0-9ec9-cd98cdcf12db',
|
||||
title: 'Review task',
|
||||
prompt: 'Review the task.',
|
||||
agentId: 'iris',
|
||||
sessionKey: 'agent:iris:main',
|
||||
status: 'running',
|
||||
taskId: '931a9025-f2d6-4e48-8a41-24e8733807db',
|
||||
projectId: null,
|
||||
openClawRunId: 'oc-run-1',
|
||||
retriedFromRunId: null,
|
||||
correlationId: 'correlation-1',
|
||||
actor: 'bao',
|
||||
lastError: null,
|
||||
lastGatewaySequence: 1,
|
||||
sequenceGapDetected: false,
|
||||
canStop: true,
|
||||
canRetry: false,
|
||||
canResume: false,
|
||||
resumeCapabilityMessage: null,
|
||||
createdAt: '2026-07-31T10:00:00.000Z',
|
||||
updatedAt: '2026-07-31T10:01:00.000Z',
|
||||
startedAt: '2026-07-31T10:00:01.000Z',
|
||||
finishedAt: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function runCollection(items: OpenClawRunDto[]): OpenClawRunCollectionDto {
|
||||
return {
|
||||
items,
|
||||
nextCursor: 'cursor-1',
|
||||
checkedAt: '2026-07-31T10:01:00.000Z',
|
||||
}
|
||||
}
|
||||
|
||||
function cronJob(id: string, enabled = true): OpenClawCronJob {
|
||||
return {
|
||||
id,
|
||||
name: `Job ${id}`,
|
||||
description: null,
|
||||
schedule: '0 7 * * *',
|
||||
timeZone: 'Europe/Berlin',
|
||||
enabled,
|
||||
status: 'idle',
|
||||
agentId: 'iris',
|
||||
sessionKey: 'agent:iris:main',
|
||||
nextRunAt: null,
|
||||
lastRunAt: null,
|
||||
lastRunStatus: null,
|
||||
lastError: null,
|
||||
canRun: true,
|
||||
resourceHash: `hash-${id}`,
|
||||
}
|
||||
}
|
||||
|
||||
function cronDetail(id: string, enabled = true): OpenClawCronJobDetail {
|
||||
return {
|
||||
id,
|
||||
name: `Updated ${id}`,
|
||||
displayName: null,
|
||||
description: null,
|
||||
enabled,
|
||||
deleteAfterRun: false,
|
||||
agentId: 'iris',
|
||||
sessionKey: 'agent:iris:main',
|
||||
sessionTarget: 'isolated',
|
||||
wakeMode: 'now',
|
||||
schedule: {
|
||||
kind: 'cron',
|
||||
expression: '0 8 * * *',
|
||||
timeZone: 'Europe/Berlin',
|
||||
at: null,
|
||||
everyMs: null,
|
||||
anchorMs: null,
|
||||
staggerMs: null,
|
||||
command: null,
|
||||
workingDirectory: null,
|
||||
},
|
||||
payload: {
|
||||
kind: 'agentTurn',
|
||||
text: null,
|
||||
message: 'Run a bounded check.',
|
||||
model: null,
|
||||
fallbacks: [],
|
||||
thinking: null,
|
||||
timeoutSeconds: null,
|
||||
allowUnsafeExternalContent: null,
|
||||
lightContext: null,
|
||||
toolsAllow: [],
|
||||
arguments: [],
|
||||
workingDirectory: null,
|
||||
environmentKeys: [],
|
||||
inputConfigured: false,
|
||||
noOutputTimeoutSeconds: null,
|
||||
outputMaxBytes: null,
|
||||
},
|
||||
delivery: null,
|
||||
trigger: null,
|
||||
failureAlert: null,
|
||||
createdAt: '2026-07-31T10:00:00.000Z',
|
||||
updatedAt: '2026-07-31T10:02:00.000Z',
|
||||
nextRunAt: null,
|
||||
lastRunAt: null,
|
||||
lastRunStatus: null,
|
||||
lastError: null,
|
||||
resourceHash: `updated-hash-${id}`,
|
||||
canUpdate: true,
|
||||
canDelete: true,
|
||||
canRun: true,
|
||||
}
|
||||
}
|
||||
|
||||
describe('OpenClaw Vue Query cache adapters', () => {
|
||||
it('patches generated run contracts across list and task-related caches', () => {
|
||||
const client = new QueryClient()
|
||||
const current = run()
|
||||
const other = run({
|
||||
id: '568f10e3-4fb0-4af4-acff-53030387ce72',
|
||||
taskId: null,
|
||||
title: 'Other run',
|
||||
})
|
||||
const listKey = queryKeys.openClawRuns({ limit: 50 })
|
||||
const taskKey = queryKeys.taskRuns(current.taskId!)
|
||||
const runningKey = queryKeys.openClawRuns({ limit: 50, status: 'running' })
|
||||
client.setQueryData(listKey, runCollection([current, other]))
|
||||
client.setQueryData(taskKey, runCollection([current]))
|
||||
client.setQueryData(runningKey, runCollection([current]))
|
||||
const completed = run({
|
||||
status: 'completed',
|
||||
canStop: false,
|
||||
updatedAt: '2026-07-31T10:03:00.000Z',
|
||||
finishedAt: '2026-07-31T10:03:00.000Z',
|
||||
})
|
||||
const operation: OpenClawRunOperationDto = {
|
||||
ok: true,
|
||||
state: 'completed',
|
||||
message: 'Completed.',
|
||||
run: completed,
|
||||
resultRun: null,
|
||||
completedAt: '2026-07-31T10:03:00.000Z',
|
||||
}
|
||||
|
||||
applyOpenClawRunOperation(client, operation)
|
||||
|
||||
expect(client.getQueryData<OpenClawRunCollectionDto>(listKey)?.items)
|
||||
.toEqual([completed, other])
|
||||
expect(client.getQueryData<OpenClawRunCollectionDto>(listKey)?.nextCursor)
|
||||
.toBe('cursor-1')
|
||||
expect(client.getQueryData<OpenClawRunCollectionDto>(taskKey)?.items)
|
||||
.toEqual([completed])
|
||||
expect(client.getQueryData<OpenClawRunCollectionDto>(runningKey)?.items)
|
||||
.toEqual([])
|
||||
expect(client.getQueryData(queryKeys.openClawRun(completed.id))).toEqual(completed)
|
||||
})
|
||||
|
||||
it('merges cron pages and updates only compatible cached lists', () => {
|
||||
const client = new QueryClient()
|
||||
const enabledKey = queryKeys.openClawCron({ includeDisabled: false, limit: 50 })
|
||||
const allKey = queryKeys.openClawCron({ includeDisabled: true, limit: 50 })
|
||||
const pages: InfiniteData<OpenClawCollection<OpenClawCronJob>, string | null> = {
|
||||
pages: [
|
||||
{
|
||||
state: 'ready',
|
||||
items: [cronJob('a'), cronJob('b')],
|
||||
nextCursor: 'next',
|
||||
message: null,
|
||||
recovery: null,
|
||||
checkedAt: '2026-07-31T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
state: 'ready',
|
||||
items: [cronJob('b'), cronJob('c')],
|
||||
nextCursor: null,
|
||||
message: null,
|
||||
recovery: null,
|
||||
checkedAt: '2026-07-31T10:01:00.000Z',
|
||||
},
|
||||
],
|
||||
pageParams: [null, 'next'],
|
||||
}
|
||||
client.setQueryData(enabledKey, pages)
|
||||
client.setQueryData(allKey, pages)
|
||||
|
||||
applyOpenClawCronDetail(client, cronDetail('a', false))
|
||||
|
||||
const enabled = mergeOpenClawCronPages(client.getQueryData(enabledKey))
|
||||
const all = mergeOpenClawCronPages(client.getQueryData(allKey))
|
||||
expect(enabled?.items.map(item => item.id)).toEqual(['b', 'c'])
|
||||
expect(all?.items.find(item => item.id === 'a')).toMatchObject({
|
||||
name: 'Updated a',
|
||||
enabled: false,
|
||||
resourceHash: 'updated-hash-a',
|
||||
})
|
||||
expect(all?.items.map(item => item.id)).toEqual(['a', 'b', 'c'])
|
||||
expect(all?.nextCursor).toBeNull()
|
||||
})
|
||||
|
||||
it('invalidates only the affected run and cron domains', async () => {
|
||||
const runId = '98f927bb-3df8-44d0-9ec9-cd98cdcf12db'
|
||||
const otherRunId = '568f10e3-4fb0-4af4-acff-53030387ce72'
|
||||
const cronId = 'cron-a'
|
||||
const client = (await import('../src/api/queryClient')).queryClient
|
||||
client.clear()
|
||||
client.setQueryData(queryKeys.openClawRuns({ limit: 50 }), runCollection([]))
|
||||
client.setQueryData(queryKeys.openClawRunHistory(runId), { marker: 'target' })
|
||||
client.setQueryData(queryKeys.openClawRunHistory(otherRunId), { marker: 'other' })
|
||||
client.setQueryData(queryKeys.openClawCron({ includeDisabled: true, limit: 50 }), { marker: 'list' })
|
||||
client.setQueryData(queryKeys.openClawCronDetail(cronId), cronDetail(cronId))
|
||||
client.setQueryData(queryKeys.openClawCronDetail('cron-b'), cronDetail('cron-b'))
|
||||
client.setQueryData(queryKeys.openClawAgents(), { marker: 'agents' })
|
||||
|
||||
await invalidateOpenClawRunEventQueries(runId)
|
||||
|
||||
expect(client.getQueryState(queryKeys.openClawRuns({ limit: 50 }))?.isInvalidated).toBe(true)
|
||||
expect(client.getQueryState(queryKeys.openClawRunHistory(runId))?.isInvalidated).toBe(true)
|
||||
expect(client.getQueryState(queryKeys.openClawRunHistory(otherRunId))?.isInvalidated).toBe(false)
|
||||
expect(client.getQueryState(queryKeys.openClawAgents())?.isInvalidated).toBe(false)
|
||||
|
||||
await invalidateOpenClawCronEventQueries(cronId)
|
||||
|
||||
expect(client.getQueryState(queryKeys.openClawCron({ includeDisabled: true, limit: 50 }))?.isInvalidated).toBe(true)
|
||||
expect(client.getQueryState(queryKeys.openClawCronDetail(cronId))?.isInvalidated).toBe(true)
|
||||
expect(client.getQueryState(queryKeys.openClawCronDetail('cron-b'))?.isInvalidated).toBe(false)
|
||||
client.clear()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user