Skip to content

test(api/chat): add DI seam for OpenAI client + mocked route tests#18

Open
sergiopesch wants to merge 1 commit into
fix/chat-route-hardeningfrom
test/openai-di-seam
Open

test(api/chat): add DI seam for OpenAI client + mocked route tests#18
sergiopesch wants to merge 1 commit into
fix/chat-route-hardeningfrom
test/openai-di-seam

Conversation

@sergiopesch

Copy link
Copy Markdown
Owner

Why

The pure-function tests added in #16 cover every branch of POST /api/chat except the ones that actually call OpenAI. That means the normalize → validate → sanitize pipeline (the bulk of the server-side logic) is exercised only through its individual unit tests, not end-to-end.

Rather than mock OpenAI at the module level (brittle, tsx-specific) or spin up a real API call in CI (flaky, needs secrets), introduce a minimal dependency-injection seam so tests can swap in a fake client.

What

app/api/chat/openai-client.ts (new)

export type ChatCompletionClient = {
  chat: { completions: { create: OpenAI['chat']['completions']['create'] } };
};
export function getChatClient(): ChatCompletionClient;
export function __setChatClientForTests(client: ChatCompletionClient | null): void;
  • Prod: lazily constructs a real new OpenAI(), same as before.
  • Tests: call __setChatClientForTests(fake) to inject, null to clear.
  • The double-underscore prefix is intentional; production code must not reach for it.

app/api/chat/route.ts

One-line swap: getClient()getChatClient(). import OpenAI from 'openai' is removed (no longer needed in this file — the concrete type lives behind the seam).

No behavior change on the happy path.

tests/chat-route-mocked.test.ts (new, 7 tests)

Drives the full handler with canned LLM outputs:

Test What it covers
returns a validated spec card from the LLM normalize + validate happy path
voice mode attaches voice.spokenSummary voice-mode branch with VOICE_PROMPT_RULES
unparseable LLM output → FALLBACK_TEXT_RESPONSE !normalized branch
partially-valid payload → rescued via toSafeTextResponse !validated + text-recovery branch
AbortError from client → FALLBACK_TEXT_RESPONSE (not 500) the timeout path added in #17
non-timeout upstream error → 500 with generic message error-leakage guard
requestedImageCount > 1 overrides LLM's imageCount post-validate override branch

The fake client also records calls, so tests can assert on the exact messages / model sent to OpenAI.

Test count

Branch Before After
#17 fix/chat-route-hardening 17 24 (+7)

Verification

  • npm run validate
  • npm test — 24/24
  • npm run build — production build succeeds

Depends on #17 (uses __resetRateLimitMapForTests from there). Stacked onto fix/chat-route-hardening.

Introduces a minimal dependency-injection seam so POST /api/chat can
be driven in tests without a live OpenAI API key:

  app/api/chat/openai-client.ts
    export type ChatCompletionClient = { chat: { completions: { create } } }
    export function getChatClient(): ChatCompletionClient
    export function __setChatClientForTests(client | null): void

Production behavior is unchanged: getChatClient() lazily constructs a
real OpenAI client when no test override is set. The route.ts handler
now imports getChatClient() instead of constructing OpenAI inline.

New tests (tests/chat-route-mocked.test.ts, 7 tests):
- Returns a validated spec card from a canned LLM response
- Voice mode attaches voice.spokenSummary
- Unparseable LLM output -> FALLBACK_TEXT_RESPONSE
- Partially-valid payload is rescued via toSafeTextResponse
- AbortError from the client -> FALLBACK_TEXT_RESPONSE (not 500)
- Non-timeout upstream error -> 500 with generic error message
- Post-normalize: requestedImageCount > 1 overrides LLM's imageCount

Together with the pure-function tests added earlier, POST /api/chat
now has end-to-end coverage for every major branch except the
happy-path forced-photo fast path (already covered).

Total: 17 -> 24 tests. All pass with validate + build.
@vercel

vercel Bot commented Apr 21, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
clitronic Ready Ready Preview, Comment Apr 21, 2026 10:31am

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant