import { expect, test } from '@playwright/test' import { execFile } from 'node:child_process' import { promisify } from 'node:util' import path from 'node:path' import { API_KEY_HEADER, createTestAgent, deleteTestAgent, createTestTask, deleteTestTask } from './helpers' const execFileAsync = promisify(execFile) const CLI = path.resolve('scripts/mc-cli.cjs') const BASE_URL = process.env.E2E_BASE_URL || 'http://127.0.0.1:3005' const API_KEY = 'test-api-key-e2e-12345' /** Run mc-cli command via execFile (no shell) and return parsed JSON output */ async function mc(...args: string[]): Promise<{ stdout: string; parsed: any; exitCode: number }> { try { const { stdout } = await execFileAsync('node', [CLI, ...args, '--json', '--url', BASE_URL, '--api-key', API_KEY], { timeout: 15000, env: { ...process.env, MC_URL: BASE_URL, MC_API_KEY: API_KEY }, }) let parsed: any try { parsed = JSON.parse(stdout) } catch { parsed = { raw: stdout } } return { stdout, parsed, exitCode: 0 } } catch (err: any) { const stdout = err.stdout || '' let parsed: any try { parsed = JSON.parse(stdout) } catch { parsed = { raw: stdout, stderr: err.stderr } } return { stdout, parsed, exitCode: err.code ?? 1 } } } test.describe('CLI Integration', () => { // --- Help & Usage --- test('--help shows usage and exits 0', async () => { const { stdout, exitCode } = await mc('--help') expect(exitCode).toBe(0) expect(stdout).toContain('Mission Control CLI') expect(stdout).toContain('agents') expect(stdout).toContain('tasks') }) test('unknown group exits 2 with error', async () => { const { exitCode } = await mc('nonexistent', 'action') expect(exitCode).toBe(2) }) test('missing required flag exits 2 with error message', async () => { const { exitCode, parsed } = await mc('agents', 'get') expect(exitCode).toBe(2) expect(parsed.error).toContain('--id') }) // --- Status --- test('status health returns healthy', async () => { const { parsed, exitCode } = await mc('status', 'health') expect(exitCode).toBe(0) expect(parsed.data?.status || parsed.status).toBeDefined() }) test('status overview returns system info', async () => { const { parsed, exitCode } = await mc('status', 'overview') expect(exitCode).toBe(0) }) // --- Agents CRUD --- test.describe('agents', () => { const agentIds: number[] = [] test.afterEach(async ({ request }) => { for (const id of agentIds.splice(0)) { await deleteTestAgent(request, id).catch(() => {}) } }) test('list returns array', async () => { const { parsed, exitCode } = await mc('agents', 'list') expect(exitCode).toBe(0) const data = parsed.data || parsed expect(data).toBeDefined() }) test('get + heartbeat lifecycle', async ({ request }) => { const agent = await createTestAgent(request) agentIds.push(agent.id) // Get via CLI const { parsed: getResult, exitCode: getCode } = await mc('agents', 'get', '--id', String(agent.id)) expect(getCode).toBe(0) const agentData = getResult.data?.agent || getResult.data || getResult expect(agentData).toBeDefined() // Heartbeat via CLI const { exitCode: hbCode } = await mc('agents', 'heartbeat', '--id', String(agent.id)) expect(hbCode).toBe(0) }) test('memory set and get work', async ({ request }) => { const agent = await createTestAgent(request) agentIds.push(agent.id) // Set memory — may succeed or fail depending on workspace state const { exitCode: setCode } = await mc('agents', 'memory', 'set', '--id', String(agent.id), '--content', 'CLI test memory') expect([0, 2, 6]).toContain(setCode) // Get memory const { exitCode: getCode } = await mc('agents', 'memory', 'get', '--id', String(agent.id)) expect([0, 2, 6]).toContain(getCode) }) test('attribution returns response', async ({ request }) => { const agent = await createTestAgent(request) agentIds.push(agent.id) // Attribution may return 403 for test API key depending on auth scope — accept 0 or 4 const { exitCode } = await mc('agents', 'attribution', '--id', String(agent.id), '--hours', '1') expect([0, 4]).toContain(exitCode) }) }) // --- Tasks --- test.describe('tasks', () => { const taskIds: number[] = [] test.afterEach(async ({ request }) => { for (const id of taskIds.splice(0)) { await deleteTestTask(request, id).catch(() => {}) } }) test('list returns data', async () => { const { exitCode } = await mc('tasks', 'list') expect(exitCode).toBe(0) }) test('queue returns response', async () => { const { exitCode } = await mc('tasks', 'queue', '--agent', 'e2e-test-agent') expect(exitCode).toBe(0) }) test('comments list/add lifecycle', async ({ request }) => { const task = await createTestTask(request) taskIds.push(task.id) // Add comment via CLI const { exitCode: addCode } = await mc('tasks', 'comments', 'add', '--id', String(task.id), '--content', 'CLI comment test') expect(addCode).toBe(0) // List comments via CLI const { parsed, exitCode: listCode } = await mc('tasks', 'comments', 'list', '--id', String(task.id)) expect(listCode).toBe(0) const comments = parsed.data?.comments || parsed.comments || [] expect(comments.length).toBeGreaterThanOrEqual(1) }) }) // --- Sessions --- test('sessions list returns response', async () => { // Sessions endpoint may return various codes depending on gateway state const { exitCode } = await mc('sessions', 'list') expect([0, 2, 4, 6]).toContain(exitCode) }) // --- Tokens --- test('tokens stats returns data', async () => { const { exitCode } = await mc('tokens', 'stats') expect(exitCode).toBe(0) }) test('tokens by-agent returns data', async () => { const { exitCode } = await mc('tokens', 'by-agent', '--days', '7') expect(exitCode).toBe(0) }) // --- Skills --- test('skills list returns data', async () => { const { exitCode } = await mc('skills', 'list') expect(exitCode).toBe(0) }) // --- Cron --- test('cron list returns response', async () => { // Cron may return error in test mode — accept 0, 2, or 6 const { exitCode } = await mc('cron', 'list') expect([0, 2, 6]).toContain(exitCode) }) // --- Connect --- test('connect list returns data', async () => { const { exitCode } = await mc('connect', 'list') expect(exitCode).toBe(0) }) // --- Raw passthrough --- test('raw GET /api/status works', async () => { const { exitCode } = await mc('raw', '--method', 'GET', '--path', '/api/status?action=health') expect(exitCode).toBe(0) }) })