Skip to content

Karina/1077 cloudflareworkermock - #1088

Draft
karinamzalez wants to merge 5 commits into
mainfrom
karina/1077-cloudflareworkermock
Draft

Karina/1077 cloudflareworkermock#1088
karinamzalez wants to merge 5 commits into
mainfrom
karina/1077-cloudflareworkermock

Conversation

@karinamzalez

@karinamzalez karinamzalez commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

This is a timeboxed experiment, not a feature. It may close unmerged. The point is
to find out what ADR Option 3A would actually cost.

Changes proposed

New mock-api/ workspace package: a small Cloudflare Worker that serves fake grant
opportunity data at one real URL.

  • mock-api/ a Worker serving three opportunity endpoints (list, detail, search)
    for spec versions 0.1.0–0.4.0, with CORS. 11 fixed records, same answer every time.
  • Fixture ported from the earlier MSW spike ([POC] #1034 Test A: client-side MSW mock playground #1049) unchanged. Handlers rewritten
    to drop MSW.
  • Conformance test checks every record against the schemas the docs site
    generates, so the mock can't silently drift from the protocol.
  • .github/workflows/ci-mock-api.yml runs checks and tests on PRs; deploys by
    hand (workflow_dispatch) to keep the experiment cheap.
  • mock-api/DEPLOYMENT.md what deploying costs, and what's still missing.

Nothing outside mock-api/ changes behaviour. The only shared edits are adding the
package to the workspace and a ci:mock-api script.

Context for reviewers

Not done yet: nothing is deployed yet (WIP). The
runbook to deploy is in mock-api/DEPLOYMENT.md.

Three bugs the conformance test found. Two are not the mock's fault and are worth
their own issues:

  1. Versioned schemas are wrong for some models. ApplicantType, OppStatus, and
    three others are only written into v0.4.0/, though the changelog says they were
    added in 0.2.0. Cause is in website/src/lib/schema/version-generator.ts, it skips
    a check when a version has no changelog entry. This also means the docs site's own
    versioned schema pages are wrong for those models.
  2. isoTime doesn't match its own format. It says format: time but describes
    itself as HH:mm:ss with no timezone, and publishes 17:00:00 as its example. JSON
    Schema's time requires a timezone, so any strict validator rejects the protocol's
    own example.
  3. The mock serves invalid competition data. 3 of 11 records leave out fields
    CompetitionBase requires. Inherited from [POC] #1034 Test A: client-side MSW mock playground #1049 on purpose. Pinned by a test that
    will fail if someone fixes the fixture, so it can't be forgotten.

How to try it:

pnpm install && pnpm --filter @common-grants/mock-api run dev
curl -s http://localhost:8787/v0.3.0/common-grants/opportunities | jq '.items | length'

How it was verified: 232 tests pass. Endpoints, CORS preflight, and error shapes
checked by hand against wrangler dev. The deploy job's smoke test was run against a
local Worker to confirm it works. Reviewed by subagent at each step; the review caught
a masked deploy failure (tee swallowing wrangler's exit code), a concurrency group
that allowed the race it was meant to stop, and an audit step that could be skipped on
a deploying run.

Additional information

Endpoints, CORS preflight, and the unsupported-version error, against wrangler dev:
download

Full package CI:
download-1

…Worker

First ticket of the timeboxed #1077 experiment measuring the surface area of a
standalone Cloudflare Worker mock (ADR Option 3A). Adds `mock-api` as a literal
pnpm workspace member with the repo's standard TS package layout (catalog pins,
eslint flat config, prettier, vitest, `checks`/`test`/`ci` scripts) plus the
Worker toolchain: a `wrangler.jsonc` with a real `main` entrypoint — unlike the
assets-only docs Worker in `website/` — targeting the `cg-mock-api` service name
so it cannot collide with the `common-grants` preview Worker. `src/index.ts` is
a pure `fetch(Request) => Response` handler serving a health route at `GET /`
that reports the service name and the four supported spec versions
(0.1.0-0.4.0); the accompanying vitest suite invokes the exported handler
directly, establishing the test pattern the opportunity router reuses in
#1077-T3. Also wires the package into the root `ci` aggregate.

Refs #1077

Files changed:
- pnpm-workspace.yaml
- package.json
- pnpm-lock.yaml
- mock-api/package.json
- mock-api/wrangler.jsonc
- mock-api/tsconfig.json
- mock-api/eslint.config.js
- mock-api/vitest.config.ts
- mock-api/.prettierrc
- mock-api/.gitignore
- mock-api/src/index.ts
- mock-api/__tests__/index.spec.ts
Brings the deterministic opportunity dataset from the MSW mock-playground spike
(#1049, branch `karina/playground-spike`) into the standalone Worker package as
`src/data/fixtures.ts`: the 11 frozen records, `SUPPORTED_VERSIONS`,
`isSupportedVersion`, `shapeOpportunityForVersion`, `getById`, and
`allForVersion`. The file carried zero MSW imports on the spike branch, so the
records, the type surface, and the v0.1/list-variant stripping logic all move
over unchanged — the canonical id Swagger UI pre-fills
(30a12e5e-5940-4c08-921c-17a8960fcf4b) and the reserved-missing id are both
preserved. The one behavior change is extending `SUPPORTED_VERSIONS` with
"0.4.0", which shipped on main via #976; v0.4.0 added awards/organizations
routes but left `OpportunityBase`/`OpportunityDetails` byte-identical to
v0.3.0, so it shapes identically and needs no new branch in
`shapeOpportunityForVersion` — #1077-T4 will verify that against the generated
per-version schemas. The ported spec is verbatim apart from that criterion:
0.4.0 is now asserted supported, "0.5.0" becomes the rejected case, and a new
case pins 0.4.0 shaping equal to 0.3.0 across both list and detail variants.
`src/index.ts` now sources the health route's version list from the fixture
instead of the local const #1077-T1 left as a placeholder.

Refs #1077

Files changed:
- mock-api/src/data/fixtures.ts
- mock-api/__tests__/data/fixtures.spec.ts
- mock-api/src/index.ts
Ports the #1049 spike's opportunity handlers into the standalone Worker,
replacing MSW's handler registration with a path-prefix router and adding
cross-origin access. `GET /v{version}/common-grants/opportunities`, `GET
.../opportunities/{oppId}`, and `POST .../opportunities/search` now serve the
fixture set for all four spec versions (0.1.0-0.4.0), returning the same
envelopes as the spike for the same inputs; an unsupported, missing, or
malformed version prefix answers a protocol-shaped 404. MSW touched the
handlers in exactly two places, both removed: `http.get/post` registration
became exported `(Request, Version) => Response` functions, and
`HttpResponse.json` became `Response.json`. Every filter, sort, pagination,
and validation rule moved across unchanged.

CORS is applied once, wrapping the router, so success responses, validation
errors, and route misses alike carry `Access-Control-Allow-Origin: *` and
allow `Content-Type`, `X-API-Key`, and `Authorization` -- the headers the docs
site's "Try it out" and the TS SDK need. `OPTIONS` preflights are answered for
any path, matched or not, since a 404 to a preflight surfaces in the browser
as an opaque CORS failure rather than the protocol error the real request
would return.

One deliberate behavior change from the port: a search body that is valid JSON
but not an object (`null`, `[]`, `42`, a bare string) now answers 400 instead
of either throwing -- which escaped the handler before CORS was attached -- or
quietly returning 200 with the whole unfiltered set. A `withErrorBoundary`
backstop keeps any future throw on the CORS path as a 500 envelope.

Verified by 107 new tests (the ported suite re-pointed at the Worker's fetch
handler, plus router, CORS, and SDK-envelope cases) and by curl against
`wrangler dev`.

Refs #1077

Files changed:
- mock-api/src/index.ts
- mock-api/src/handlers/opportunities.ts
- mock-api/src/http/cors.ts
- mock-api/src/http/envelope.ts
- mock-api/__tests__/handlers/opportunities.spec.ts
- mock-api/__tests__/handlers/sdk-envelope.spec.ts
- mock-api/__tests__/http/cors.spec.ts
Validates every fixture record, shaped per (version x variant), against the
per-version JSON Schemas the docs pipeline generates, so hand-coded shaping in
`src/data/fixtures.ts` can't drift from the protocol unnoticed. A new `schemas`
script regenerates `website/public/schemas/yaml/versions/` (gitignored build
output) and `ci` runs it before the suite; the suite raises rather than skips
when those schemas are absent, since a skipped conformance run reads as a green
CI run that validated nothing.

`__tests__/utils/schema-validator.ts` overlays each version's generated
directory onto the unversioned schema set and loads the result into one Ajv2020
instance, following `website/src/lib/validation.ts:createAjvWithSchemas` and
`lib/ts-sdk/__tests__/utils/ajv-validator.ts` rather than pre-dereferencing as
`website/src/lib/schema/ref-resolver.ts` does — the versioned directories omit
`$ref` targets like `uuid.yaml`, so dereferencing them fails outright. Formats
are validated for real via ajv-formats, with `time` overridden to the
protocol's own "HH:mm:ss without timezone" definition because `isoTime.yaml`
emits JSON Schema's RFC 3339 `time`, which rejects the spec's published
`17:00:00` example.

Two limitations are documented in code rather than smoothed over. Models emitted
only into `versions/v0.4.0/` (ApplicantType, OppStatus, CompetitionStatus, and
others the changelog records as added in 0.2.0) fall back to current shapes, so
this catches drift in the opportunity models' own fields but not in the enums
they reference. And the fixture's trimmed `Competition` shape omits
CompetitionBase's required `forms`/`createdAt`/`lastModifiedAt`, which fails
OpportunityDetails validation for 3 of the 11 records; that deliberate #1049
deviation is carved out of the detail assertions and pinned by a canary designed
to fail if the fixture is ever completed.

Splits typechecking so `src/` stays Worker-only while the Node-based tests get
Node's types, keeping a stray `fs`/`process` in the Worker entrypoint an error.

Refs #1077

Files changed:
- mock-api/__tests__/conformance/fixtures-vs-schemas.spec.ts
- mock-api/__tests__/utils/schema-validator.ts
- mock-api/tsconfig.test.json
- mock-api/tsconfig.json
- mock-api/package.json
- pnpm-lock.yaml
Adds `.github/workflows/ci-mock-api.yml`, path-filtered to `mock-api/**` plus the
workflow file, following the per-package convention of the sibling `ci-lib-*.yml`
workflows. The `validate` job runs build, checks, versioned-schema generation, tests,
and audit — the schema step kept separate so the conformance suite's cross-package
dependency on the website pipeline stays visible in the CI log. The `deploy` job is
`workflow_dispatch`-only and gated on `validate`, running a plain `wrangler deploy`
with the existing CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID secrets; it is
deliberately not modelled on `ci-website-preview.yml`, which versions the shared
`common-grants` Worker, since this is a separate service with its own name. After
deploying it resolves the `*.workers.dev` URL and smoke-tests the two routes the
ticket names, so a deploy that succeeds while serving a broken Worker fails the run.
The concurrency group is intentionally not keyed on `github.ref` — every ref targets
the one `cg-mock-api` slot, so a ref-scoped group would permit exactly the race it
is meant to prevent.

`mock-api/DEPLOYMENT.md` records the deployment-overhead rubric input: jobs, secrets
(no new ones), locally measured step timings, a manual first-deploy runbook, and the
gaps worth carrying into the findings — personal-account hosting, a single
deployment slot, and a path filter that cannot see the `website/`- and
`lib/core/`-generated schemas the conformance suite depends on.

Acceptance criterion 2 is NOT met: no Worker has been deployed, so the
`*.workers.dev` URL is unverified. `wrangler deploy` publishes a public service and
needs a human decision, and `workflow_dispatch` only exposes a "Run workflow"
control once the workflow file has reached the default branch, so CI cannot do it
from this branch yet. The ticket stays open; DEPLOYMENT.md carries the runbook and
the unfilled URL.

Refs #1077

Files changed:
- .github/workflows/ci-mock-api.yml
- mock-api/DEPLOYMENT.md
- mock-api/wrangler.jsonc
@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Website Preview Deployed!

Preview your changes at: https://pr-1088-common-grants.brian-derfer.workers.dev

Built from commit 5bbc3c4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Mock API] Experiment: standalone Cloudflare Worker mock (ADR Option 3A)

1 participant