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 { 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(listKey)?.items) .toEqual([completed, other]) expect(client.getQueryData(listKey)?.nextCursor) .toBe('cursor-1') expect(client.getQueryData(taskKey)?.items) .toEqual([completed]) expect(client.getQueryData(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, 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() }) })