import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { API_OPERATIONS, capabilities, createOpenApiDocument } from '../src/web/api-catalog.js'; describe('agent API discovery', () => { it('publishes every operation under a unique method and path', () => { const keys = API_OPERATIONS.map((operation) => `${operation.method} ${operation.path}`); expect(new Set(keys).size).toBe(keys.length); expect(keys).toContain('GET /api/v1/overview'); expect(keys).toContain('POST /api/v1/control/start'); expect(keys).toContain('POST /api/v1/control/stop'); expect(keys).toContain('POST /api/v1/jobs'); expect(keys).toContain('GET /api/events/stream'); }); it('cannot drift from the Express route modules', () => { const routesDirectory = path.resolve(process.cwd(), 'src/web/routes'); const implemented = fs.readdirSync(routesDirectory) .filter((file) => file.endsWith('.ts')) .flatMap((file) => { const source = fs.readFileSync(path.join(routesDirectory, file), 'utf8'); return [...source.matchAll(/router\.(get|post|put|delete|patch)\('([^']+)'/g)].map((match) => { const routePath = match[2] === '/healthz' ? match[2] : `/api${match[2]}`; return `${match[1].toUpperCase()} ${routePath.replace(/:([A-Za-z0-9_]+)/g, '{$1}')}`; }); }); const catalogued = API_OPERATIONS.map((item) => `${item.method} ${item.path}`); expect([...new Set(catalogued)].sort()).toEqual([...new Set(implemented)].sort()); }); it('generates OpenAPI with safety metadata and schemas for versioned operations', () => { const document = createOpenApiDocument() as { openapi: string; paths: Record>> }; expect(document.openapi).toBe('3.1.0'); expect(document.paths['/healthz'].get.security).toEqual([]); expect(document.paths['/api/v1/overview'].get['x-access']).toBe('read'); expect(document.paths['/api/v1/control/start'].post['x-access']).toBe('control'); expect(document.paths['/api/v1/control/start'].post['x-mutates']).toBe(true); expect(document.paths['/api/v1/jobs'].post.requestBody).toBeDefined(); expect((document.paths['/api/v1/jobs'].post.responses as Record)['202']).toBeDefined(); }); it('returns a compact capability catalogue for agents', () => { const result = capabilities(); expect(result.authentication.agentScopes).toEqual(['read', 'control', 'admin']); expect(result.operations.find((item) => item.operationId === 'sendReply')).toMatchObject({ access: 'admin', mutates: true, }); }); });