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.

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