/** * OpenAPI 3.1 document validity (build-plan §5). * * Asserts the structural invariants of `buildOpenApiDocument`: 3.1 version, * one `POST /tools/{name}` per tool carrying `x-required-scope`, and a * `bearerAuth` security scheme present (design.md §2.5 — every tool path * requires a token). */ import { describe, it, expect } from 'vitest'; import { ToolRegistry, defineTool } from './registry.js'; import { buildOpenApiDocument } from './openapi.js'; import type { Scope } from './types.js'; function tool(name: string, scope: Scope, tier: 'ops' | 'finance') { return defineTool({ name, description: `desc ${name}`, tier, requiredScope: scope, inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], }, handler: async () => ({}), }); } function doc() { const registry = new ToolRegistry(); registry.register(tool('lookup_thing', 'ops:read', 'ops')); registry.register(tool('lookup_payment', 'finance:read', 'finance')); return buildOpenApiDocument(registry, { info: { title: 'Test', version: '1.0.0' }, servers: [{ url: 'http://localhost:8081' }], }); } describe('buildOpenApiDocument', () => { it('declares OpenAPI 3.1.0', () => { expect(doc().openapi).toBe('3.1.0'); }); it('carries info and servers', () => { const d = doc(); expect(d.info.title).toBe('Test'); expect(d.servers[0]!.url).toBe('http://localhost:8081'); }); it('emits exactly one POST /tools/{name} per tool', () => { const d = doc(); expect(Object.keys(d.paths).sort()).toEqual(['/tools/lookup_payment', '/tools/lookup_thing']); for (const path of Object.values(d.paths)) { expect(path.post).toBeDefined(); } }); it('annotates each operation with x-required-scope and a JSON request body', () => { const op = doc().paths['/tools/lookup_payment']!.post; expect(op['x-required-scope']).toBe('finance:read'); expect(op.requestBody.required).toBe(true); expect(op.requestBody.content['application/json'].schema).toBeDefined(); }); it('defines the bearerAuth security scheme and applies it document-wide', () => { const d = doc(); expect(d.components.securitySchemes.bearerAuth.scheme).toBe('bearer'); expect(d.security).toEqual([{ bearerAuth: [] }]); }); it('every operation requires the bearer token (no unauthenticated tool path)', () => { for (const path of Object.values(doc().paths)) { expect(path.post.security).toContainEqual({ bearerAuth: [] }); } }); it('produces a scope-filtered document when given a filtered registry', () => { const filtered = new ToolRegistry(); filtered.register(tool('lookup_thing', 'ops:read', 'ops')); const d = buildOpenApiDocument(filtered, { info: { title: 'Ops', version: '1.0.0' }, servers: [{ url: 'http://x' }], }); expect(Object.keys(d.paths)).toEqual(['/tools/lookup_thing']); }); });