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/main → client-proxies → util/test
→ tests) 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
Copyright: © Arda Systems 2025-2026, All rights reserved