Skip to content

Operations API Tests — Design Log

The load-bearing decisions, condensed. The full round-by-round record (18 questions) lives in the work-in-progress decision log.

DL-1 — Co-locate the suite in the operations repo

Section titled “DL-1 — Co-locate the suite in the operations repo”

Decision: the suite lives at operations/api-tests, not in a separate repo or under src/test. Alternatives: a standalone api-test-style repo; a Gradle source set. Rationale: one version per repo means a deployed tag identifies the exact matching suite revision, and API-affecting fixes travel with the tests. The subproject is kept isolated so it never touches the Gradle build and can be separated out later. Scope note: everything lives in operations now to bound this proof-of-concept; once the structure is proven, the reusable parts move to shared repositories, and a system-api-tests repo imports them to add cross-component scenarios.

DL-2 — TypeScript + Vitest + openapi-typescript + jose

Section titled “DL-2 — TypeScript + Vitest + openapi-typescript + jose”

Decision: strict TypeScript (ESM, Node 22), Vitest 4, openapi-typescript for wire types, ESLint flat + Prettier, jose for JWT. Alternatives: Bruno (the incumbent), Playwright’s API testing, supertest, hand-written types. Rationale: a full programming language (rather than a test-oriented framework) gives full control of control flow and concurrency and first-class abstraction and composition of reusable elements; contract-accurate typing from the component’s own OpenAPI makes the compiler the first line of defense. TypeScript specifically for closeness to the frontend stack (the arda-frontend-app / ux-prototype baseline): a low learning curve for engineers already in TypeScript, the suite doubles as client API-usage examples, and infrastructure can be shared with frontend development.

DL-3 — Generate proxies per run; do not commit them

Section titled “DL-3 — Generate proxies per run; do not commit them”

Decision: generated/ is a git-ignored build artifact regenerated against the SUT under test; make verify runs generate → tsc → test. Alternatives: commit the generated output as a “reified control point” (the earlier framing); commit spec snapshots. Rationale: in every workflow the proxies are generated against a live SUT, so compile-after-generate is the contract check — a committed snapshot compiles green while the live API drifts, masking exactly the drift the check exists to catch. The explicit tsc step is required because Vitest transpiles without type-checking. Cost is mitigated by a canonicalized spec cache. Accepted trade-off: no offline typecheck of proxy-facing code. Supersedes the committed-baseline framing.

DL-4 — Four-area layered package DAG, designed for reuse

Section titled “DL-4 — Four-area layered package DAG, designed for reuse”

Decision: four workspace packages (util/mainclient-proxiesutil/testtests) with a one-directional DAG enforced by workspace boundaries, project references, and dependency-cruiser. Alternatives: a single package with folder conventions. Rationale: the boundaries make illegal dependencies a red build, not a review catch, and make promotion of the lower layers to a shared library a move-and-publish — the basis for replicating the pattern across components.

DL-5 — Auth behind a TokenProvider interface

Section titled “DL-5 — Auth behind a TokenProvider interface”

Decision: client-proxies depends only on a TokenProvider interface; util/test injects a concrete provider (static key, no-auth, Cognito, local-JWT), selected per run by --auth. Alternatives: separate api-key and OAuth clients; bake auth into the proxies. Rationale: one test runs under either auth with no code change, auth-specific tests gate on an axis parallel to capabilities, and the proxies stay reusable by any component with its own provider.

DL-6 — SUT registry as a prominent typed file of op:// references

Section titled “DL-6 — SUT registry as a prominent typed file of op:// references”

Decision: one committed sut-config.ts declares every target with capabilities, auth mode, and OAM coordinates, holding 1Password references only. Alternatives: per-environment .env files; config buried in a util package. Rationale: secrets never enter the repo, the config is type-checked and discoverable, and capabilities declared here drive test gating.

DL-7 — Categories by filename suffix; quarantine as typed governance

Section titled “DL-7 — Categories by filename suffix; quarantine as typed governance”

Decision: cumulative category suffixes (probe ⊆ sanity ⊆ acceptance ⊆ all) plus orthogonal sets; quarantine is a typed option with a compile-required ticket, an expiry, and separate existence/failure budgets. Alternatives: tag metadata and runtime filtering; ad-hoc skips. Rationale: selection is a glob concern needing no multi-tagging (tests are leaves), and quarantine keeps known-bad tests visible and time-bounded rather than silently skipped.

DL-8 — Local OAuth via a self-minted JWT against Caddy JWKS

Section titled “DL-8 — Local OAuth via a self-minted JWT against Caddy JWKS”

Decision: local runs mint an RS256 JWT validated against the Caddy-served public JWKS in the chart; the private key is resolved at run time from op://Arda-LocalOAM and never committed. Alternatives: stand up Cognito locally; commit a throwaway key (as api-test does). Rationale: a hermetic local OAuth path with no Cognito dependency, keeping only the public JWKS in the repo.


Copyright: (c) Arda Systems 2025-2026, All rights reserved