Skip to content

Promote rails-api spec coverage to cross-stack tests/api (CORS, terminal status, stage IDs, salary range) #302

Description

@WhatIfWeDigDeeper

Background

While reviewing rails-api spec coverage during #300, four areas surfaced where the rails specs cover real product behavior that the cross-stack tests/api/ suite doesn't. Some are universal-but-untested, some are real contract gaps where only 1–2 stacks enforce the rule, and one is a deliberate implementation choice worth documenting.

This issue tracks the four — each with a different action because the underlying situation differs.

Inventory

Cross-checked which stacks enforce each rule today:

# Rule Enforced by Status
1 CORS preflight echoes the requesting origin every API stack (CORS middleware everywhere) universal but untested
3 Reject status transitions away from terminal states (accepted offer, declined offer) rails-api, yoga-api contract gap (8 stacks silently allow it)
4 Stage UUIDs preserved across snapshot restore rails-api only (destroy_all + recreate-with-id in ApplicationRestoreService) implementation choice; other stacks may legitimately mint new IDs
5 salary_max >= salary_min validation rails-api, yoga-api (nest-api/hono-api have min(0) Zod but no min≤max check) contract gap

(#2 default interview-stage seeding on transition to interviewing is rails-only by design and stays out of tests/api. #6 model-layer AR validation keys are too implementation-coupled to share.)

Per-item plan

#1 — Promote CORS preflight test to tests/api/

Universal behavior, zero coverage today. Easy win.

  • Add a per-stack expectedAllowedOrigin (or reuse the canonical UI port) field in tests/api/helpers.ts for each stack.
  • Add tests/api/cors.test.ts that does OPTIONS /applications with Origin: <expectedAllowedOrigin> and Access-Control-Request-Method: GET, asserts the echoed Access-Control-Allow-Origin header.
  • One test, one assertion per stack — runs against every stack via the existing harness.

Acceptance: CORS regression that breaks any stack's allowlist for the dev UI fails CI.

#3 — Standardize terminal-status transition rejection across stacks, then promote test

Almost certainly a contract gap rather than intentional divergence — accepted offer → applied doesn't make product sense. Two stacks already enforce it.

  • Decide and document the rule in the cross-stack contract (most likely: accepted offer and declined offer are terminal; reject any update that changes status away from these unless the source status equals the target).
  • Implement in the 8 stacks that don't have it: express (api/), koa-api, hono-api, nuxt-api, nest-api, nest-history-api, fastapi, go-api, spring-api, lambda-api.
  • Add a shared test in tests/api/application-status.test.ts that creates an application in accepted offer, attempts a PATCH to applied, asserts 400 with a validation_error-shaped response.

Acceptance: every stack rejects the transition with a 400; shared test passes for all stacks.

#4 — Document stage-ID preservation as deliberately stack-specific

Rails uses destroy_all + recreate-with-original-UUID; other stacks haven't made an explicit choice. Locking it as a contract would force every stack into the rails strategy without a clear product driver — and "restore mints new stage IDs" is a defensible model (treats restored stages as new entities).

  • Add a section to tests/CLAUDE.md or docs/DATABASE_ARCHITECTURE.md noting that stage UUID stability across history restore is not guaranteed by the cross-stack contract; clients depending on stable stage IDs should query the history endpoint instead.
  • No shared test added.
  • Optional: add a per-stack capability flag (preservesStageIdsOnRestore: boolean) in tests/api/helpers.ts for documentation/feature-gating purposes; only set true for rails-api initially.

Acceptance: behavior is documented; no implicit assumption baked into shared tests.

#5 — Standardize salary range ordering, then promote test

Same shape as #3. Two stacks enforce; the rest accept salaryMin: 200000, salaryMax: 100000 silently. The rule is unambiguous product behavior, not flavor.

  • Decide and document the rule (salary_max >= salary_min when both present).
  • Implement in the stacks that don't have it.
  • Add a shared test in tests/api/application-crud.test.ts (or a new tests/api/validation.test.ts) that POSTs an inverted salary range, asserts 400 with field-level error on salaryMax.

Acceptance: every stack rejects inverted salary range with a 400; shared test passes.

Suggested PR ordering

  1. feat: Job Application Tracker - complete implementation #1 CORS test — independent, low-risk, can land first as a small standalone PR.
  2. Add dark mode to filters section #4 documentation note — also independent, can fold into PR 1 or land separately.
  3. reveals status control + unsubmitted #3 + Improve modal dimensions and dark mode support #5 contract standardization — bigger; touches 8+ stacks. Could be one PR per stack to keep diffs reviewable, or one PR per rule (terminal-status across all stacks, then salary range across all stacks).

Context

Originally surfaced during PR #300 (rails-api implementation) when comparing rails-api/spec coverage to tests/api/ coverage.

Companion to #301 (CI workflow restructure) — both came out of the same PR review cycle but are independent of each other.

Addendum: contract-vs-implementation drift on specialRequirements maxLength

While reviewing rails-api in #300, surfaced another cross-stack drift worth folding into the same standardization pass:

Source maxLength enforced
specs/core/api/openapi.yaml (the contract) 1000
fastapi/src/schemas.py 5000
nest-api/src/types/api.ts 5000
hono-api/src/types/api.ts 5000
lambda-api/src/types/api.ts 5000
rails-api/app/models/job_application.rb (this PR) 5000

Every implementation diverges from the contract by the same amount. Two paths:

  • A — update specs/core/api/openapi.yaml to maxLength: 5000 (treat the implementations as the source of truth; the contract was wrong).
  • B — tighten all five implementations to maxLength: 1000 (treat the contract as binding; implementations drifted independently).

A is the lower-friction path: no behavior change for users, no breaking change for API consumers. The specialRequirements field is freeform notes; 5000 is the more useful limit. B requires updating every stack's validator, which is more code change for no real product benefit unless the contract value is being defended for a specific reason (and we should document why).

Recommend A: update the OpenAPI contract to 5000. Add a shared API test in tests/api/ that posts a 4000-char specialRequirements and asserts 200 — locks in the convention going forward.

Surfaced as a Copilot review comment on #300; declined there because aligning rails alone to 1000 would create a new cross-stack gap.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions