#51 — Tech debt: generate ApiRequestMap from the backend's OpenAPI spec instead of hand-maintaining it #51

Closed
opened 2026-08-18 13:13:16 +02:00 by lena · 2 comments
lena commented 2026-08-18 13:13:16 +02:00 (Migrated from git.butzei.de)

Tech debt: generate ApiRequestMap from the backend's OpenAPI spec instead of hand-maintaining it

Reported by: Software Architect / Frontend Engineer (found during #49 code review)
Priority: Could — no user-facing symptom; narrows a guarantee #49 already shipped rather than
fixing a bug
Date: 2026-07-12

Problem

#49 added ReactUi/src/api/requests.ts's ApiRequestMap, a hand-transcribed TypeScript map of
every requestName string to its request/response shape, sourced by reading the matching C#
IRequest<TResult> record in CqsTodo/Features/**. This closes the gap #49 set out to close (a
wrong field name at a callApi(...) call site now fails tsc -b), but only protects against
frontend-authored typos — nothing ties ApiRequestMap to the real backend contract on an
ongoing basis. If a backend engineer renames a field or changes a command's shape, the C# build
stays green and tsc -b stays green too, because requests.ts wasn't touched and still
type-checks consistently against itself. The mismatch only surfaces when a real request
400s/404s at runtime — the same class of "manual-discovery, not compile-time" problem #49's
design note says it exists to eliminate, just narrowed to one direction (frontend mistakes) that
happens to be a large fraction, but not all, of the risk this ticket's title names.

The backend already exposes a full OpenAPI spec via Swashbuckle
(CqsTodo.WebApi/Program.cs: app.UseSwagger(), .AddSwaggerGen(...)) that fully describes
every IRequest<TResult> record's shape.

Proposal

Replace (or generate) ApiRequestMap from the backend's live OpenAPI spec using a codegen tool
(openapi-typescript or NSwag), run either as a build step or a checked-in generated file
refreshed via a script. This would give the same "wrong field name fails tsc -b" guarantee with
zero hand-maintenance and no drift risk in either direction (frontend typo or backend change).

Acceptance criteria (PO-refined; moved to ready/)

  • ApiRequestMap is a derived TypeScript type computed from the backend's OpenAPI spec
    (via openapi-typescript), not a hand-transcribed interface — see
    ReactUi/src/api/requests.ts
  • A backend field rename/shape change is caught by the frontend build: paths (the
    generated schema) changes on the next npm run generate:api, and any callApi(...) call site
    or dtos.ts/valueTypes.ts assignment that no longer matches fails tsc -b
  • Regenerating is a documented, low-friction step: npm run generate:api (pure Node, no
    .NET needed) reads the backend's openapi/swagger.json; CI regenerates it automatically
    between the backend and frontend jobs (see _design.md)
  • No unintended behavior change — the OpenAPI spec itself needed three schema-accuracy fixes
    (enum wire format, required-vs-optional, response nullability) to safely reflect the real
    contract; these are spec/tooling corrections, not endpoint behavior changes. See _design.md
    for the full list and the one intentional, documented strictness change (assignedToMeOnly
    becomes an explicitly-optional key instead of implicitly always-required by omission-with-
    default, which was already its real behavior).

Blockers

None. Builds on #49's ApiRequestMap shape/call-site pattern, which stays valid regardless of
whether the map is hand-written or generated.

# Tech debt: generate `ApiRequestMap` from the backend's OpenAPI spec instead of hand-maintaining it **Reported by:** Software Architect / Frontend Engineer (found during `#49` code review) **Priority:** Could — no user-facing symptom; narrows a guarantee `#49` already shipped rather than fixing a bug **Date:** 2026-07-12 ## Problem `#49` added `ReactUi/src/api/requests.ts`'s `ApiRequestMap`, a hand-transcribed TypeScript map of every `requestName` string to its request/response shape, sourced by reading the matching C# `IRequest<TResult>` record in `CqsTodo/Features/**`. This closes the gap `#49` set out to close (a wrong field name at a `callApi(...)` call site now fails `tsc -b`), but only protects against *frontend-authored* typos — nothing ties `ApiRequestMap` to the real backend contract on an ongoing basis. If a backend engineer renames a field or changes a command's shape, the C# build stays green and `tsc -b` stays green too, because `requests.ts` wasn't touched and still type-checks consistently against itself. The mismatch only surfaces when a real request 400s/404s at runtime — the same class of "manual-discovery, not compile-time" problem `#49`'s design note says it exists to eliminate, just narrowed to one direction (frontend mistakes) that happens to be a large fraction, but not all, of the risk this ticket's title names. The backend already exposes a full OpenAPI spec via Swashbuckle (`CqsTodo.WebApi/Program.cs`: `app.UseSwagger()`, `.AddSwaggerGen(...)`) that fully describes every `IRequest<TResult>` record's shape. ## Proposal Replace (or generate) `ApiRequestMap` from the backend's live OpenAPI spec using a codegen tool (`openapi-typescript` or NSwag), run either as a build step or a checked-in generated file refreshed via a script. This would give the same "wrong field name fails `tsc -b`" guarantee with zero hand-maintenance and no drift risk in either direction (frontend typo *or* backend change). ## Acceptance criteria (PO-refined; moved to `ready/`) - [x] `ApiRequestMap` is a *derived* TypeScript type computed from the backend's OpenAPI spec (via `openapi-typescript`), not a hand-transcribed interface — see `ReactUi/src/api/requests.ts` - [x] A backend field rename/shape change is caught by the frontend build: `paths` (the generated schema) changes on the next `npm run generate:api`, and any `callApi(...)` call site or `dtos.ts`/`valueTypes.ts` assignment that no longer matches fails `tsc -b` - [x] Regenerating is a documented, low-friction step: `npm run generate:api` (pure Node, no .NET needed) reads the backend's `openapi/swagger.json`; CI regenerates it automatically between the backend and frontend jobs (see `_design.md`) - [x] No unintended behavior change — the OpenAPI spec itself needed three schema-accuracy fixes (enum wire format, required-vs-optional, response nullability) to safely reflect the *real* contract; these are spec/tooling corrections, not endpoint behavior changes. See `_design.md` for the full list and the one intentional, documented strictness change (`assignedToMeOnly` becomes an explicitly-optional key instead of implicitly always-required by omission-with- default, which was already its real behavior). ## Blockers None. Builds on `#49`'s `ApiRequestMap` shape/call-site pattern, which stays valid regardless of whether the map is hand-written or generated.
lena commented 2026-08-18 13:13:17 +02:00 (Migrated from git.butzei.de)

design (51_generate_api_request_map_from_openapi_design.md)

Design note — #51 Generate ApiRequestMap from OpenAPI

Author: Software Architect
Status: Implemented this cycle

Summary

ApiRequestMap (ReactUi/src/api/requests.ts) is now a TypeScript mapped type derived from
ReactUi/src/api/generated/openapi-schema.d.ts, which openapi-typescript generates from
ReactUi/openapi/swagger.json, which the backend emits via the Swashbuckle CLI
(dotnet swagger tofile). Neither generated file is committed — both are build artifacts,
regenerated fresh every time (.gitignore: /openapi/, /src/api/generated/).

CqsTodo.WebApi.dll --(dotnet swagger tofile)--> openapi/swagger.json
                                                       |
                                                       v (npm run generate:api)
                                          src/api/generated/openapi-schema.d.ts
                                                       |
                                                       v (TS mapped type, requests.ts)
                                                 ApiRequestMap

Key decisions

1. Spec extraction doesn't need a live database

dotnet tool run swagger tofile runs the WebApi's Main via Swashbuckle's
HostFactoryResolver (the same mechanism dotnet ef uses), which intercepts at app.Run()
not at builder.Build(). Everything between Build() and Run() in Program.Main still
executes for real, which includes await Setup.MigrateAndSeedAsync(app.Services) — confirmed
experimentally (it ran real migrations against a live Postgres). Fix: Program.cs now skips
that call when SWAGGER_GEN=1 is set, so the whole extraction is DB-free and portable — it works
in the CI backend job (no Postgres/Redis service containers needed) and in any local dev
environment, DB or no DB.

2. Swashbuckle's default schema is wrong in three ways relative to actual runtime behavior

Naively wiring openapi-typescript straight to Swashbuckle's out-of-the-box output would have
been a regression, not a neutral tooling swap — three custom ISchemaFilter/IOperationFilter
implementations in CqsTodo.WebApi/ fix this:

  • StringEnumSchemaFilterProgram.cs registers a global JsonStringEnumConverter, so
    enums serialize as strings on the wire, but Swashbuckle's default schema described them as
    integers (0 | 1 | 2). Uncorrected, generated types like TodoPriority would have been
    0 | 1 | 2 instead of 'High' | 'Normal' | 'Low' — wrong, not just imprecise.
  • RequiredPropertiesSchemaFilter — Swashbuckle's minimal-API schema generation doesn't
    compute required for record-bound bodies at all (every field looked optional). The fix
    distinguishes two schema roles, because "required" means something different for each:
    • IRequest<TResult> types (top-level Command/Query bodies, client-constructed): a
      primary-constructor parameter with a default value (e.g.
      GetTodosOfCurrentUserQuery(bool AssignedToMeOnly = false)) is genuinely optional to send.
    • Everything else (DTOs, nested value objects, server-produced): System.Text.Json serializes
      every public property regardless of a constructor default or nullability, so these are
      always required — nullability is captured separately, not via omitting the key.
      Getting this rule wrong in the DTO direction was caught immediately by
      requests.typecheck.ts's assertMissingRequiredFieldIsRejected test and by tsc -b picking
      up dozens of call sites that assumed always-present fields like TodoDto.priority were
      optional.
  • NullableReferenceSchemaFilter + NullableResponseOperationFilter — OpenAPI 3.0
    forbids sibling keywords next to a bare $ref, so Swashbuckle silently drops nullability for
    any property or response whose schema is a $ref — which is every Vogen value object
    (TodoDueDate?, UserId?, ...) and every nested DTO reference (TodoAssigneeDto? Assignee),
    plus top-level nullable results (GetActiveListInvitationQuery : IRequest<ListInvitationDto?>,
    GetCurrentUserIdQuery : IRequest<UserId?>). Both filters wrap the $ref as
    { allOf: [ref], type: "null" }, which Microsoft.OpenApi's OpenAPI 3.0 writer downgrades to the
    legacy { allOf: [ref], nullable: true } form. The operation-level filter reads nullability off
    the handler's concrete Handle method via NullabilityInfoContext (not the generic
    EndpointRouteBuilderExtensions.Handle<TRequest, TResult> dispatcher, whose substituted
    TResult carries no annotation of its own), plus a direct Nullable.GetUnderlyingType check
    for value-type results (UserId?), since NullabilityInfoContext only tracks reference-type
    NRT annotations.

3. ApiRequestMap is a mapped type over paths, not per-endpoint hand transcription

type ApiPath = keyof paths & `/api/${string}`;
type RequestNameOf<P extends ApiPath> = P extends `/api/${infer Name}` ? Name : never;
export type ApiRequestMap = {
    [P in ApiPath as RequestNameOf<P>]: { request: RequestBodyOf<P>; result: ResultOf<P> };
};

Adding a new CQS endpoint on the backend needs no requests.ts edit at all — the next
npm run generate:api picks it up automatically. dtos.ts and valueTypes.ts stay hand-written
(the same names/shapes the rest of the app already imports), which is what actually delivers the
drift guarantee: every callApi(...) result flows from the generated schema into a
hand-maintained TodoDto/UserDto/etc. at the call site, so any real divergence between them is
a tsc -b structural-assignability error, not a silent pass. dtos.ts needed two accuracy fixes
in the same direction as filter #2 above (avatarImage, assignee were marked optional with
? where the wire format is actually "always-present key, nullable value" — dropped the ?,
kept the | null).

4. Bare-string results aren't application/json

GetPushVapidPublicKeyQuery returns VapidPublicKey (a Vogen struct wrapping string) — ASP.NET
Core's minimal APIs write bare-string results as Content-Type: text/plain, confirmed against a
live request in this cycle's dev environment. api.tsx's callApi already special-cases this
(reads .text() when the content type contains "text"). ResultOf<P> in requests.ts now falls
back to the text/plain content entry when application/json isn't present, matching that
existing runtime behavior instead of silently resolving to never for this one endpoint.

5. CI wiring

The backend job now also restores the swashbuckle.aspnetcore.cli local tool
(dotnet-tools.json, already at repo root) and runs
SWAGGER_GEN=1 dotnet tool run swagger tofile right after dotnet build, uploading
openapi/swagger.json as a build artifact. The frontend job now depends on backend,
downloads that artifact, and runs npm run generate:api before npm run build/npm run coverage — so a backend contract change that isn't reflected on the frontend fails CI on the
very next push, which is the ticket's core acceptance criterion.

Trade-offs accepted

  • assignedToMeOnly (and similarly unreadOnly, limit, skip, take) become genuinely
    optional keys
    (matching their C# default values) rather than the previous hand-map's ad hoc
    per-field choices — this is the only behavior-adjacent change, and it's a loosening (callers
    may now omit a key that used to be optional too) not a tightening, so no existing call site
    broke.
  • Field-requiredness accuracy depends on the IRequest<TResult> vs. DTO distinction holding
    for future types. If a future request type is reused as a nested field inside another request
    (not currently the case anywhere in the codebase), the "client-constructed, defaults are
    optional" rule would incorrectly apply to it in a response context. Worth a one-line comment
    if that pattern ever appears.
  • The generated schema files are gitignored, not committed. Local npm run build outside CI
    requires a prior dotnet build + dotnet tool run swagger tofile step (documented in
    package.json's generate:api script and this design note) — there is no fallback for a
    frontend-only checkout with no .NET SDK available.
  • CI's frontend job now depends on backend (to consume its openapi-spec artifact),
    where it previously ran in parallel. This adds roughly backend's duration to the critical
    path before frontend even starts, since frontend can no longer overlap with it. Accepted
    as the direct cost of the drift guarantee this ticket asks for — the alternative (skip the spec
    in CI, only regenerate it as a manual/local step) would silently reintroduce the exact gap #51
    exists to close.
**design** (`51_generate_api_request_map_from_openapi_design.md`) # Design note — `#51` Generate `ApiRequestMap` from OpenAPI **Author:** Software Architect **Status:** Implemented this cycle ## Summary `ApiRequestMap` (`ReactUi/src/api/requests.ts`) is now a TypeScript mapped type derived from `ReactUi/src/api/generated/openapi-schema.d.ts`, which `openapi-typescript` generates from `ReactUi/openapi/swagger.json`, which the backend emits via the Swashbuckle CLI (`dotnet swagger tofile`). Neither generated file is committed — both are build artifacts, regenerated fresh every time (`.gitignore`: `/openapi/`, `/src/api/generated/`). ``` CqsTodo.WebApi.dll --(dotnet swagger tofile)--> openapi/swagger.json | v (npm run generate:api) src/api/generated/openapi-schema.d.ts | v (TS mapped type, requests.ts) ApiRequestMap ``` ## Key decisions ### 1. Spec extraction doesn't need a live database `dotnet tool run swagger tofile` runs the WebApi's `Main` via Swashbuckle's `HostFactoryResolver` (the same mechanism `dotnet ef` uses), which intercepts at `app.Run()` — **not** at `builder.Build()`. Everything between `Build()` and `Run()` in `Program.Main` still executes for real, which includes `await Setup.MigrateAndSeedAsync(app.Services)` — confirmed experimentally (it ran real migrations against a live Postgres). Fix: `Program.cs` now skips that call when `SWAGGER_GEN=1` is set, so the whole extraction is DB-free and portable — it works in the CI `backend` job (no Postgres/Redis service containers needed) and in any local dev environment, DB or no DB. ### 2. Swashbuckle's default schema is wrong in three ways relative to actual runtime behavior Naively wiring `openapi-typescript` straight to Swashbuckle's out-of-the-box output would have been a **regression**, not a neutral tooling swap — three custom `ISchemaFilter`/`IOperationFilter` implementations in `CqsTodo.WebApi/` fix this: - **`StringEnumSchemaFilter`** — `Program.cs` registers a global `JsonStringEnumConverter`, so enums serialize as strings on the wire, but Swashbuckle's default schema described them as integers (`0 | 1 | 2`). Uncorrected, generated types like `TodoPriority` would have been `0 | 1 | 2` instead of `'High' | 'Normal' | 'Low'` — wrong, not just imprecise. - **`RequiredPropertiesSchemaFilter`** — Swashbuckle's minimal-API schema generation doesn't compute `required` for record-bound bodies at all (every field looked optional). The fix distinguishes two schema roles, because "required" means something different for each: - `IRequest<TResult>` types (top-level `Command`/`Query` bodies, client-constructed): a primary-constructor parameter with a default value (e.g. `GetTodosOfCurrentUserQuery(bool AssignedToMeOnly = false)`) is genuinely optional to send. - Everything else (DTOs, nested value objects, server-produced): System.Text.Json serializes every public property regardless of a constructor default or nullability, so these are always required — nullability is captured separately, not via omitting the key. Getting this rule wrong in the DTO direction was caught immediately by `requests.typecheck.ts`'s `assertMissingRequiredFieldIsRejected` test and by `tsc -b` picking up dozens of call sites that assumed always-present fields like `TodoDto.priority` were optional. - **`NullableReferenceSchemaFilter`** + **`NullableResponseOperationFilter`** — OpenAPI 3.0 forbids sibling keywords next to a bare `$ref`, so Swashbuckle silently drops nullability for any property or response whose schema is a `$ref` — which is every Vogen value object (`TodoDueDate?`, `UserId?`, ...) and every nested DTO reference (`TodoAssigneeDto? Assignee`), plus top-level nullable results (`GetActiveListInvitationQuery : IRequest<ListInvitationDto?>`, `GetCurrentUserIdQuery : IRequest<UserId?>`). Both filters wrap the `$ref` as `{ allOf: [ref], type: "null" }`, which Microsoft.OpenApi's OpenAPI 3.0 writer downgrades to the legacy `{ allOf: [ref], nullable: true }` form. The operation-level filter reads nullability off the *handler's* concrete `Handle` method via `NullabilityInfoContext` (not the generic `EndpointRouteBuilderExtensions.Handle<TRequest, TResult>` dispatcher, whose substituted `TResult` carries no annotation of its own), plus a direct `Nullable.GetUnderlyingType` check for value-type results (`UserId?`), since `NullabilityInfoContext` only tracks *reference*-type NRT annotations. ### 3. `ApiRequestMap` is a mapped type over `paths`, not per-endpoint hand transcription ```ts type ApiPath = keyof paths & `/api/${string}`; type RequestNameOf<P extends ApiPath> = P extends `/api/${infer Name}` ? Name : never; export type ApiRequestMap = { [P in ApiPath as RequestNameOf<P>]: { request: RequestBodyOf<P>; result: ResultOf<P> }; }; ``` Adding a new CQS endpoint on the backend needs no `requests.ts` edit at all — the next `npm run generate:api` picks it up automatically. `dtos.ts` and `valueTypes.ts` stay hand-written (the same names/shapes the rest of the app already imports), which is what actually delivers the drift guarantee: every `callApi(...)` result flows from the *generated* schema into a hand-maintained `TodoDto`/`UserDto`/etc. at the call site, so any real divergence between them is a `tsc -b` structural-assignability error, not a silent pass. `dtos.ts` needed two accuracy fixes in the same direction as filter `#2` above (`avatarImage`, `assignee` were marked optional with `?` where the wire format is actually "always-present key, nullable value" — dropped the `?`, kept the `| null`). ### 4. Bare-string results aren't `application/json` `GetPushVapidPublicKeyQuery` returns `VapidPublicKey` (a Vogen struct wrapping `string`) — ASP.NET Core's minimal APIs write bare-string results as `Content-Type: text/plain`, confirmed against a live request in this cycle's dev environment. `api.tsx`'s `callApi` already special-cases this (reads `.text()` when the content type contains "text"). `ResultOf<P>` in `requests.ts` now falls back to the `text/plain` content entry when `application/json` isn't present, matching that existing runtime behavior instead of silently resolving to `never` for this one endpoint. ### 5. CI wiring The `backend` job now also restores the `swashbuckle.aspnetcore.cli` local tool (`dotnet-tools.json`, already at repo root) and runs `SWAGGER_GEN=1 dotnet tool run swagger tofile` right after `dotnet build`, uploading `openapi/swagger.json` as a build artifact. The `frontend` job now depends on `backend`, downloads that artifact, and runs `npm run generate:api` before `npm run build`/`npm run coverage` — so a backend contract change that isn't reflected on the frontend fails CI on the very next push, which is the ticket's core acceptance criterion. ## Trade-offs accepted - **`assignedToMeOnly` (and similarly `unreadOnly`, `limit`, `skip`, `take`) become genuinely optional keys** (matching their C# default values) rather than the previous hand-map's ad hoc per-field choices — this is the *only* behavior-adjacent change, and it's a loosening (callers may now omit a key that used to be optional too) not a tightening, so no existing call site broke. - **Field-requiredness accuracy depends on the `IRequest<TResult>` vs. DTO distinction holding** for future types. If a future request type is reused as a nested field inside another request (not currently the case anywhere in the codebase), the "client-constructed, defaults are optional" rule would incorrectly apply to it in a response context. Worth a one-line comment if that pattern ever appears. - **The generated schema files are gitignored, not committed.** Local `npm run build` outside CI requires a prior `dotnet build` + `dotnet tool run swagger tofile` step (documented in `package.json`'s `generate:api` script and this design note) — there is no fallback for a frontend-only checkout with no .NET SDK available. - **CI's `frontend` job now depends on `backend`** (to consume its `openapi-spec` artifact), where it previously ran in parallel. This adds roughly `backend`'s duration to the critical path before `frontend` even starts, since `frontend` can no longer overlap with it. Accepted as the direct cost of the drift guarantee this ticket asks for — the alternative (skip the spec in CI, only regenerate it as a manual/local step) would silently reintroduce the exact gap `#51` exists to close.
lena commented 2026-08-18 13:13:17 +02:00 (Migrated from git.butzei.de)

qa (51_generate_api_request_map_from_openapi_qa.md)

QA notes — #51 Generate ApiRequestMap from OpenAPI

Verified this cycle (real backend + Postgres/Redis were reachable in this dev environment,
unlike prior cycles — see team memory update):

  • dotnet build Cqs.sln -c Release — clean, 0 errors
  • dotnet testCommon.Tests 119/119, CqsTodo.WebApi.Tests 12/12 (incl.
    VogenSchemaFilterTests and CanBuildApp, confirming the new schema/operation filters don't
    break app construction). CqsTodo.Tests's Testcontainers-backed suite still can't run in this
    sandbox (no docker binary/socket) — pre-existing limitation, unrelated to this change; will
    run for real on the next "go" cycle's CI build-check per the standing pattern noted in the
    roadmap's #29 decision-log entry.
  • dotnet tool run swagger tofile — confirmed twice: with the sandbox's real Postgres/Redis
    reachable, and with ConnectionStrings__* env vars explicitly unset — spec generation succeeds
    either way once SWAGGER_GEN=1 skips migration/seeding.
  • Manually hit the live dev server (after seeding a fresh user via CreateUserCommand) to
    confirm real wire behavior for two specific things the generated spec needed to get right:
    avatarImage is present-but-null in a real UserDto response (not omitted), and
    GetPushVapidPublicKeyQuery really does respond Content-Type: text/plain.
  • npx tsc -b — 0 errors, including requests.typecheck.ts's 4 @ts-expect-error regression
    assertions from #49 (still correctly reporting errors, not "unused directive" — the
    required-field guarantee survived the switch to a generated map)
  • npx vitest run / npm run coverage — 250/250 tests, exit 0
  • npm run build (tsc -b && vite build) — clean production build

Fixed as a mechanical consequence of tightening avatarImage/assignee from optional to
required-key-nullable-value in dtos.ts
(matching real wire behavior): ~15 test fixtures across
AssigneeBadge.test.tsx, AssignmentPicker.test.tsx, CurrentUserAvatar.test.tsx,
Register.test.tsx, SettingsModal.test.tsx, store.test.ts, TodoItem.test.tsx,
TodoList.test.tsx needed the now-required field added to their mock literals. No production
code changes were needed beyond dtos.ts itself.

Not covered: an actual CI run of the new backendfrontend artifact hand-off (sandbox has
no Gitea Actions runner). This is the same category of gap #46/#50 already documented — verified
locally to the fullest extent possible, real confirmation deferred to the next "go" cycle's
CI build-check step.

**qa** (`51_generate_api_request_map_from_openapi_qa.md`) # QA notes — `#51` Generate `ApiRequestMap` from OpenAPI **Verified this cycle** (real backend + Postgres/Redis were reachable in this dev environment, unlike prior cycles — see team memory update): - `dotnet build Cqs.sln -c Release` — clean, 0 errors - `dotnet test` — `Common.Tests` 119/119, `CqsTodo.WebApi.Tests` 12/12 (incl. `VogenSchemaFilterTests` and `CanBuildApp`, confirming the new schema/operation filters don't break app construction). `CqsTodo.Tests`'s Testcontainers-backed suite still can't run in this sandbox (no `docker` binary/socket) — pre-existing limitation, unrelated to this change; will run for real on the next "go" cycle's CI build-check per the standing pattern noted in the roadmap's `#29` decision-log entry. - `dotnet tool run swagger tofile` — confirmed twice: with the sandbox's real Postgres/Redis reachable, and with `ConnectionStrings__*` env vars explicitly unset — spec generation succeeds either way once `SWAGGER_GEN=1` skips migration/seeding. - Manually hit the live dev server (after seeding a fresh user via `CreateUserCommand`) to confirm real wire behavior for two specific things the generated spec needed to get right: `avatarImage` is present-but-`null` in a real `UserDto` response (not omitted), and `GetPushVapidPublicKeyQuery` really does respond `Content-Type: text/plain`. - `npx tsc -b` — 0 errors, including `requests.typecheck.ts`'s 4 `@ts-expect-error` regression assertions from `#49` (still correctly reporting errors, not "unused directive" — the required-field guarantee survived the switch to a generated map) - `npx vitest run` / `npm run coverage` — 250/250 tests, exit 0 - `npm run build` (`tsc -b && vite build`) — clean production build **Fixed as a mechanical consequence of tightening `avatarImage`/`assignee` from optional to required-key-nullable-value in `dtos.ts`** (matching real wire behavior): ~15 test fixtures across `AssigneeBadge.test.tsx`, `AssignmentPicker.test.tsx`, `CurrentUserAvatar.test.tsx`, `Register.test.tsx`, `SettingsModal.test.tsx`, `store.test.ts`, `TodoItem.test.tsx`, `TodoList.test.tsx` needed the now-required field added to their mock literals. No production code changes were needed beyond `dtos.ts` itself. **Not covered:** an actual CI run of the new `backend` → `frontend` artifact hand-off (sandbox has no Gitea Actions runner). This is the same category of gap `#46`/`#50` already documented — verified locally to the fullest extent possible, real confirmation deferred to the next "go" cycle's CI build-check step.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
robert/todo#51
No description provided.