237 lines
7.9 KiB
TypeScript
237 lines
7.9 KiB
TypeScript
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)
|
|
})
|
|
})
|