feat: ship agent-first mission control v0.2.57
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s

This commit is contained in:
AzuTear
2026-07-31 22:39:47 +02:00
parent 3bc7622977
commit f5552218bc
535 changed files with 95242 additions and 8791 deletions
+60
View File
@@ -0,0 +1,60 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { fetchActivity } from '../src/api/activity'
import { queryClient } from '../src/api/queryClient'
import { useAuthStore } from '../src/stores/auth'
beforeEach(() => {
setActivePinia(createPinia())
const auth = useAuthStore()
auth.initialized = true
auth.applySession({
accessToken: 'activity-access-token',
expiresAt: '2026-07-31T12:00:00.000Z',
user: {
id: 'bao',
email: 'bao@nexus.local',
displayName: 'Bao',
role: 'owner',
},
})
queryClient.clear()
})
afterEach(() => {
queryClient.clear()
vi.unstubAllGlobals()
})
describe('activity query contract', () => {
it('preserves the backend entity reference for result navigation', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
items: [{
id: 42,
type: 'TaskUpdated',
message: 'Task moved to review.',
at: '2026-07-31T10:00:00.000Z',
entity: {
type: 'task',
id: '11111111-1111-4111-8111-111111111111',
label: null,
},
}],
totalCount: 1,
page: 1,
pageSize: 200,
totalPages: 1,
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})))
const activity = await fetchActivity()
expect(activity.items[0]?.entity).toEqual({
type: 'task',
id: '11111111-1111-4111-8111-111111111111',
label: null,
})
})
})
+101
View File
@@ -0,0 +1,101 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import {
canApproveAgentProposal,
canRejectAgentProposal,
canRetryAgentProposal,
getAgentProposalStatusMeta,
type AgentProposalDto,
} from '../src/api/agentProposals'
import { apiFetch } from '../src/services/api'
import { useAuthStore } from '../src/stores/auth'
const baseProposal: AgentProposalDto = {
id: '0d2d33d7-bafa-4510-b135-c6a96021957a',
source: 'manual',
requestedName: 'Release Coordinator',
requestedAgentId: 'release-coordinator',
role: 'Delivery Operations',
description: 'Coordinates releases.',
model: null,
emoji: null,
avatar: null,
workspace: '/managed/workspace-release-coordinator',
files: [],
status: 'awaiting_approval',
requestedBy: 'bao',
approvedBy: null,
rejectedBy: null,
rejectionReason: null,
openClawAgentId: null,
openClawWorkspace: null,
error: null,
revision: 1,
createdAt: '2026-07-31T08:00:00.000Z',
updatedAt: '2026-07-31T08:00:00.000Z',
approvedAt: null,
rejectedAt: null,
completedAt: null,
}
beforeEach(() => {
setActivePinia(createPinia())
const auth = useAuthStore()
auth.initialized = true
auth.applySession({
accessToken: 'proposal-access-token',
expiresAt: '2026-07-31T12:00:00.000Z',
user: {
id: 'bao',
email: 'bao@nexus.local',
displayName: 'Bao',
role: 'owner',
},
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
describe('agent proposal frontend contract', () => {
it('maps lifecycle states to safe owner actions', () => {
expect(getAgentProposalStatusMeta('awaiting_approval').label).toBe('Freigabe offen')
expect(canApproveAgentProposal(baseProposal)).toBe(true)
expect(canRejectAgentProposal(baseProposal)).toBe(true)
expect(canRetryAgentProposal(baseProposal)).toBe(false)
const failed = { ...baseProposal, status: 'failed' }
expect(canApproveAgentProposal(failed)).toBe(false)
expect(canRejectAgentProposal(failed)).toBe(false)
expect(canRetryAgentProposal(failed)).toBe(true)
expect(canRetryAgentProposal({ ...baseProposal, status: 'in_doubt' })).toBe(true)
})
it('preserves generated Request headers through the authenticated fetch boundary', async () => {
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}))
vi.stubGlobal('fetch', fetchMock)
const request = new Request('http://localhost/api/v1/openclaw/agent-proposals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': 'agent-proposal-create:test',
'X-Correlation-ID': 'd4f72d7d-6331-4bcc-87e8-e74480315073',
},
body: JSON.stringify({ name: 'Release Coordinator' }),
})
await apiFetch(request)
const [, init] = fetchMock.mock.calls[0]
const headers = new Headers(init?.headers)
expect(headers.get('Authorization')).toBe('Bearer proposal-access-token')
expect(headers.get('Content-Type')).toBe('application/json')
expect(headers.get('Idempotency-Key')).toBe('agent-proposal-create:test')
expect(headers.get('X-Correlation-ID')).toBe('d4f72d7d-6331-4bcc-87e8-e74480315073')
})
})
+70
View File
@@ -0,0 +1,70 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useAuthStore } from '../src/stores/auth'
import { apiFetch } from '../src/services/api'
afterEach(() => {
vi.unstubAllGlobals()
})
describe('auth store', () => {
it('shares the initial refresh across overlapping router guards', async () => {
setActivePinia(createPinia())
const store = useAuthStore()
let resolveRefresh!: (response: Response) => void
const fetchMock = vi.fn(() => new Promise<Response>(resolve => {
resolveRefresh = resolve
}))
vi.stubGlobal('fetch', fetchMock)
const firstGuard = store.initialize()
const secondGuard = store.initialize()
expect(fetchMock).toHaveBeenCalledTimes(1)
resolveRefresh(new Response(JSON.stringify({
accessToken: 'test-access-token',
expiresAt: '2026-07-28T12:00:00.000Z',
user: {
id: 'bao',
email: 'bao@nexus.local',
displayName: 'Bao',
role: 'owner',
},
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}))
await expect(Promise.all([firstGuard, secondGuard])).resolves.toEqual([true, true])
expect(store.isAuthenticated).toBe(true)
expect(store.user?.displayName).toBe('Bao')
})
it('uses the JWT without emitting a browser-controlled agent identity header', async () => {
setActivePinia(createPinia())
const store = useAuthStore()
store.initialized = true
store.applySession({
accessToken: 'signed-jwt',
expiresAt: '2026-07-28T12:00:00.000Z',
user: {
id: 'iris-user',
email: 'iris@nexus.local',
displayName: 'Iris',
role: 'owner',
},
})
const fetchMock = vi.fn(() => Promise.resolve(new Response(null, { status: 204 })))
vi.stubGlobal('fetch', fetchMock)
await apiFetch('/api/v1/tasks/board')
const requestInit = fetchMock.mock.calls[0]?.[1] as RequestInit
const headers = new Headers(requestInit.headers)
expect(headers.get('Authorization')).toBe('Bearer signed-jwt')
expect(headers.has('X-Agent-Id')).toBe(false)
})
})
+119
View File
@@ -0,0 +1,119 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useAuthStore } from '../src/stores/auth'
import { useMissionControlUiStore } from '../src/stores/missionControlUi'
import { buildMissionControlContext } from '../src/utils/missionControlContext'
import { reportOperationEnvelope } from '../src/services/operationResults'
import { routeForEntity } from '../src/utils/entityNavigation'
beforeEach(() => {
setActivePinia(createPinia())
})
describe('mission control context', () => {
it('maps an object route into bounded Iris context', () => {
const context = buildMissionControlContext({
name: 'TaskDetail',
path: '/tasks/98f927bb',
params: { id: '98f927bb' },
})
expect(context).toEqual({
routeName: 'TaskDetail',
path: '/tasks/98f927bb',
surface: 'Task detail',
entityType: 'task',
entityId: '98f927bb',
})
})
it('keeps index routes free of invented entity identifiers', () => {
const context = buildMissionControlContext({
name: 'Run Control',
path: '/runs',
params: {},
})
expect(context.surface).toBe('Run Control')
expect(context.entityType).toBeNull()
expect(context.entityId).toBeNull()
})
})
describe('mission control overlays', () => {
it('keeps command and Iris dialogs mutually exclusive', () => {
useAuthStore().applySession({
accessToken: 'owner-token',
expiresAt: '2026-07-31T12:00:00.000Z',
user: {
id: 'bao',
email: 'bao@nexus.local',
displayName: 'Bao',
role: 'owner',
},
})
const store = useMissionControlUiStore()
store.openCommand()
expect(store.commandOpen).toBe(true)
expect(store.irisOpen).toBe(false)
store.openIris()
expect(store.commandOpen).toBe(false)
expect(store.irisOpen).toBe(true)
})
it('refuses to open the Iris mutation dialog for non-owners', () => {
useAuthStore().applySession({
accessToken: 'viewer-token',
expiresAt: '2026-07-31T12:00:00.000Z',
user: {
id: 'viewer',
email: 'viewer@nexus.local',
displayName: 'Viewer',
role: 'viewer',
},
})
const store = useMissionControlUiStore()
store.openCommand()
store.openIris()
expect(store.commandOpen).toBe(true)
expect(store.irisOpen).toBe(false)
})
it('publishes and dismisses a normalized mutation result', () => {
const store = useMissionControlUiStore()
const result = reportOperationEnvelope({
operation: {
operationId: 'operation-9',
status: 'updated',
revision: '3',
primaryRef: { type: 'task', id: 'task-9', label: 'Task 9' },
affectedRefs: [{ type: 'agent', id: 'iris', label: 'Iris' }],
traceId: null,
},
}, 'Task aktualisiert')
expect(result?.revision).toBe(3)
expect(store.operationTitle).toBe('Task aktualisiert')
expect(store.operationResult?.primaryRef?.id).toBe('task-9')
store.dismissOperation()
expect(store.operationResult).toBeNull()
})
it('resolves OpenClaw runtime and agent-file references to existing routes', () => {
expect(routeForEntity({ type: 'openclaw-task', id: 'runtime-1', label: null })).toEqual({
name: 'Run Control',
query: { task: 'runtime-1' },
})
expect(routeForEntity({ type: 'agent-file', id: 'iris/AGENTS.md', label: null })).toEqual({
name: 'AgentDetail',
params: { id: 'iris' },
query: { file: 'AGENTS.md' },
})
})
})
+88
View File
@@ -0,0 +1,88 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import {
fetchOpenClawModelAuthStatus,
refreshOpenClawModelAuthStatus,
type OpenClawModelAuthCollectionDto,
} from '../src/api/openClawModels'
import { queryClient, queryKeys } from '../src/api/queryClient'
import { useAuthStore } from '../src/stores/auth'
function collection(status: string): OpenClawModelAuthCollectionDto {
return {
state: 'ready',
items: [{
provider: 'openai',
displayName: 'OpenAI',
status,
expiry: null,
profiles: [{ type: 'oauth', status: 'ok', count: 1 }],
apiKey: null,
usage: { summary: 'Ready', plan: 'team' },
}],
nextCursor: null,
message: null,
recovery: null,
checkedAt: '2026-07-31T10:00:00.000Z',
}
}
function requestUrl(input: RequestInfo | URL): URL {
const raw = input instanceof Request ? input.url : String(input)
return new URL(raw, 'http://localhost')
}
beforeEach(() => {
setActivePinia(createPinia())
const auth = useAuthStore()
auth.initialized = true
auth.applySession({
accessToken: 'models-access-token',
expiresAt: '2026-07-31T12:00:00.000Z',
user: {
id: 'bao',
email: 'bao@nexus.local',
displayName: 'Bao',
role: 'owner',
},
})
queryClient.clear()
})
afterEach(() => {
queryClient.clear()
vi.unstubAllGlobals()
})
describe('OpenClaw model authentication query', () => {
it('loads the generated sanitized contract without forcing a gateway refresh', async () => {
const fetchMock = vi.fn(async () => new Response(JSON.stringify(collection('ok')), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}))
vi.stubGlobal('fetch', fetchMock)
await expect(fetchOpenClawModelAuthStatus()).resolves.toEqual(collection('ok'))
const [input, init] = fetchMock.mock.calls[0]!
const url = requestUrl(input)
expect(url.pathname).toBe('/api/v1/openclaw/models/auth-status')
expect(url.searchParams.get('refresh')).toBe('false')
expect(new Headers(init?.headers).get('Authorization')).toBe('Bearer models-access-token')
})
it('forces a refresh through the same canonical query cache', async () => {
queryClient.setQueryData(queryKeys.openClawModelAuth(), collection('stale'))
const fetchMock = vi.fn(async () => new Response(JSON.stringify(collection('ok')), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}))
vi.stubGlobal('fetch', fetchMock)
await expect(refreshOpenClawModelAuthStatus()).resolves.toEqual(collection('ok'))
const url = requestUrl(fetchMock.mock.calls[0]![0])
expect(url.searchParams.get('refresh')).toBe('true')
expect(queryClient.getQueryData(queryKeys.openClawModelAuth())).toEqual(collection('ok'))
})
})
+251
View File
@@ -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()
})
})
+79
View File
@@ -0,0 +1,79 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import {
startOpenClawRun,
type OpenClawRunDto,
} from '../src/api/openClawRuns'
import { useAuthStore } from '../src/stores/auth'
const run: OpenClawRunDto = {
id: '98f927bb-3df8-44d0-9ec9-cd98cdcf12db',
title: 'Review the task',
prompt: 'Review task 42 and propose the next action.',
agentId: 'iris',
sessionKey: 'agent:iris:main',
status: 'running',
taskId: null,
projectId: null,
openClawRunId: 'openclaw-run-42',
retriedFromRunId: null,
correlationId: 'correlation-42',
actor: 'bao',
lastError: null,
lastGatewaySequence: 42,
sequenceGapDetected: false,
canStop: true,
canRetry: false,
canResume: false,
resumeCapabilityMessage: 'Same-run resume is not advertised.',
createdAt: '2026-07-30T10:00:00.000Z',
updatedAt: '2026-07-30T10:00:01.000Z',
startedAt: '2026-07-30T10:00:01.000Z',
finishedAt: null,
}
beforeEach(() => {
setActivePinia(createPinia())
const auth = useAuthStore()
auth.initialized = true
auth.applySession({
accessToken: 'test-access-token',
expiresAt: '2026-07-30T12:00:00.000Z',
user: {
id: 'bao',
email: 'bao@nexus.local',
displayName: 'Bao',
role: 'owner',
},
})
})
describe('OpenClaw durable runs', () => {
it('sends idempotency, correlation and W3C trace metadata for a dispatch', async () => {
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
ok: true,
state: 'running',
message: 'Run dispatched.',
run,
resultRun: null,
completedAt: '2026-07-30T10:00:01.000Z',
}), {
status: 201,
headers: { 'Content-Type': 'application/json' },
}))
vi.stubGlobal('fetch', fetchMock)
const result = await startOpenClawRun({
prompt: run.prompt,
agentId: run.agentId,
sessionKey: run.sessionKey,
})
expect(result.run.id).toBe(run.id)
const [, init] = fetchMock.mock.calls[0]
const headers = new Headers(init?.headers)
expect(headers.get('Authorization')).toBe('Bearer test-access-token')
expect(headers.get('Idempotency-Key')).toMatch(/^openclaw-run-start:/)
expect(headers.get('X-Correlation-ID')).toMatch(/^[0-9a-f-]{36}$/i)
expect(headers.get('traceparent')).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/)
})
})
+217
View File
@@ -0,0 +1,217 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import {
fetchOpenClawAgents,
fetchOpenClawCapabilities,
openClawOverviewQueryOptions,
projectOpenClawAgentNodes,
type OpenClawOverviewDto,
} from '../src/api/openclawRuntime'
import { queryClient } from '../src/api/queryClient'
import { useAuthStore } from '../src/stores/auth'
import type {
OpenClawAgent,
OpenClawCapability,
OpenClawCollection,
} from '../src/types/openclaw'
const now = '2026-07-31T10:00:00.000Z'
function collection<T>(items: T[]): OpenClawCollection<T> {
return {
state: 'ready',
items,
nextCursor: null,
message: null,
recovery: null,
checkedAt: now,
}
}
function overviewFixture(): OpenClawOverviewDto {
return {
connection: {
state: 'connected',
configured: true,
credentialConfigured: true,
connected: true,
endpoint: 'ws://openclaw-gateway:18789',
gatewayVersion: '2026.7.1',
requiredVersion: '2026.7.1',
versionPinned: true,
versionMatches: true,
protocolVersion: 4,
grantedScopes: ['operator.read'],
advertisedEvents: ['agent', 'task'],
lastConnectedAt: now,
lastEventAt: now,
reconnectAttempts: 0,
message: 'Connected',
recovery: null,
checkedAt: now,
deviceId: 'nexus-test',
pairingRequired: false,
pairingRequestId: null,
},
capabilities: [],
tasks: collection([{
id: 'task-1',
title: 'Prepare release',
status: 'running',
kind: 'agent',
runtime: 'openclaw',
agentId: 'iris',
sessionKey: 'agent:iris:main',
runId: 'run-1',
flowId: null,
parentTaskId: null,
createdAt: now,
startedAt: now,
updatedAt: now,
finishedAt: null,
progress: 42,
summary: 'Checks the release gates.',
error: null,
canCancel: true,
}]),
sessions: collection([{
key: 'agent:iris:main',
sessionId: 'session-1',
agentId: 'iris',
title: 'Release session',
status: 'running',
kind: 'agent',
channel: null,
model: 'openai/gpt-5.4',
provider: 'openai',
runId: 'run-1',
updatedAt: now,
inputTokens: 800,
outputTokens: 400,
totalTokens: 1_200,
canAbort: true,
}]),
cronJobs: collection([]),
approvals: collection([]),
activity: collection([]),
models: collection([{
id: 'openai/gpt-5.4',
name: 'GPT-5.4',
provider: 'openai',
configured: true,
available: true,
contextWindow: 1_000_000,
reason: null,
}]),
agents: collection([{
id: 'iris',
name: 'Iris',
description: 'Coordinates the team.',
model: null,
provider: 'openai',
workspace: '/managed/agents/iris',
status: 'ready',
}]),
generatedAt: now,
}
}
function authenticate(): void {
const auth = useAuthStore()
auth.initialized = true
auth.applySession({
accessToken: 'runtime-access-token',
expiresAt: '2026-07-31T12:00:00.000Z',
user: {
id: 'bao',
email: 'bao@nexus.local',
displayName: 'Bao',
role: 'owner',
},
})
}
function pathOf(input: RequestInfo | URL): string {
const raw = input instanceof Request ? input.url : String(input)
return new URL(raw, 'http://localhost').pathname
}
beforeEach(() => {
setActivePinia(createPinia())
authenticate()
queryClient.clear()
})
afterEach(() => {
queryClient.clear()
vi.unstubAllGlobals()
})
describe('shared OpenClaw runtime queries', () => {
it('deduplicates simultaneous overview consumers at the authenticated boundary', async () => {
const fetchMock = vi.fn(async () => new Response(JSON.stringify(overviewFixture()), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}))
vi.stubGlobal('fetch', fetchMock)
const options = openClawOverviewQueryOptions()
const results = await Promise.all([
queryClient.fetchQuery(options),
queryClient.fetchQuery(options),
queryClient.fetchQuery(options),
])
expect(results.every(result => result.connection.connected)).toBe(true)
expect(fetchMock).toHaveBeenCalledTimes(1)
const [input, init] = fetchMock.mock.calls[0]!
expect(pathOf(input)).toBe('/api/v1/openclaw/overview')
expect(new Headers(init?.headers).get('Authorization')).toBe('Bearer runtime-access-token')
})
it('uses the dedicated capability and agent contracts', async () => {
const capabilities: OpenClawCapability[] = [{
id: 'agents-list',
label: 'List agents',
method: 'agents.list',
requiredScope: 'operator.read',
available: true,
state: 'ready',
reason: null,
}]
const agents: OpenClawCollection<OpenClawAgent> = collection([
overviewFixture().agents.items[0]!,
])
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const path = pathOf(input)
return new Response(JSON.stringify(
path.endsWith('/capabilities') ? capabilities : agents,
), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
})
vi.stubGlobal('fetch', fetchMock)
await expect(fetchOpenClawCapabilities()).resolves.toEqual(capabilities)
await expect(fetchOpenClawAgents()).resolves.toEqual(agents)
expect(fetchMock.mock.calls.map(call => pathOf(call[0]))).toEqual([
'/api/v1/openclaw/capabilities',
'/api/v1/openclaw/agents',
])
})
it('projects task, session, model and token state without a dashboard facade', () => {
expect(projectOpenClawAgentNodes(overviewFixture())).toEqual([
expect.objectContaining({
id: 'iris',
status: 'work',
statusLabel: 'Arbeitet',
task: 'Prepare release',
progress: 42,
model: 'openai/gpt-5.4',
tokens: '1.2k',
}),
])
})
})
-25
View File
@@ -1,25 +0,0 @@
import { describe, it, expect } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useOperationsStore } from '../src/stores/operations'
describe('operations store', () => {
it('initializes with safe fallback structure', () => {
setActivePinia(createPinia())
const store = useOperationsStore()
// Fallback provides a valid structure even when API is down
expect(store.snapshot).toBeDefined()
expect(store.snapshot.runtime.runtime).toBe('OpenClaw')
expect(store.snapshot.metrics).toBeDefined()
expect(Array.isArray(store.snapshot.projects)).toBe(true)
expect(Array.isArray(store.snapshot.tasks)).toBe(true)
expect(Array.isArray(store.snapshot.activity)).toBe(true)
expect(Array.isArray(store.snapshot.models)).toBe(true)
})
it('initializes routing as empty array', () => {
setActivePinia(createPinia())
const store = useOperationsStore()
expect(Array.isArray(store.routing)).toBe(true)
expect(store.routing.length).toBe(0)
})
})
+88
View File
@@ -0,0 +1,88 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { queryClient, queryKeys } from '../src/api/queryClient'
import { applyTaskBoardCardDelta, type TaskBoardCardDto, type TaskBoardPageDto } from '../src/api/taskBoard'
afterEach(() => {
queryClient.clear()
})
describe('canonical server-state query cache', () => {
it('deduplicates three simultaneous consumers into one HTTP query', async () => {
let resolveRequest: ((value: string[]) => void) | undefined
const response = new Promise<string[]>(resolve => {
resolveRequest = resolve
})
const queryFn = vi.fn(() => response)
const options = {
queryKey: queryKeys.projects(),
queryFn,
staleTime: 15_000,
}
const consumers = [
queryClient.fetchQuery(options),
queryClient.fetchQuery(options),
queryClient.fetchQuery(options),
]
expect(queryFn).toHaveBeenCalledTimes(1)
resolveRequest?.(['project-1'])
await expect(Promise.all(consumers)).resolves.toEqual([
['project-1'],
['project-1'],
['project-1'],
])
expect(queryFn).toHaveBeenCalledTimes(1)
})
it('moves a persisted task delta between board columns without a full snapshot', () => {
const card: TaskBoardCardDto = {
id: '0d2d33d7-bafa-4510-b135-c6a96021957a',
title: 'Release review',
detail: null,
source: 'bao',
state: 'Backlog',
priority: 'Medium',
assignedTo: 'bao',
parentTaskId: null,
projectId: null,
dueDate: null,
createdAt: '2026-07-31T08:00:00.000Z',
updatedAt: '2026-07-31T08:00:00.000Z',
isAgentTask: false,
expectedFrom: null,
lastActivityMessage: null,
lastActivityAt: null,
childTaskCount: 0,
openChildTaskCount: 0,
hasVisibleDelegation: false,
}
const page: TaskBoardPageDto = {
revision: 'board-r1',
offen: [card],
inProgress: [],
review: [],
blocked: [],
done: [],
nextDoneCursor: null,
hasMoreDone: false,
}
queryClient.setQueryData(queryKeys.taskBoard(50), {
pages: [page],
pageParams: [null],
})
applyTaskBoardCardDelta({
...card,
state: 'Review',
updatedAt: '2026-07-31T08:01:00.000Z',
})
const result = queryClient.getQueryData<{
pages: TaskBoardPageDto[]
pageParams: Array<string | null>
}>(queryKeys.taskBoard(50))
expect(result?.pages[0]?.offen).toEqual([])
expect(result?.pages[0]?.review.map(item => item.id)).toEqual([card.id])
})
})
+236
View File
@@ -0,0 +1,236 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { QueryClient } from '@tanstack/vue-query'
import { createPinia, setActivePinia } from 'pinia'
import {
applyAgentFileWrite,
fetchAgentActivity,
fetchAgentDetail,
fetchAgentFile,
fetchAgentFiles,
fetchAgentSummary,
type AgentFileCollectionDto,
type AgentFileDto,
type AgentFileWriteDto,
} from '../src/api/agentDetail'
import {
fetchDoc,
fetchDocs,
fetchMemoryFile,
fetchMemoryFiles,
fetchMemorySearch,
} from '../src/api/knowledge'
import { fetchIncident, fetchIncidents } from '../src/api/incidents'
import { queryKeys } from '../src/api/queryClient'
import { fetchSecurityStatus } from '../src/api/security'
import { useAuthStore } from '../src/stores/auth'
function requestUrl(input: RequestInfo | URL): URL {
const raw = input instanceof Request ? input.url : String(input)
return new URL(raw, 'http://localhost')
}
function jsonResponse(value: unknown) {
return new Response(JSON.stringify(value), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}
const file: AgentFileDto = {
agentId: 'iris',
name: 'SOUL.md',
missing: false,
size: 42,
updatedAt: '2026-07-31T10:00:00.000Z',
content: '# Iris',
contentHash: 'hash-1',
checkedAt: '2026-07-31T10:00:01.000Z',
}
beforeEach(() => {
setActivePinia(createPinia())
const auth = useAuthStore()
auth.initialized = true
auth.applySession({
accessToken: 'server-state-token',
expiresAt: '2026-07-31T12:00:00.000Z',
user: {
id: 'bao',
email: 'bao@nexus.local',
displayName: 'Bao',
role: 'owner',
},
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
describe('remaining server-state fetchers', () => {
it('loads agent detail, activity, summary and generated OpenClaw file contracts', async () => {
const responses = new Map<string, unknown>([
['/api/v1/agents/iris', {
id: 'iris',
name: 'Iris',
role: 'orchestrator',
model: 'gpt-5',
status: 'Online',
}],
['/api/v1/agents/iris/activity', [{
id: 1,
type: 'task',
message: 'Delegated work',
at: '2026-07-31T09:00:00.000Z',
source: 'openclaw',
}]],
['/api/v1/agents/iris/summary', {
now: { text: 'Working', source: 'openclaw' },
today: { text: 'Completed review', source: 'openclaw' },
generatedAt: '2026-07-31T10:00:00.000Z',
}],
['/api/v1/openclaw/agents/iris/files', {
agentId: 'iris',
files: [{
name: file.name,
missing: file.missing,
size: file.size,
updatedAt: file.updatedAt,
contentHash: file.contentHash,
}],
checkedAt: file.checkedAt,
}],
['/api/v1/openclaw/agents/iris/files/SOUL.md', file],
])
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const value = responses.get(requestUrl(input).pathname)
return value === undefined
? new Response(null, { status: 404 })
: jsonResponse(value)
})
vi.stubGlobal('fetch', fetchMock)
await expect(fetchAgentDetail('iris')).resolves.toMatchObject({ id: 'iris' })
await expect(fetchAgentActivity('iris')).resolves.toHaveLength(1)
await expect(fetchAgentSummary('iris')).resolves.toMatchObject({
now: { text: 'Working' },
})
await expect(fetchAgentFiles('iris')).resolves.toMatchObject({
files: [{ name: 'SOUL.md' }],
})
await expect(fetchAgentFile('iris', 'SOUL.md')).resolves.toEqual(file)
expect(fetchMock).toHaveBeenCalledTimes(5)
for (const [, init] of fetchMock.mock.calls) {
expect(new Headers(init?.headers).get('Authorization'))
.toBe('Bearer server-state-token')
}
})
it('loads docs, memory, incidents and security through authenticated fetchers', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = requestUrl(input)
if (url.pathname === '/api/v1/docs') return jsonResponse([])
if (url.pathname === '/api/v1/docs/runbook%2Fdeploy.md') {
return jsonResponse({ name: 'deploy.md', path: 'runbook/deploy.md', content: '# Deploy' })
}
if (url.pathname === '/api/v1/memory') return jsonResponse([])
if (url.pathname === '/api/v1/memory/2026-07-31.md') {
return jsonResponse({ name: '2026-07-31.md', content: 'Memory' })
}
if (url.pathname === '/api/v1/memory/search') {
return jsonResponse([{ name: '2026-07-31.md', excerpt: 'Mission control' }])
}
if (url.pathname === '/api/v1/incidents') return jsonResponse([])
if (url.pathname === '/api/v1/incidents/gateway.md') {
return jsonResponse({ name: 'gateway.md', title: 'Gateway', content: '# Gateway' })
}
if (url.pathname === '/api/v1/security/status') {
return jsonResponse({
authMethod: 'JWT',
tokenConfig: {
issuer: 'nexus',
audience: 'nexus-web',
refreshTokenDays: 7,
accessTokenMinutes: 15,
},
rateLimit: 'enabled',
passwordPolicy: 'strong',
cookieConfig: { httpOnly: true, secure: true, sameSite: 'Strict' },
twoFactorEnabled: false,
passkeyEnabled: false,
})
}
return new Response(null, { status: 404 })
})
vi.stubGlobal('fetch', fetchMock)
await expect(fetchDocs()).resolves.toEqual([])
await expect(fetchDoc('runbook/deploy.md')).resolves.toMatchObject({ name: 'deploy.md' })
await expect(fetchMemoryFiles()).resolves.toEqual([])
await expect(fetchMemoryFile('2026-07-31.md')).resolves.toMatchObject({
content: 'Memory',
})
await expect(fetchMemorySearch('mission control')).resolves.toHaveLength(1)
await expect(fetchIncidents()).resolves.toEqual([])
await expect(fetchIncident('gateway.md')).resolves.toMatchObject({ title: 'Gateway' })
await expect(fetchSecurityStatus()).resolves.toMatchObject({ authMethod: 'JWT' })
const urls = fetchMock.mock.calls.map(([input]) => requestUrl(input))
expect(urls.find(url => url.pathname === '/api/v1/memory/search')
?.searchParams.get('q')).toBe('mission control')
})
})
describe('agent file cache synchronization', () => {
it('patches and invalidates only the written file and its collection', async () => {
const client = new QueryClient()
const collection: AgentFileCollectionDto = {
agentId: 'iris',
files: [{
name: file.name,
missing: true,
size: null,
updatedAt: null,
contentHash: null,
}],
checkedAt: '2026-07-31T09:00:00.000Z',
}
const previousFile: AgentFileDto = {
...file,
missing: true,
size: null,
updatedAt: null,
content: null,
contentHash: 'missing',
checkedAt: collection.checkedAt,
}
const writeResult: AgentFileWriteDto = {
ok: true,
state: 'saved',
message: 'Saved and verified.',
file,
verified: true,
idempotencyKey: 'idem-1',
correlationId: 'corr-1',
completedAt: file.checkedAt,
}
client.setQueryData(queryKeys.agentFiles('iris'), collection)
client.setQueryData(queryKeys.agentFile('iris', 'SOUL.md'), previousFile)
client.setQueryData(queryKeys.agentFile('iris', 'TOOLS.md'), { marker: 'other' })
await applyAgentFileWrite(client, 'iris', writeResult)
expect(client.getQueryData(queryKeys.agentFile('iris', 'SOUL.md'))).toEqual(file)
expect(client.getQueryData<AgentFileCollectionDto>(queryKeys.agentFiles('iris')))
.toMatchObject({
checkedAt: file.checkedAt,
files: [{ name: 'SOUL.md', missing: false, contentHash: 'hash-1' }],
})
expect(client.getQueryState(queryKeys.agentFiles('iris'))?.isInvalidated).toBe(true)
expect(client.getQueryState(queryKeys.agentFile('iris', 'SOUL.md'))?.isInvalidated)
.toBe(true)
expect(client.getQueryState(queryKeys.agentFile('iris', 'TOOLS.md'))?.isInvalidated)
.toBe(false)
})
})
+93
View File
@@ -0,0 +1,93 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { AuthenticatedSseHub, type SseConnectionState } from '../src/services/sseHub'
import { useAuthStore } from '../src/stores/auth'
beforeEach(() => {
setActivePinia(createPinia())
const auth = useAuthStore()
auth.initialized = true
auth.applySession({
accessToken: 'test-access-token',
expiresAt: '2026-07-31T12:00:00.000Z',
user: {
id: 'bao',
email: 'bao@nexus.local',
displayName: 'Bao',
role: 'owner',
},
})
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
describe('authenticated SSE hub', () => {
it('backs off with jitter after a normal EOF instead of reconnecting immediately', async () => {
vi.useFakeTimers()
vi.spyOn(Math, 'random').mockReturnValue(0.5)
const fetchMock = vi.fn(async () => new Response('', {
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
}))
vi.stubGlobal('fetch', fetchMock)
const states: SseConnectionState[] = []
const hub = new AuthenticatedSseHub()
const unsubscribe = hub.subscribe('/api/v1/events', () => undefined, {
onStateChange: state => states.push(state),
})
await vi.advanceTimersByTimeAsync(0)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(states).toContain('open')
expect(states.at(-1)).toBe('reconnecting')
await vi.advanceTimersByTimeAsync(999)
expect(fetchMock).toHaveBeenCalledTimes(1)
await vi.advanceTimersByTimeAsync(1)
expect(fetchMock).toHaveBeenCalledTimes(2)
unsubscribe()
hub.closeAll()
})
it('reconnects a stalled stream when the heartbeat budget expires', async () => {
vi.useFakeTimers()
vi.spyOn(Math, 'random').mockReturnValue(0.5)
const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
init?.signal?.addEventListener('abort', () => {
controller.error(init.signal?.reason ?? new Error('aborted'))
}, { once: true })
},
})
return new Response(stream, {
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
})
})
vi.stubGlobal('fetch', fetchMock)
const states: SseConnectionState[] = []
const hub = new AuthenticatedSseHub()
const unsubscribe = hub.subscribe('/api/v1/events', () => undefined, {
onStateChange: state => states.push(state),
})
await vi.advanceTimersByTimeAsync(45_000)
expect(states).toContain('error')
expect(fetchMock).toHaveBeenCalledTimes(1)
await vi.advanceTimersByTimeAsync(1_000)
expect(fetchMock).toHaveBeenCalledTimes(2)
unsubscribe()
hub.closeAll()
})
})
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import {
buildTaskAgentOptions,
taskAgentLabel,
} from '../src/utils/taskAgentOptions'
describe('live task assignee options', () => {
it('combines only local assignment concepts with the live OpenClaw inventory', () => {
const options = buildTaskAgentOptions([
{ id: 'iris', name: 'Iris' },
{ id: 'new-specialist', name: 'New Specialist' },
])
expect(options).toEqual([
{ id: '', label: 'Nicht zugewiesen' },
{ id: 'bao', label: 'Bao' },
{ id: 'iris', label: 'Iris' },
{ id: 'new-specialist', label: 'New Specialist' },
])
expect(options.some(option => option.id === 'programmer')).toBe(false)
})
it('preserves a persisted unknown assignee while OpenClaw is disconnected', () => {
const options = buildTaskAgentOptions([], ['retired-agent'])
expect(options).toContainEqual({
id: 'retired-agent',
label: 'retired-agent',
})
expect(taskAgentLabel('retired-agent', options)).toBe('retired-agent')
})
it('prefers the live display name for an already persisted agent id', () => {
const options = buildTaskAgentOptions(
[{ id: 'product-owner', name: 'Product Owner' }],
['product-owner'],
)
expect(taskAgentLabel('product-owner', options)).toBe('Product Owner')
expect(options.filter(option => option.id === 'product-owner')).toHaveLength(1)
})
})