53 lines
2.6 KiB
TypeScript
53 lines
2.6 KiB
TypeScript
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<string, Record<string, Record<string, unknown>>> };
|
|
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<string, unknown>)['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,
|
|
});
|
|
});
|
|
});
|