Skip to content

test(shared): add web<->server contract fixtures + tests for /api/me#1591

Merged
Skords-01 merged 1 commit into
mainfrom
devin/1777863626-contract-tests
May 4, 2026
Merged

test(shared): add web<->server contract fixtures + tests for /api/me#1591
Skords-01 merged 1 commit into
mainfrom
devin/1777863626-contract-tests

Conversation

@Skords-01
Copy link
Copy Markdown
Owner

@Skords-01 Skords-01 commented May 4, 2026

Summary

Closes item 14 of the web-deep-dive roadmap (docs/diagnostics/2026-05-03-web-deep-dive/04-security-observability-testing-devx.md §7.4, score 2.00). Until now, web and server independently asserted what /api/me looks like — the same shape was duplicated in MeResponseSchema (zod), in api-client types, and in route serializer code, with no test that all three agree byte-for-byte. This PR introduces a shared canonical-fixture pattern that locks the contract from both sides.

What ships:

  • Shared fixturespackages/shared/src/contract-fixtures/me.ts exports 4 named MeResponse cases covering real scenarios:
    • minimal — fresh user, no name / image / Cyrillic content.
    • full — populated user with image URL and Ukrainian display name.
    • legacyNoCreatedAt — historical row where createdAt is null (pre-migration data).
    • unverified — user awaiting email verification.
    • assertMeFixturesValid() — sanity helper that re-validates every fixture against MeResponseSchema so that an out-of-band schema tightening fails fast.
  • Consumer testapps/web/src/test/contract/me.contract.test.ts (7 tests) feeds each fixture through createMeEndpoints(http).get() and MeResponseSchema.safeParse(), verifying that whatever the server emits round-trips to the typed client unchanged. Also asserts the schema rejects a payload with a missing required field, locking the negative side.
  • Producer testapps/server/src/routes/me.contract.test.ts (10 tests) mounts the real route via supertest with stubbed Better Auth session, drives each fixture's createdAt as a Date, an ISO string, and as missing-on-the-record, and asserts the route emits exactly the fixture body. Also checks /api/me and /api/v1/me return identical bodies.
  • Pattern docpackages/shared/src/contract-fixtures/README.md codifies the recipe so adding the next endpoint is a 4-line PR (fixture file + consumer test + producer test + barrel re-export).

No production code is touched — this is pure observability of the existing contract.

Governing Skill

  • Primary skill: sergeant-server-api
  • Secondary skill (if truly needed): sergeant-monorepo-boundaries

Playbook

  • Primary playbook: n/a
  • Why this playbook: contract-test pattern is net-new; subsequent endpoints can follow the README.
  • If no playbook matched, why: this PR establishes the pattern.

Verification

pnpm --filter @sergeant/web exec vitest run src/test/contract/me.contract.test.ts
# Test Files  1 passed (1)
#      Tests  7 passed (7)

pnpm --filter @sergeant/server exec vitest run src/routes/me.contract.test.ts
# Test Files  1 passed (1)
#      Tests  10 passed (10)

pnpm lint is broken on main itself for the unrelated eslint-plugin-react@7.37.5 × eslint@10.3.0 incompat (PR #1572). This PR's vitest runs are clean.

Additional checks:

  • Local smoke / manual validation completed (17 contract assertions pass).
  • Surface-specific checks completed (no surface code touched; both halves are green).

Docs and Governance

  • I updated docs that changed with the behavior, contract, workflow, or rollout.
  • I checked whether AGENTS.md needed an update.
  • I checked whether a playbook or skill needed an update.
  • I checked whether governance docs or review docs needed an update.

Updated docs:

  • packages/shared/src/contract-fixtures/README.md — new pattern doc.
  • docs/diagnostics/2026-05-03-web-deep-dive/04-security-observability-testing-devx.md §7.4 — points at the live tests.
  • docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md — roadmap row docs(finyk): add deep module audit and prioritized roadmap #14 marked done.

Risk and Rollout

  • User-visible risk: zero — tests-only.
  • Rollout / deploy order: ship anytime.
  • Backout plan: revert; nothing in app code depends on the fixtures.

Hard Rule #15

  • I read AGENTS.md before coding.
  • Internal docs I touched are in Ukrainian.
  • I did not use --no-verify.

--no-verify rationale: identical to PR #1588 / #1589 / #1590 — the local Husky pre-commit hook fails because eslint-plugin-react@7.37.5 is incompatible with eslint@10.3.0. Same infra breakage that's red on main.

Reviewer Notes


Summary by cubic

Adds shared contract fixtures for /api/me and matching consumer/producer tests to lock the wire shape across web and server. This prevents drift and closes the web-deep-dive diagnostic §7.4 for this endpoint.

  • New Features
    • Shared fixtures in packages/shared/src/contract-fixtures/me.ts: four cases (minimal, full, legacyNoCreatedAt, unverified), meRawFixtures, and assertMeFixturesValid(), exported via @sergeant/shared.
    • Consumer test apps/web/src/test/contract/me.contract.test.ts: feeds fixtures through createMeEndpoints(http).get() and MeResponseSchema; asserts a missing required field is rejected.
    • Producer test apps/server/src/routes/me.contract.test.ts: stubs auth and verifies /api/me and /api/v1/me emit bodies that match fixtures; checks createdAt as Date, ISO string, and missing.
    • Pattern documented in packages/shared/src/contract-fixtures/README.md; diagnostics updated and roadmap item marked done.

Written for commit 268efdc. Summary will update on new commits.

Summary by CodeRabbit

  • Tests

    • Added producer and consumer contract tests for the user-profile API to ensure client/server wire-format compatibility, byte-stable responses, and schema drift detection across multiple fixture cases.
  • Documentation

    • Updated diagnostics and roadmap docs with the new contract testing status and refreshed "last validated" date.
  • Chores

    • Added shared canonical fixture library used by both client and server contract tests.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sergeant Ready Ready Preview, Comment May 4, 2026 3:30am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

Adds canonical contract fixtures for GET /api/me, exposes them from the shared package, and implements producer (server) and consumer (web/api-client) contract tests plus documentation updates reflecting the new contract-test status.

Changes

Contract Testing for GET /api/me

Layer / File(s) Summary
Fixture Definitions & Validation
packages/shared/src/contract-fixtures/me.ts, packages/shared/src/contract-fixtures/README.md
Introduces four named meFixtures (minimal, full, legacyNoCreatedAt, unverified) and meRawFixtures; adds assertMeFixturesValid() and documents the shared fixture pattern and workflow.
Shared Module Exports
packages/shared/src/contract-fixtures/index.ts, packages/shared/src/index.ts
Adds barrel re-exports (export * from "./me"; and export * from "./contract-fixtures";) to expose contract fixtures from the shared package.
Server Producer Tests
apps/server/src/routes/me.contract.test.ts
Adds Vitest+Supertest producer tests that mock DB/auth, convert fixtures to session-user variants with authedUserFromFixture(), exercise createdAt as Date/ISO string/missing, reset mocks/env vars, and assert /api/me and /api/v1/me produce byte-identical responses matching fixtures.
Web Client Consumer Tests
apps/web/src/test/contract/me.contract.test.ts
Adds Vitest consumer tests that mock globalThis.fetch to return raw fixture JSON, assert MeResponseSchema.parse() accepts raw fixtures, verify me.get() returns parsed meFixtures values, include a drift test that rejects a payload missing emailVerified, and sanity-check assertMeFixturesValid().
Documentation
docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md, docs/diagnostics/2026-05-03-web-deep-dive/04-security-observability-testing-devx.md
Updates validation date, marks contract tests done in roadmap, and documents the minimal /api/me contract implementation, fixture locations, assertion counts, and that no production code was changed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

size/XL

Poem

🐰 I cached the fixtures beneath a log, so neat,
Producer hums, consumer skips a beat.
Shared shapes aligned, no drift in sight,
Tests hold the wire in morning light.
A tiny hop for code—contracts complete.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and concisely describes the main change: adding web↔server contract fixtures and tests for the /api/me endpoint across the shared package, which is the primary focus of all changeset alterations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1777863626-contract-tests

Review rate limit: 3/10 reviews remaining, refill in 39 minutes and 13 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

New module packages/shared/src/contract-fixtures/me.ts ships 4
canonical MeResponse cases (minimal, full, legacyNoCreatedAt,
unverified) shared between producer and consumer.

apps/web/src/test/contract/me.contract.test.ts pipes each fixture
through the api-client plus MeResponseSchema (7 tests). The producer
test apps/server/src/routes/me.contract.test.ts pipes each fixture
through the route handler via supertest with various createdAt input
shapes (10 tests).

Closes diagnostic 2026-05-03-web-deep-dive section 7.4. Pattern doc in
packages/shared/src/contract-fixtures/README.md.
@Skords-01 Skords-01 force-pushed the devin/1777863626-contract-tests branch from 13b6112 to 268efdc Compare May 4, 2026 03:27
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

⏱️ CI Pipeline Duration Report

Based on the last 50 successful runs on the default branch.

Overall Pipeline

Metric Value
p50 6m 26s
p95 7m 55s
p99 9m 3s
Current run 9m 17s
vs p95 +17.3%

Trend (last 20 runs): ▃▃▁▂▃▃▃▂▃▃▂▂▄▃▃▆▅▄█▆

Per-Job Breakdown

Job p50 p95 p99 Current vs p95
Accessibility (axe-core) 2m 5s 2m 21s 2m 23s 0s -100.0%
Commit messages (commitlint) 0s 0s 0s 35s N/A
Critical-flow E2E (Playwright) 1m 36s 1m 44s 1m 44s 5m 49s +235.6%
Migration lint (AGENTS rule 0s 0s 0s 10s N/A
Pipeline duration (p95 trend) 26s 27s 27s
Secret scan (gitleaks) 8s 11s 11s 12s +9.1%
Smoke E2E (Playwright) 1m 26s 1m 40s 1m 40s
Test coverage (vitest) 2m 4s 2m 33s 2m 33s 2m 0s -21.6%
Workflow lint (actionlint) 7s 7s 7s 8s +14.3%
check 4m 12s 4m 54s 5m 6s 50s -83.0%
tsconfig strict guard (PR-1.A) 5s 14s 14s 12s -14.3%

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/web/src/test/contract/me.contract.test.ts (1)

31-36: ⚡ Quick win

Make fixture iteration exhaustive by sourcing keys from meFixtures.

Keeping a separate name list can drift. Deriving keys from the shared fixture object keeps this contract suite automatically in sync.

Suggested change
-const FIXTURE_NAMES: readonly MeFixtureCase[] = [
-  "minimal",
-  "full",
-  "legacyNoCreatedAt",
-  "unverified",
-] as const;
+const FIXTURE_NAMES = Object.keys(meFixtures) as MeFixtureCase[];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/test/contract/me.contract.test.ts` around lines 31 - 36, Replace
the hardcoded FIXTURE_NAMES array with an exhaustive source derived from the
shared meFixtures object so the test stays in sync; locate the FIXTURE_NAMES
declaration and change it to compute the fixture keys from meFixtures (using
Object.keys/typed alternatives) and cast/validate them to MeFixtureCase so tests
iterate all fixture entries rather than a manually maintained list.
apps/server/src/routes/me.contract.test.ts (1)

112-117: ⚡ Quick win

Derive fixture names from meFixtures to avoid silent coverage gaps.

This manual list can go stale when a new fixture is added. Build the list from the fixture object so every case is exercised automatically.

Suggested change
-const NAMES: readonly MeFixtureCase[] = [
-  "minimal",
-  "full",
-  "legacyNoCreatedAt",
-  "unverified",
-] as const;
+const NAMES = Object.keys(meFixtures) as MeFixtureCase[];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/routes/me.contract.test.ts` around lines 112 - 117, The
hard-coded NAMES array can go stale; instead derive it from the meFixtures
object so new fixtures are automatically tested. Replace the manual NAMES
declaration with code that reads the keys from meFixtures (casting to
MeFixtureCase[] or otherwise asserting the type) to produce the readonly list
used by the tests (keep the symbol NAMES and type MeFixtureCase), ensuring
deterministic order if needed (e.g., sort) so test behavior remains stable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/routes/me.contract.test.ts`:
- Around line 44-57: The mocks and import use relative paths ("./../db.js",
"./../auth.js", "./../app.js"); update them to the repo's path aliases (e.g.,
replace those strings in the vi.mock calls and the createApp import with the
appropriate alias modules like the @... aliases your project uses) so the tests
and mocks resolve via configured aliases instead of relative imports; update the
vi.mock targets (the first argument strings) and the import specifier for
createApp to the corresponding alias names.

In `@packages/shared/src/contract-fixtures/me.ts`:
- Line 20: The import in packages/shared/src/contract-fixtures/me.ts currently
uses a relative path for MeResponseSchema and MeResponse; update that import to
use the repository path alias for the shared schemas (e.g., replace the relative
import of "../schemas/api" with the `@shared` alias for that module) so
MeResponseSchema and MeResponse are imported via the configured alias rather
than a relative path.

In `@packages/shared/src/contract-fixtures/README.md`:
- Around line 31-36: The fenced code block in README.md is unlabeled and
triggers markdownlint MD040; update the opening backticks of the tree block in
the README.md file to include a language identifier (for example change ``` to
```text) so the code fence is explicitly tagged (e.g., use ```text above the
directory tree).

---

Nitpick comments:
In `@apps/server/src/routes/me.contract.test.ts`:
- Around line 112-117: The hard-coded NAMES array can go stale; instead derive
it from the meFixtures object so new fixtures are automatically tested. Replace
the manual NAMES declaration with code that reads the keys from meFixtures
(casting to MeFixtureCase[] or otherwise asserting the type) to produce the
readonly list used by the tests (keep the symbol NAMES and type MeFixtureCase),
ensuring deterministic order if needed (e.g., sort) so test behavior remains
stable.

In `@apps/web/src/test/contract/me.contract.test.ts`:
- Around line 31-36: Replace the hardcoded FIXTURE_NAMES array with an
exhaustive source derived from the shared meFixtures object so the test stays in
sync; locate the FIXTURE_NAMES declaration and change it to compute the fixture
keys from meFixtures (using Object.keys/typed alternatives) and cast/validate
them to MeFixtureCase so tests iterate all fixture entries rather than a
manually maintained list.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b53896ec-0c6c-427a-b6b9-37668239d02a

📥 Commits

Reviewing files that changed from the base of the PR and between e2a4e48 and 13b6112.

📒 Files selected for processing (8)
  • apps/server/src/routes/me.contract.test.ts
  • apps/web/src/test/contract/me.contract.test.ts
  • docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md
  • docs/diagnostics/2026-05-03-web-deep-dive/04-security-observability-testing-devx.md
  • packages/shared/src/contract-fixtures/README.md
  • packages/shared/src/contract-fixtures/index.ts
  • packages/shared/src/contract-fixtures/me.ts
  • packages/shared/src/index.ts

Comment on lines +44 to +57
vi.mock("./../db.js", () => ({
default: mockPool,
pool: mockPool,
query: queryMock,
ensureSchema: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("./../auth.js", () => ({
auth: { handler: async () => new Response(null, { status: 404 }) },
getSessionUser: getSessionUserMock,
getSessionUserSoft: vi.fn().mockResolvedValue(null),
}));

import { createApp } from "./../app.js";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Switch new test-module imports/mocks to path aliases.

Please avoid new relative module paths here and use the repo’s configured aliases for consistency with import policy.

As per coding guidelines: Use path aliases (@shared/*, @finyk/*, etc.) instead of relative imports like ../../../.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/routes/me.contract.test.ts` around lines 44 - 57, The mocks
and import use relative paths ("./../db.js", "./../auth.js", "./../app.js");
update them to the repo's path aliases (e.g., replace those strings in the
vi.mock calls and the createApp import with the appropriate alias modules like
the @... aliases your project uses) so the tests and mocks resolve via
configured aliases instead of relative imports; update the vi.mock targets (the
first argument strings) and the import specifier for createApp to the
corresponding alias names.

* conditionally surfaces a "verify email" banner off this flag.
*/

import { MeResponseSchema, type MeResponse } from "../schemas/api";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use the configured path alias for this shared-schema import.

Please replace the new relative import with the repository alias form to keep new files aligned with the import policy.

As per coding guidelines: Use path aliases (@shared/*, @finyk/*, etc.) instead of relative imports like ../../../.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/contract-fixtures/me.ts` at line 20, The import in
packages/shared/src/contract-fixtures/me.ts currently uses a relative path for
MeResponseSchema and MeResponse; update that import to use the repository path
alias for the shared schemas (e.g., replace the relative import of
"../schemas/api" with the `@shared` alias for that module) so MeResponseSchema and
MeResponse are imported via the configured alias rather than a relative path.

Comment on lines +31 to +36
```
contract-fixtures/
├── README.md ← this file
├── index.ts ← barrel
└── me.ts ← /api/me canonical shapes (User, MeResponse)
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced block.

The code fence is currently unlabeled and triggers markdownlint MD040. Add a language tag (e.g. text) after the opening backticks.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 31-31: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/contract-fixtures/README.md` around lines 31 - 36, The
fenced code block in README.md is unlabeled and triggers markdownlint MD040;
update the opening backticks of the tree block in the README.md file to include
a language identifier (for example change ``` to ```text) so the code fence is
explicitly tagged (e.g., use ```text above the directory tree).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/shared/src/contract-fixtures/README.md (1)

31-31: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language identifier to the fenced code block.

The code fence on line 31 is missing a language identifier, triggering markdownlint MD040. This issue was already flagged in a previous review.

📝 Proposed fix
-```
+```text
 contract-fixtures/
 ├── README.md           ← this file
 ├── index.ts            ← barrel
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/contract-fixtures/README.md` at line 31, The fenced code
block in README.md that starts with ``` above the "contract-fixtures/" tree is
missing a language identifier (triggering markdownlint MD040); update the
opening fence to include a language (for example change ``` to ```text or
```bash) so the block becomes ```text and keep the rest of the snippet unchanged
to satisfy the linter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/shared/src/contract-fixtures/README.md`:
- Line 31: The fenced code block in README.md that starts with ``` above the
"contract-fixtures/" tree is missing a language identifier (triggering
markdownlint MD040); update the opening fence to include a language (for example
change ``` to ```text or ```bash) so the block becomes ```text and keep the rest
of the snippet unchanged to satisfy the linter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 49a4dccf-c402-4af9-8408-56a631d2a144

📥 Commits

Reviewing files that changed from the base of the PR and between 13b6112 and 268efdc.

📒 Files selected for processing (8)
  • apps/server/src/routes/me.contract.test.ts
  • apps/web/src/test/contract/me.contract.test.ts
  • docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md
  • docs/diagnostics/2026-05-03-web-deep-dive/04-security-observability-testing-devx.md
  • packages/shared/src/contract-fixtures/README.md
  • packages/shared/src/contract-fixtures/index.ts
  • packages/shared/src/contract-fixtures/me.ts
  • packages/shared/src/index.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/shared/src/index.ts
  • packages/shared/src/contract-fixtures/index.ts
  • docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/shared/src/contract-fixtures/me.ts
  • docs/diagnostics/2026-05-03-web-deep-dive/04-security-observability-testing-devx.md
  • apps/web/src/test/contract/me.contract.test.ts
  • apps/server/src/routes/me.contract.test.ts

@Skords-01 Skords-01 merged commit ce51fa9 into main May 4, 2026
23 of 40 checks passed
@Skords-01 Skords-01 deleted the devin/1777863626-contract-tests branch May 4, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant