Files
nexus/frontend/e2e/support/nexusApi.ts
T
AzuTear f5552218bc
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
feat: ship agent-first mission control v0.2.57
2026-07-31 22:39:47 +02:00

1200 lines
33 KiB
TypeScript

import type { Page, Request, Route } from '@playwright/test'
export const PROPOSAL_ID = '11111111-1111-4111-8111-111111111111'
export const PROJECT_ID = '22222222-2222-4222-8222-222222222222'
export const DOC_PATH = 'nexus/mission-control.md'
export const MEMORY_NAME = '2026-07-31.md'
export const INCIDENT_NAME = '2026-07-30-gateway-timeout.md'
type NexusRole = 'owner' | 'member'
type ProposalStatus =
| 'awaiting_approval'
| 'provisioning'
| 'ready'
| 'partial'
| 'failed'
| 'in_doubt'
| 'rejected'
interface MockOptions {
authenticated?: boolean
role?: NexusRole
proposalStatus?: ProposalStatus
domainResyncSequence?: number
boardRefreshDelayMs?: number
boardRefreshTitle?: string
}
interface CapturedRequest {
path: string
method: string
headers: Record<string, string>
body: unknown
}
export interface NexusApiHarness {
readonly proposalRequests: CapturedRequest[]
readonly boardRequestUrls: string[]
readonly domainEventRequests: Array<{ lastEventId: string | null }>
readonly taskMutationRequests: CapturedRequest[]
readonly chatRequests: CapturedRequest[]
readonly resyncResponseCount: number
readonly openClawOverviewRequestCount: number
readonly proposal: Record<string, unknown>
}
const now = '2026-07-31T10:00:00.000Z'
function collection(items: unknown[] = []) {
return {
state: 'ready',
items,
nextCursor: null,
message: null,
recovery: null,
checkedAt: now,
}
}
function task(
id: string,
title: string,
state: string,
overrides: Record<string, unknown> = {},
) {
return {
id,
title,
detail: `${title} detail`,
source: 'bao',
state,
priority: 'Medium',
assignedTo: 'bao',
parentTaskId: null,
projectId: null,
dueDate: null,
createdAt: now,
updatedAt: now,
isAgentTask: false,
expectedFrom: null,
lastActivityMessage: 'Visible E2E activity',
lastActivityAt: now,
childTaskCount: 0,
openChildTaskCount: 0,
hasVisibleDelegation: false,
childTasks: [],
...overrides,
}
}
const openTask = task('task-open-1', 'E2E Backlog task', 'Backlog')
const progressTask = task('task-progress-1', 'E2E Active task', 'In progress')
const firstDoneTask = task('task-done-1', 'E2E Done page one', 'Done')
const secondDoneTask = task('task-done-2', 'E2E Done page two', 'Done')
const projectTask = task('task-project-1', 'E2E Project task', 'In progress', {
projectId: PROJECT_ID,
assignedTo: 'iris',
expectedFrom: 'iris',
isAgentTask: true,
})
const unrelatedTask = task('task-unrelated-1', 'E2E Unrelated task', 'Backlog', {
projectId: null,
})
const projectFixture = {
id: PROJECT_ID,
name: 'Release Readiness',
description: 'Coordinates the release across tasks, agents, and runs.',
status: 'Online',
progress: 64,
createdAt: now,
updatedAt: now,
}
const projectRunFixture = {
id: '33333333-3333-4333-8333-333333333333',
title: 'Release readiness verification',
prompt: 'Verify the linked project.',
agentId: 'iris',
sessionKey: 'agent:iris:project-release',
status: 'completed',
taskId: projectTask.id,
projectId: PROJECT_ID,
openClawRunId: 'openclaw-project-run-1',
retriedFromRunId: null,
correlationId: 'e2e-project-correlation',
actor: 'bao',
lastError: null,
lastGatewaySequence: 42,
sequenceGapDetected: false,
canStop: false,
canRetry: true,
canResume: false,
resumeCapabilityMessage: 'Completed runs cannot be resumed.',
createdAt: now,
updatedAt: now,
startedAt: now,
finishedAt: now,
}
const agentDetailFixture = {
id: 'iris',
name: 'Iris',
role: 'Chief of Staff',
model: 'openai/gpt-5.4',
status: 'Online',
lastSeen: now,
workspace: '/managed/agents/iris',
agentDir: '/managed/agents/iris/agent',
description: 'Coordinates E2E work.',
subAgents: ['release-sentinel'],
identityName: 'Iris',
}
const agentActivityFixture = [
{
id: 81,
type: 'task',
message: 'Verified the OpenClaw content inventory.',
at: now,
source: 'activity',
relativeTime: 'just now',
},
]
const agentSummaryFixture = {
now: {
text: 'Verified the OpenClaw content inventory.',
source: 'nexus-activity',
timestamp: now,
},
today: {
text: 'Last 24h: Verified the OpenClaw content inventory.',
source: 'nexus-activity',
timestamp: now,
},
generatedAt: now,
}
const standardAgentFiles = [
'AGENTS.md',
'SOUL.md',
'TOOLS.md',
'IDENTITY.md',
'USER.md',
'HEARTBEAT.md',
'BOOTSTRAP.md',
'MEMORY.md',
] as const
const agentFileCollectionFixture = {
agentId: 'iris',
files: standardAgentFiles.map((name, index) => ({
name,
missing: false,
size: 64 + index,
updatedAt: now,
contentHash: `sha256-e2e-${name.toLowerCase().replace('.', '-')}`,
})),
checkedAt: now,
}
const docFixture = {
name: 'mission-control.md',
path: DOC_PATH,
category: 'nexus',
type: 'md',
size: 112,
modifiedAt: now,
sourceAgentId: 'iris',
workspacePath: DOC_PATH,
}
const docDetailFixture = {
name: docFixture.name,
path: docFixture.path,
content: '# Mission Control Runbook\n\nOpenClaw-backed documentation is visible in Nexus.',
size: docFixture.size,
modifiedAt: docFixture.modifiedAt,
sourceAgentId: docFixture.sourceAgentId,
workspacePath: docFixture.workspacePath,
}
const memoryFixture = {
name: MEMORY_NAME,
path: MEMORY_NAME,
size: 96,
modifiedAt: now,
sourceAgentId: 'iris',
workspacePath: `memory/${MEMORY_NAME}`,
}
const memoryDetailFixture = {
...memoryFixture,
content: '# Daily Memory\n\nAgent-first context is loaded from Iris.',
}
const memorySearchFixture = {
name: memoryFixture.name,
path: memoryFixture.path,
excerpt: 'Agent-first context is loaded from Iris.',
size: memoryFixture.size,
sourceAgentId: memoryFixture.sourceAgentId,
workspacePath: memoryFixture.workspacePath,
}
const incidentFixture = {
name: INCIDENT_NAME,
title: 'Gateway Timeout Recovered',
date: '2026-07-30',
severity: 'major',
excerpt: 'The gateway recovered without duplicate provisioning.',
size: 144,
sourceAgentId: 'iris',
workspacePath: `memory/incidents/${INCIDENT_NAME}`,
}
const incidentDetailFixture = {
name: incidentFixture.name,
title: incidentFixture.title,
date: incidentFixture.date,
content: [
'# Gateway Timeout Recovered',
'',
'**Severity:** major',
'',
'The gateway recovered without duplicate provisioning.',
].join('\n'),
size: incidentFixture.size,
sourceAgentId: incidentFixture.sourceAgentId,
workspacePath: incidentFixture.workspacePath,
}
const securityStatusFixture = {
authMethod: 'JWT + PBKDF2',
tokenConfig: {
issuer: 'nexus',
audience: 'nexus-web',
refreshTokenDays: 7,
accessTokenMinutes: 30,
},
rateLimit: '5 login attempts per minute per IP',
passwordPolicy: 'Minimum 10 characters',
cookieConfig: {
httpOnly: true,
secure: true,
sameSite: 'Strict',
},
twoFactorEnabled: false,
passkeyEnabled: false,
checkedAt: now,
}
function buildProposal(status: ProposalStatus = 'awaiting_approval'): Record<string, unknown> {
return {
id: PROPOSAL_ID,
source: 'manual',
requestedName: 'Release Sentinel',
requestedAgentId: 'release-sentinel',
role: 'Release Operations',
description: 'Verifies release readiness and reports blockers.',
model: 'openai/gpt-5.4',
emoji: null,
avatar: null,
workspace: '/managed/agents/release-sentinel',
files: [
{
name: 'SOUL.md',
contentHash: 'abc123def456abc123def456',
size: 42,
content: '# Release Sentinel\nVerify before reporting.',
},
],
status,
requestedBy: 'Bao Owner',
approvedBy: status === 'ready' ? 'Bao Owner' : null,
rejectedBy: null,
rejectionReason: null,
openClawAgentId: status === 'ready' ? 'release-sentinel' : null,
openClawWorkspace: status === 'ready' ? '/managed/agents/release-sentinel' : null,
error: null,
revision: status === 'ready' ? 2 : 1,
createdAt: now,
updatedAt: now,
approvedAt: status === 'ready' ? now : null,
rejectedAt: null,
completedAt: status === 'ready' ? now : null,
}
}
function openClawOverview() {
const agents = [
{
id: 'iris',
name: 'Iris',
description: 'Coordinates E2E work.',
model: 'openai/gpt-5.4',
provider: 'openai',
workspace: '/managed/agents/iris',
status: 'ready',
},
]
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', 'operator.admin'],
advertisedEvents: [],
lastConnectedAt: now,
lastEventAt: now,
reconnectAttempts: 0,
message: 'Connected',
recovery: null,
checkedAt: now,
deviceId: 'nexus-e2e',
pairingRequired: false,
pairingRequestId: null,
},
capabilities: [],
tasks: collection([]),
sessions: collection([]),
cronJobs: collection([]),
approvals: collection([]),
activity: collection([]),
models: collection([]),
agents: collection(agents),
generatedAt: now,
}
}
function operationsSnapshot() {
return {
generatedAt: now,
runtime: {
runtime: 'OpenClaw',
status: 'Connected',
detail: 'E2E fixture',
},
models: [],
metrics: {
activeAgents: 1,
queuedTasks: 2,
successRate: 100,
incidents: 0,
},
projects: [],
tasks: [],
activity: [],
}
}
async function fulfillJson(route: Route, body: unknown, status = 200) {
await route.fulfill({
status,
contentType: 'application/json',
body: JSON.stringify(body),
})
}
function requestBody(request: Request): unknown {
const raw = request.postData()
if (!raw) return null
try {
return JSON.parse(raw)
} catch {
return raw
}
}
function problem(detail: string, status: number) {
return {
type: 'about:blank',
title: status === 403 ? 'Forbidden' : 'Not found',
status,
detail,
}
}
function operationResult(
status: string,
primaryRef: { type: string; id: string; label?: string | null },
affectedRefs: Array<{ type: string; id: string; label?: string | null }> = [],
) {
return {
operationId: `e2e-${status}-${primaryRef.id}`,
status,
revision: 1,
primaryRef,
affectedRefs,
traceId: 'e2e-trace-1',
}
}
export async function mockNexusApi(
page: Page,
options: MockOptions = {},
): Promise<NexusApiHarness> {
const authenticated = options.authenticated ?? true
const role = options.role ?? 'owner'
const proposalRequests: CapturedRequest[] = []
const boardRequestUrls: string[] = []
const domainEventRequests: Array<{ lastEventId: string | null }> = []
const taskMutationRequests: CapturedRequest[] = []
const chatRequests: CapturedRequest[] = []
let proposal = buildProposal(options.proposalStatus)
let resyncResponseCount = 0
let boardInitialRequestCount = 0
let openClawOverviewRequestCount = 0
let openTaskState = 'Backlog'
await page.route('**/api/**', async route => {
const request = route.request()
const url = new URL(request.url())
const { pathname: path } = url
const method = request.method()
// The Vite source graph contains paths such as /src/api/client.ts, which
// also match Playwright's **/api/** glob. Only intercept the actual HTTP
// API boundary and let application modules continue to the dev server.
if (!path.startsWith('/api/')) {
await route.fallback()
return
}
if (path === '/api/v1/auth/refresh' && method === 'POST') {
if (!authenticated) {
await fulfillJson(route, problem('No active E2E session.', 401), 401)
return
}
await fulfillJson(route, {
accessToken: `e2e-${role}-token`,
expiresAt: '2026-08-01T10:00:00.000Z',
user: {
id: role === 'owner' ? 'owner-e2e' : 'member-e2e',
email: role === 'owner' ? 'bao@example.test' : 'viewer@example.test',
displayName: role === 'owner' ? 'Bao Owner' : 'Nexus Viewer',
role,
},
})
return
}
if (path === '/api/v1/auth/logout' && method === 'POST') {
await route.fulfill({ status: 204 })
return
}
if (path === '/api/v1/events') {
const lastEventId = request.headers()['last-event-id'] ?? null
domainEventRequests.push({ lastEventId })
if (options.domainResyncSequence && resyncResponseCount === 0) {
const deadline = Date.now() + 2_000
while (!boardRequestUrls.some(value => !value.includes('doneCursor=')) && Date.now() < deadline) {
await new Promise(resolve => setTimeout(resolve, 20))
}
// Let the initial query paint before the stream requests a REST resync.
await new Promise(resolve => setTimeout(resolve, 120))
resyncResponseCount += 1
await route.fulfill({
status: 200,
contentType: 'text/event-stream',
headers: { 'Cache-Control': 'no-cache' },
body: [
`id: ${options.domainResyncSequence}`,
'event: resync_required',
`data: ${JSON.stringify({
sequence: options.domainResyncSequence,
eventType: 'resync_required',
entity: { type: 'event-stream', id: '*', label: null },
entityRevision: `stream-${options.domainResyncSequence}`,
occurredAt: now,
payload: { reason: 'stale_or_missing_cursor' },
})}`,
'',
'',
].join('\n'),
})
return
}
const cursorLine = options.domainResyncSequence
? `id: ${options.domainResyncSequence}\n`
: ''
await route.fulfill({
status: 200,
contentType: 'text/event-stream',
headers: { 'Cache-Control': 'no-cache' },
body: `${cursorLine}event: heartbeat\ndata: {}\n\n`,
})
return
}
if (path === '/api/v1/projects' && method === 'GET') {
await fulfillJson(route, [projectFixture])
return
}
if (path === '/api/v1/activity' && method === 'GET') {
await fulfillJson(route, {
items: [{
id: 71,
type: 'task.updated',
message: 'Nexus task deep link is ready.',
at: now,
entity: {
type: 'task',
id: openTask.id,
label: openTask.title,
},
}],
totalCount: 1,
page: 1,
pageSize: 200,
totalPages: 1,
})
return
}
if (path === `/api/v1/projects/${PROJECT_ID}` && method === 'GET') {
await fulfillJson(route, projectFixture)
return
}
if (path === `/api/v1/projects/${PROJECT_ID}/tasks` && method === 'GET') {
await fulfillJson(route, [projectTask])
return
}
if (path === '/api/v1/tasks' && method === 'GET') {
await fulfillJson(route, [projectTask, unrelatedTask])
return
}
if (path === '/api/v1/operations/snapshot') {
await fulfillJson(route, operationsSnapshot())
return
}
if (path === '/api/v1/routing') {
await fulfillJson(route, [])
return
}
if (path === '/api/v1/openclaw/overview') {
openClawOverviewRequestCount += 1
await fulfillJson(route, openClawOverview())
return
}
if (path === '/api/v1/openclaw/capabilities') {
await fulfillJson(route, [])
return
}
if (path === '/api/v1/openclaw/models/auth-status' && method === 'GET') {
await fulfillJson(route, collection([{
provider: 'openai',
displayName: 'OpenAI',
status: 'ok',
expiry: null,
profiles: [{ type: 'oauth', status: 'ok', count: 1 }],
apiKey: null,
usage: { summary: 'Ready', plan: 'team' },
}]))
return
}
if (path === '/api/v1/openclaw/agents' && method === 'GET') {
await fulfillJson(route, openClawOverview().agents)
return
}
if (path === '/api/v1/agents/iris' && method === 'GET') {
await fulfillJson(route, agentDetailFixture)
return
}
if (path === '/api/v1/agents/iris/activity' && method === 'GET') {
await fulfillJson(route, agentActivityFixture)
return
}
if (path === '/api/v1/agents/iris/summary' && method === 'GET') {
await fulfillJson(route, agentSummaryFixture)
return
}
if (
path === '/api/v1/openclaw/agents/iris/files'
&& method === 'GET'
) {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
await fulfillJson(route, agentFileCollectionFixture)
return
}
const agentFileMatch = path.match(
/^\/api\/v1\/openclaw\/agents\/iris\/files\/([^/]+)$/,
)
if (agentFileMatch && method === 'GET') {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
const fileName = decodeURIComponent(agentFileMatch[1])
const summary = agentFileCollectionFixture.files.find(
file => file.name === fileName,
)
if (!summary) {
await fulfillJson(route, problem('Agent file not found.', 404), 404)
return
}
await fulfillJson(route, {
agentId: 'iris',
...summary,
content: `# ${fileName}\n\nOpenClaw returned the authoritative Iris file.`,
checkedAt: now,
})
return
}
if (
path === '/api/v1/openclaw/agents/iris/workspace'
&& method === 'GET'
) {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
await fulfillJson(route, {
agentId: 'iris',
path: url.searchParams.get('path') ?? '',
parentPath: null,
entries: [],
totalEntries: 0,
offset: Number(url.searchParams.get('offset') ?? 0),
checkedAt: now,
})
return
}
if (path === '/api/v1/docs' && method === 'GET') {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
await fulfillJson(route, [docFixture])
return
}
if (path.startsWith('/api/v1/docs/') && method === 'GET') {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
const requestedPath = decodeURIComponent(
path.slice('/api/v1/docs/'.length),
)
await fulfillJson(
route,
requestedPath === DOC_PATH
? docDetailFixture
: problem('Document not found.', 404),
requestedPath === DOC_PATH ? 200 : 404,
)
return
}
if (path === '/api/v1/memory' && method === 'GET') {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
await fulfillJson(route, [memoryFixture])
return
}
if (path === '/api/v1/memory/search' && method === 'GET') {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
const query = url.searchParams.get('q')?.toLocaleLowerCase() ?? ''
await fulfillJson(
route,
query.includes('agent') ? [memorySearchFixture] : [],
)
return
}
const memoryMatch = path.match(/^\/api\/v1\/memory\/([^/]+)$/)
if (memoryMatch && method === 'GET') {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
const requestedName = decodeURIComponent(memoryMatch[1])
await fulfillJson(
route,
requestedName === MEMORY_NAME
? memoryDetailFixture
: problem('Memory file not found.', 404),
requestedName === MEMORY_NAME ? 200 : 404,
)
return
}
if (path === '/api/v1/incidents' && method === 'GET') {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
await fulfillJson(route, [incidentFixture])
return
}
const incidentMatch = path.match(/^\/api\/v1\/incidents\/([^/]+)$/)
if (incidentMatch && method === 'GET') {
if (role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
const requestedName = decodeURIComponent(incidentMatch[1])
await fulfillJson(
route,
requestedName === INCIDENT_NAME
? incidentDetailFixture
: problem('Incident not found.', 404),
requestedName === INCIDENT_NAME ? 200 : 404,
)
return
}
if (path === '/api/v1/security/status' && method === 'GET') {
await fulfillJson(route, securityStatusFixture)
return
}
if (path === '/api/v1/openclaw/runs' && method === 'GET') {
await fulfillJson(route, {
items: url.searchParams.get('projectId') === PROJECT_ID
? [projectRunFixture]
: [],
nextCursor: null,
checkedAt: now,
})
return
}
if (path === '/api/dashboard/chat/messages' && method === 'GET') {
await fulfillJson(route, [])
return
}
if (path === '/api/v1/chat' && method === 'POST') {
chatRequests.push({
path,
method,
headers: request.headers(),
body: requestBody(request),
})
await fulfillJson(route, {
runtime: 'OpenClaw Protocol v4',
agentId: 'iris',
conversationId: 'e2e-iris-conversation',
content: 'OpenClaw accepted the Iris request.',
runId: 'run-chat-e2e-1',
state: 'running',
operation: {
operationId: 'run-chat-e2e-1',
status: 'running',
revision: 1,
primaryRef: {
type: 'run',
id: 'run-chat-e2e-1',
label: 'Iris agent proposal run',
},
affectedRefs: [],
traceId: null,
},
})
return
}
if (path === '/api/dashboard/notifications/unread-count') {
await fulfillJson(route, { count: 0 })
return
}
if (path === '/api/dashboard/notifications/snapshot') {
await fulfillJson(route, {
notifications: [{
id: '33333333-3333-4333-8333-333333333333',
type: 'task_assigned',
title: 'Inspect E2E backlog task',
message: 'A linked task requires attention.',
forUser: 'bao',
taskId: openTask.id,
isRead: false,
createdAt: now,
}],
unreadCount: 1,
forUser: 'bao',
})
return
}
if (
/^\/api\/dashboard\/notifications\/[^/]+\/read$/.test(path)
&& method === 'PATCH'
) {
const notificationId = path.split('/').at(-2)!
await fulfillJson(route, {
id: notificationId,
type: 'task_assigned',
title: 'Inspect E2E backlog task',
message: 'A linked task requires attention.',
forUser: 'bao',
taskId: openTask.id,
isRead: true,
createdAt: now,
operation: operationResult(
'completed',
{ type: 'notification', id: notificationId, label: 'Inspect E2E backlog task' },
[{ type: 'task', id: openTask.id, label: openTask.title }],
),
})
return
}
if (path === '/api/dashboard/notifications/read-all' && method === 'PATCH') {
await fulfillJson(route, {
marked: 1,
operation: operationResult('completed', {
type: 'notification',
id: '*',
label: 'Benachrichtigungen',
}),
})
return
}
if (path === '/api/v1/telemetry/browser') {
await route.fulfill({ status: 202 })
return
}
if (path === '/api/dashboard/agents') {
await fulfillJson(route, [
{
id: 'iris',
name: 'Iris',
role: 'Chief of Staff',
description: 'Coordinates E2E work.',
tags: ['Orchestration'],
model: 'openai/gpt-5.4',
statusLabel: 'Bereit',
statusKind: 'ready',
statusDetail: 'Mock runtime is ready.',
isActive: false,
progress: null,
currentTask: null,
},
])
return
}
const isProposalRequest = path === '/api/v1/openclaw/agents/create-options'
|| path.startsWith('/api/v1/openclaw/agent-proposals')
if (isProposalRequest && role !== 'owner') {
await fulfillJson(route, problem('Owner access is required.', 403), 403)
return
}
if (path === '/api/v1/openclaw/agents/create-options' && method === 'GET') {
await fulfillJson(route, {
canSubmitProposal: true,
canProvision: true,
state: 'ready',
reason: null,
workspaceRoot: '/managed/agents',
existingAgentIds: ['iris'],
models: [
{
id: 'openai/gpt-5.4',
name: 'GPT-5.4',
provider: 'OpenAI',
available: true,
},
{
id: 'unavailable/model',
name: 'Unavailable',
provider: 'Fixture',
available: false,
},
],
standardFiles: ['AGENTS.md', 'SOUL.md', 'TOOLS.md'],
checkedAt: now,
})
return
}
if (path === '/api/v1/openclaw/agent-proposals' && method === 'GET') {
await fulfillJson(route, {
items: [proposal],
nextCursor: null,
checkedAt: now,
})
return
}
if (path === '/api/v1/openclaw/agent-proposals' && method === 'POST') {
const body = requestBody(request) as Record<string, unknown>
proposalRequests.push({
path,
method,
headers: request.headers(),
body,
})
const fileEntries = body.files && typeof body.files === 'object'
? Object.entries(body.files as Record<string, string>)
: []
const requestedName = String(body.name || 'Unnamed Agent')
proposal = {
...buildProposal('awaiting_approval'),
requestedName,
requestedAgentId: requestedName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''),
role: body.role || null,
description: body.description || null,
model: body.model || null,
files: fileEntries.map(([name, content], index) => ({
name,
contentHash: `e2e-hash-${index}`,
size: content.length,
content,
})),
}
await fulfillJson(route, {
ok: true,
state: 'awaiting_approval',
message: 'Agent-Vorschlag wurde angelegt.',
proposal,
recovery: null,
correlationId: request.headers()['x-correlation-id'] ?? null,
completedAt: now,
}, 201)
return
}
if (path === `/api/v1/openclaw/agent-proposals/${PROPOSAL_ID}` && method === 'GET') {
await fulfillJson(route, proposal)
return
}
const actionMatch = path.match(
new RegExp(`^/api/v1/openclaw/agent-proposals/${PROPOSAL_ID}/(approve|reject|retry)$`),
)
if (actionMatch && method === 'POST') {
const action = actionMatch[1]
const body = requestBody(request)
proposalRequests.push({
path,
method,
headers: request.headers(),
body,
})
if (action === 'approve' || action === 'retry') {
proposal = {
...proposal,
status: 'ready',
approvedBy: 'Bao Owner',
approvedAt: now,
openClawAgentId: proposal.requestedAgentId,
openClawWorkspace: proposal.workspace,
completedAt: now,
updatedAt: now,
revision: Number(proposal.revision) + 1,
}
} else {
const actionBody = body as Record<string, unknown>
proposal = {
...proposal,
status: 'rejected',
rejectedBy: 'Bao Owner',
rejectionReason: actionBody.reason || 'Rejected in E2E',
rejectedAt: now,
completedAt: now,
updatedAt: now,
revision: Number(proposal.revision) + 1,
}
}
await fulfillJson(route, {
ok: true,
state: proposal.status,
message: action === 'reject'
? 'Agent-Vorschlag wurde abgelehnt.'
: 'Agent ist bereit.',
proposal,
recovery: null,
correlationId: request.headers()['x-correlation-id'] ?? null,
completedAt: now,
}, action === 'reject' ? 200 : 202)
return
}
if (path === '/api/v1/tasks/board' && method === 'GET') {
boardRequestUrls.push(url.toString())
const cursor = url.searchParams.get('doneCursor')
if (cursor) {
await fulfillJson(route, {
revision: 'board-r1',
offen: [],
inProgress: [],
review: [],
blocked: [],
done: [secondDoneTask],
nextDoneCursor: null,
hasMoreDone: false,
})
} else {
boardInitialRequestCount += 1
if (boardInitialRequestCount > 1 && options.boardRefreshDelayMs) {
await new Promise(resolve => setTimeout(resolve, options.boardRefreshDelayMs))
}
const currentOpenTask = {
...openTask,
title: boardInitialRequestCount > 1 && options.boardRefreshTitle
? options.boardRefreshTitle
: openTask.title,
state: openTaskState,
}
const columns = {
offen: openTaskState === 'Backlog' ? [currentOpenTask] : [],
inProgress: openTaskState === 'In progress'
? [currentOpenTask, progressTask]
: [progressTask],
review: openTaskState === 'Review' ? [currentOpenTask] : [],
blocked: openTaskState === 'Blocked' ? [currentOpenTask] : [],
done: openTaskState === 'Done'
? [currentOpenTask, firstDoneTask]
: [firstDoneTask],
}
await fulfillJson(route, {
revision: boardInitialRequestCount > 1 && options.boardRefreshTitle
? 'board-r2'
: 'board-r1',
...columns,
nextDoneCursor: 'done-page-2',
hasMoreDone: true,
})
}
return
}
if (path === `/api/dashboard/tasks/${openTask.id}` && method === 'GET') {
await fulfillJson(route, {
...openTask,
state: openTaskState,
})
return
}
if (path === `/api/dashboard/tasks/${openTask.id}/children` && method === 'GET') {
await fulfillJson(route, [])
return
}
if (path === `/api/dashboard/tasks/${openTask.id}/activity` && method === 'GET') {
await fulfillJson(route, [])
return
}
const boardCardMatch = path.match(/^\/api\/v1\/tasks\/([^/]+)\/board-card$/)
if (boardCardMatch && method === 'GET') {
const taskId = decodeURIComponent(boardCardMatch[1])
if (taskId !== openTask.id) {
await fulfillJson(route, problem('Task not found.', 404), 404)
return
}
await fulfillJson(route, {
...openTask,
state: openTaskState,
updatedAt: now,
})
return
}
const moveTaskMatch = path.match(/^\/api\/dashboard\/tasks\/([^/]+)\/move$/)
if (moveTaskMatch && method === 'PATCH') {
const body = requestBody(request) as Record<string, unknown>
taskMutationRequests.push({
path,
method,
headers: request.headers(),
body,
})
const requestedState = String(body.state || 'Backlog')
const canonicalStates: Record<string, string> = {
offen: 'Backlog',
inProgress: 'In progress',
review: 'Review',
blocked: 'Blocked',
done: 'Done',
}
if (decodeURIComponent(moveTaskMatch[1]) === openTask.id) {
openTaskState = canonicalStates[requestedState] ?? requestedState
}
await fulfillJson(route, {
...openTask,
state: openTaskState,
updatedAt: now,
operation: operationResult('state_updated', {
type: 'task',
id: openTask.id,
label: openTask.title,
}),
})
return
}
if (path === '/api/dashboard/tasks/agent-overview') {
await fulfillJson(route, {
waitingForBao: [],
waitingForIris: [],
waitingForOthers: [],
staleTasks: [],
staleThreshold: '02:00:00',
})
return
}
if (path === '/api/dashboard/tasks') {
await fulfillJson(route, [openTask, progressTask, firstDoneTask])
return
}
if (path === '/api/dashboard/tasks/board') {
await fulfillJson(route, {
offen: [openTask],
inProgress: [progressTask],
review: [],
blocked: [],
done: [firstDoneTask],
})
return
}
if (path === '/api/dashboard/status') {
await fulfillJson(route, {
gatewayOk: true,
irisStatus: 'Ready',
activeAgents: 1,
pendingTasks: 2,
})
return
}
if (path === '/api/dashboard/operations' || path === '/api/dashboard/queue') {
await fulfillJson(route, [])
return
}
await fulfillJson(route, problem(`No E2E fixture for ${method} ${path}.`, 404), 404)
})
return {
proposalRequests,
boardRequestUrls,
domainEventRequests,
taskMutationRequests,
chatRequests,
get resyncResponseCount() {
return resyncResponseCount
},
get openClawOverviewRequestCount() {
return openClawOverviewRequestCount
},
get proposal() {
return proposal
},
}
}