Context
apps/loopover-ui/src/lib/openapi.ts builds the client-side OpenApiSpec model consumed by every
/api/* docs page (apps/loopover-ui/src/routes/api.$op.tsx, api.index.tsx, api.tsx, and
components/site/api/api-sidebar.tsx / try-it.tsx) from the committed public/openapi.json. It has
real, non-trivial parsing/transform logic:
build() walks every path/method in the spec, derives a URL-safe id via slugify(), resolves
requiresAuthentication(), merges declared vs. path-inferred parameters via extractPathParams(),
and extracts a first example per response via extractExample(); it then groups operations into
tags, including a fallback branch (lines 150-155) for any operation whose tag was never declared in
raw.tags at all.
extractPathParams() derives {param} path parameters from the URL template and excludes any
that the spec already declares explicitly (declaredKeys), so a doc author can override the
auto-derived description without ending up with a duplicate parameter entry.
extractExample() prefers a raw example, falls back to the first entry of examples, and returns
undefined when the response has no application/json content at all.
slugify() is the sole thing that turns e.g. GET /v1/repos/{owner}/{repo} into the id
get-v1-repos-owner-repo that every operation-detail-page URL (/api/$op) and prev/next link is
keyed on.
normalizeServers() always overwrites the first server's url with the live getApiOrigin() (so a
self-hosted deployment's generated curl/fetch/Python snippets point at its own API, not whatever
origin was baked into the committed spec at generation time), and synthesizes a single "Production"
server entry when the spec declares none.
generateCurl/generateFetch/generatePython each branch on op.requiresAuth and on whether the
method takes a body (method !== "get" && method !== "delete").
None of this has a test — there is no openapi.test.ts anywhere in the repo, and no existing test file
references openapi, findOperation, generateCurl, generateFetch, or generatePython. This is
the exact same class of gap already confirmed and fixed once in this repo for the server-side spec
writer (closed issue #7770, "preserveExistingObjectOrder() in write-ui-openapi.ts has zero direct
unit tests despite gating every PR touching the API surface") — this is the client-side reader with
the same gap, on the file every /api/* doc page and code-sample generator actually runs at request
time.
Requirements
- Add
apps/loopover-ui/src/lib/openapi.test.ts covering, using a small hand-built RawOpenApiSpec
fixture (do not depend on the real committed public/openapi.json's current contents, since that
drifts) passed through the same shape build() consumes:
slugify(): strips {}, collapses non-alphanumeric runs to a single -, and trims leading/
trailing - (test via the operations it produces, since slugify itself isn't exported — assert
on op.id for a path containing {owner}/{repo}-style segments).
extractPathParams(): a {param} in the path with no matching declared parameter is auto-added
as required: true/in: "path"; a {param} that is already declared is not duplicated.
extractExample(): prefers content["application/json"].example when present; falls back to the
first value of .examples when example is absent; returns undefined when there's no
application/json content entry at all, and when responses is empty.
- Tag grouping: an operation whose tag was never declared in
raw.tags still gets its own tag group
(the fallback branch), and its operations are not duplicated into any other group.
requiresAuthentication(): true only when security is a non-empty array; false for
security: [], security: undefined, and a non-array value.
normalizeServers(): with zero declared servers, synthesizes one "Production" entry pointing at
getApiOrigin(); with one or more declared servers, the first server's url is overwritten
with getApiOrigin() while any later servers keep their original url.
findOperation(id): returns the matching operation, and undefined for an id that doesn't exist.
generateCurl/generateFetch/generatePython: each includes an Authorization
header/placeholder only when op.requiresAuth; each includes a body placeholder only for methods
other than get/delete; generateCurl uses the real token argument when provided and falls
back to the literal $LOOPOVER_TOKEN placeholder when it isn't.
Deliverables
Test Coverage Requirements
apps/** is outside this repo's Codecov coverage.include (vitest.config.ts / codecov.yml's
ignore: ["apps/**", ...]), so this change owes no Codecov patch-coverage percentage. Aim for genuine
branch coverage of every conditional listed above (both the declared-tag and fallback-tag branches,
both the example-present and examples-fallback branches, both the zero-servers and
existing-servers branches, both the auth and no-auth branches in each generate* function). Run
npx vitest run apps/loopover-ui/src/lib/openapi.test.ts while iterating, and npm run ui:test
before opening the PR.
Expected Outcome
The client-side OpenAPI parsing/transform logic that every /api/* doc page, prev/next link, and
generated code sample depends on has a real regression suite, mirroring the fix already shipped for
the server-side spec writer in closed issue #7770. A future change to slugify, tag grouping, or the
generate* functions that broke operation ids or leaked/dropped an auth header would fail a test
instead of only surfacing as a broken doc page in production.
Links & Resources
Context
apps/loopover-ui/src/lib/openapi.tsbuilds the client-sideOpenApiSpecmodel consumed by every/api/*docs page (apps/loopover-ui/src/routes/api.$op.tsx,api.index.tsx,api.tsx, andcomponents/site/api/api-sidebar.tsx/try-it.tsx) from the committedpublic/openapi.json. It hasreal, non-trivial parsing/transform logic:
build()walks everypath/methodin the spec, derives a URL-safeidviaslugify(), resolvesrequiresAuthentication(), merges declared vs. path-inferred parameters viaextractPathParams(),and extracts a first example per response via
extractExample(); it then groups operations intotags, including a fallback branch (lines 150-155) for any operation whose tag was never declared inraw.tagsat all.extractPathParams()derives{param}path parameters from the URL template and excludes anythat the spec already declares explicitly (
declaredKeys), so a doc author can override theauto-derived description without ending up with a duplicate parameter entry.
extractExample()prefers a rawexample, falls back to the first entry ofexamples, and returnsundefinedwhen the response has noapplication/jsoncontent at all.slugify()is the sole thing that turns e.g.GET /v1/repos/{owner}/{repo}into the idget-v1-repos-owner-repothat every operation-detail-page URL (/api/$op) and prev/next link iskeyed on.
normalizeServers()always overwrites the first server'surlwith the livegetApiOrigin()(so aself-hosted deployment's generated
curl/fetch/Python snippets point at its own API, not whateverorigin was baked into the committed spec at generation time), and synthesizes a single "Production"
server entry when the spec declares none.
generateCurl/generateFetch/generatePythoneach branch onop.requiresAuthand on whether themethod takes a body (
method !== "get" && method !== "delete").None of this has a test — there is no
openapi.test.tsanywhere in the repo, and no existing test filereferences
openapi,findOperation,generateCurl,generateFetch, orgeneratePython. This isthe exact same class of gap already confirmed and fixed once in this repo for the server-side spec
writer (closed issue #7770, "
preserveExistingObjectOrder()in write-ui-openapi.ts has zero directunit tests despite gating every PR touching the API surface") — this is the client-side reader with
the same gap, on the file every
/api/*doc page and code-sample generator actually runs at requesttime.
Requirements
apps/loopover-ui/src/lib/openapi.test.tscovering, using a small hand-builtRawOpenApiSpecfixture (do not depend on the real committed
public/openapi.json's current contents, since thatdrifts) passed through the same shape
build()consumes:slugify(): strips{}, collapses non-alphanumeric runs to a single-, and trims leading/trailing
-(test via the operations it produces, sinceslugifyitself isn't exported — asserton
op.idfor a path containing{owner}/{repo}-style segments).extractPathParams(): a{param}in the path with no matching declared parameter is auto-addedas
required: true/in: "path"; a{param}that is already declared is not duplicated.extractExample(): preferscontent["application/json"].examplewhen present; falls back to thefirst value of
.exampleswhenexampleis absent; returnsundefinedwhen there's noapplication/jsoncontent entry at all, and whenresponsesis empty.raw.tagsstill gets its own tag group(the fallback branch), and its operations are not duplicated into any other group.
requiresAuthentication():trueonly whensecurityis a non-empty array;falseforsecurity: [],security: undefined, and a non-array value.normalizeServers(): with zero declared servers, synthesizes one"Production"entry pointing atgetApiOrigin(); with one or more declared servers, the first server'surlis overwrittenwith
getApiOrigin()while any later servers keep their originalurl.findOperation(id): returns the matching operation, andundefinedfor an id that doesn't exist.generateCurl/generateFetch/generatePython: each includes anAuthorizationheader/placeholder only when
op.requiresAuth; each includes a body placeholder only for methodsother than
get/delete;generateCurluses the realtokenargument when provided and fallsback to the literal
$LOOPOVER_TOKENplaceholder when it isn't.Deliverables
apps/loopover-ui/src/lib/openapi.test.tswith the coverage listed above, built against a localfixture spec (not the real
public/openapi.json).Test Coverage Requirements
apps/**is outside this repo's Codecovcoverage.include(vitest.config.ts/codecov.yml'signore: ["apps/**", ...]), so this change owes no Codecov patch-coverage percentage. Aim for genuinebranch coverage of every conditional listed above (both the declared-tag and fallback-tag branches,
both the example-present and examples-fallback branches, both the zero-servers and
existing-servers branches, both the auth and no-auth branches in each
generate*function). Runnpx vitest run apps/loopover-ui/src/lib/openapi.test.tswhile iterating, andnpm run ui:testbefore opening the PR.
Expected Outcome
The client-side OpenAPI parsing/transform logic that every
/api/*doc page, prev/next link, andgenerated code sample depends on has a real regression suite, mirroring the fix already shipped for
the server-side spec writer in closed issue #7770. A future change to
slugify, tag grouping, or thegenerate*functions that broke operation ids or leaked/dropped an auth header would fail a testinstead of only surfacing as a broken doc page in production.
Links & Resources
apps/loopover-ui/src/lib/openapi.ts(the module to test)apps/loopover-ui/src/routes/api.$op.tsx,api.index.tsx,api.tsx(the real call sites)apps/loopover-ui/src/components/site/api/try-it.tsx/try-it.test.ts(a sibling API-docs filethat already has a real test, for the local pattern)
write-ui-openapi.ts'spreserveExistingObjectOrder()