Files
OmniRoute/tests/unit/cli-openapi-endpoints-shape-10082.test.ts
Amar Tinawi 4a7e8ddc16 fix(cli): openapi endpoints/paths/validate accept the served catalog shape (#10091)
GET /api/openapi/spec answers with a compact catalog
({ info, servers, tags, endpoints[], schemas }) rather than an OpenAPI document,
while dist/docs/openapi.yaml is a real spec. The CLI only read spec.paths, so
against a live server 'openapi endpoints' and 'openapi paths' printed nothing
and 'openapi validate' reported 'missing openapi/swagger version field' — with
318 endpoints sitting in spec.endpoints.

Normalize both shapes through extractEndpoints()/extractPaths() and let
validateBasic() accept a catalog that carries endpoints[] instead of a version
field. Path Item members that are not operations (parameters, summary,
description, servers, $ref) are no longer emitted as fake operations.

Closes #10082
2026-08-13 04:43:02 -03:00

89 lines
3.2 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
// #10082 — `GET /api/openapi/spec` answers with a compact catalog
// ({ info, servers, tags, endpoints[], schemas }), not an OpenAPI document.
// The CLI read `spec.paths`, so `openapi endpoints` and `openapi paths` printed
// "(empty)" against a live server and `openapi validate` reported the API as
// invalid, even though 318 endpoints were sitting in `spec.endpoints`.
const { extractEndpoints, extractPaths } = await import("../../bin/cli/commands/openapi.mjs");
const CATALOG_SHAPE = {
info: { title: "OmniRoute API", version: "3.8.50" },
servers: [{ url: "http://localhost:20128" }],
tags: ["Playground"],
endpoints: [
{
method: "POST",
path: "/api/playground/improve-prompt",
tags: ["Playground"],
summary: "Improve prompt via LLM",
},
{ method: "GET", path: "/api/providers", summary: "List provider connections" },
{ path: "/api/health", description: "Health probe" },
],
schemas: {},
};
const SPEC_SHAPE = {
openapi: "3.1.0",
info: { title: "OmniRoute API", version: "3.8.50" },
paths: {
"/api/providers": {
// Path Item fields that sit alongside operations must not be treated as one.
parameters: [{ name: "limit", in: "query" }],
summary: "Provider connections",
get: { summary: "List provider connections", operationId: "listProviders" },
post: { description: "Create connection", operationId: "createProvider" },
},
"/api/health": { get: { summary: "Health probe" } },
},
};
test("extractEndpoints reads the compact catalog shape served by /api/openapi/spec", () => {
const rows = extractEndpoints(CATALOG_SHAPE);
assert.equal(rows.length, 3);
assert.deepEqual(rows[0], {
method: "POST",
path: "/api/playground/improve-prompt",
summary: "Improve prompt via LLM",
operationId: undefined,
});
// description is used when summary is absent, and method defaults to GET.
assert.deepEqual(rows[2], {
method: "GET",
path: "/api/health",
summary: "Health probe",
operationId: undefined,
});
});
test("extractEndpoints still reads a real OpenAPI document", () => {
const rows = extractEndpoints(SPEC_SHAPE);
assert.equal(rows.length, 3);
assert.ok(rows.every((r) => r.method !== "PARAMETERS" && r.method !== "SUMMARY"));
const get = rows.find((r) => r.path === "/api/providers" && r.method === "GET");
assert.equal(get?.operationId, "listProviders");
assert.equal(get?.summary, "List provider connections");
});
test("extractPaths returns sorted, de-duplicated paths for both shapes", () => {
assert.deepEqual(extractPaths(CATALOG_SHAPE), [
"/api/health",
"/api/playground/improve-prompt",
"/api/providers",
]);
// /api/providers has two operations but must appear once.
assert.deepEqual(extractPaths(SPEC_SHAPE), ["/api/health", "/api/providers"]);
});
test("extractEndpoints degrades to an empty list on junk instead of throwing", () => {
assert.deepEqual(extractEndpoints(null), []);
assert.deepEqual(extractEndpoints({}), []);
assert.deepEqual(extractEndpoints({ info: {}, endpoints: "nope" }), []);
assert.deepEqual(extractEndpoints({ paths: { "/a": null } }), []);
});