Skip to content

lib/openapi.ts's client-side spec parsing/transform logic has zero test coverage #8389

Description

@JSONbored

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

  • apps/loopover-ui/src/lib/openapi.test.ts with the coverage listed above, built against a local
    fixture spec (not the real public/openapi.json).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions