Browse Source
Phase 18: API Route Tests - 5 plans created covering: - 18-01: File I/O routes (workflow, save-generation) - 18-02: LLM route (Google, OpenAI providers) - 18-03: Generate route (Gemini provider) - 18-04: Generate route (Replicate, fal.ai providers) - 18-05: Models route (caching, aggregation) - 10 total tasks defined - Ready for execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>handoff-20260429-1057
7 changed files with 712 additions and 11 deletions
@ -0,0 +1,122 @@ |
|||||
|
--- |
||||
|
phase: 18-api-route-tests |
||||
|
plan: 01 |
||||
|
type: execute |
||||
|
--- |
||||
|
|
||||
|
<objective> |
||||
|
Add tests for workflow and save-generation API routes covering file I/O operations. |
||||
|
|
||||
|
Purpose: Establish API route testing patterns with file system mocking for subsequent plans. |
||||
|
Output: Test files for workflow and save-generation routes with fs mocking patterns. |
||||
|
</objective> |
||||
|
|
||||
|
<execution_context> |
||||
|
~/.claude/get-shit-done/workflows/execute-phase.md |
||||
|
~/.claude/get-shit-done/templates/summary.md |
||||
|
</execution_context> |
||||
|
|
||||
|
<context> |
||||
|
@.planning/PROJECT.md |
||||
|
@.planning/ROADMAP.md |
||||
|
@.planning/STATE.md |
||||
|
|
||||
|
**Routes to test:** |
||||
|
@src/app/api/workflow/route.ts |
||||
|
@src/app/api/save-generation/route.ts |
||||
|
|
||||
|
**Test patterns reference:** |
||||
|
@src/store/utils/__tests__/localStorage.test.ts |
||||
|
@vitest.config.ts |
||||
|
</context> |
||||
|
|
||||
|
<tasks> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 1: Create workflow route tests</name> |
||||
|
<files>src/app/api/workflow/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Create tests for /api/workflow route covering: |
||||
|
- POST: Save workflow - validates required fields, creates directories, writes JSON |
||||
|
- POST: Rejects missing directoryPath/filename/workflow |
||||
|
- POST: Rejects non-directory paths |
||||
|
- POST: Handles directory creation for inputs/generations |
||||
|
- GET: Validates directory existence |
||||
|
- GET: Returns exists: false for non-existent paths |
||||
|
|
||||
|
Mock fs/promises (stat, mkdir, writeFile, readdir) with vi.mock. |
||||
|
Mock NextRequest/NextResponse using vitest patterns. |
||||
|
Use vi.spyOn for fs methods to control return values per test. |
||||
|
|
||||
|
Pattern for mocking Next.js API routes: |
||||
|
```typescript |
||||
|
import { describe, it, expect, vi, beforeEach } from "vitest"; |
||||
|
import { NextRequest } from "next/server"; |
||||
|
import * as fs from "fs/promises"; |
||||
|
|
||||
|
vi.mock("fs/promises"); |
||||
|
|
||||
|
// Create mock request helper |
||||
|
function createMockRequest(body: unknown, method = "POST") { |
||||
|
return { |
||||
|
json: vi.fn().mockResolvedValue(body), |
||||
|
nextUrl: { searchParams: new URLSearchParams() }, |
||||
|
} as unknown as NextRequest; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Do NOT mock the entire route module - import and call POST/GET directly. |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/workflow/__tests__/route.test.ts</verify> |
||||
|
<done>All workflow route tests pass covering POST save and GET validation</done> |
||||
|
</task> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 2: Create save-generation route tests</name> |
||||
|
<files>src/app/api/save-generation/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Create tests for /api/save-generation route covering: |
||||
|
- POST: Saves base64 image with hash-based filename |
||||
|
- POST: Saves base64 video with hash-based filename |
||||
|
- POST: Deduplicates existing files by hash suffix |
||||
|
- POST: Rejects missing directoryPath or content |
||||
|
- POST: Rejects non-directory paths |
||||
|
- POST: Handles data URL with various MIME types |
||||
|
- POST: Handles HTTP URLs (mock fetch for external URL) |
||||
|
|
||||
|
Mock fs/promises (stat, mkdir, writeFile, readdir) with vi.mock. |
||||
|
Mock global fetch for HTTP URL handling. |
||||
|
Use crypto.createHash for consistent hash generation in tests. |
||||
|
|
||||
|
Test the deduplication logic: |
||||
|
1. Mock readdir to return file with matching hash suffix |
||||
|
2. Verify no writeFile called, isDuplicate: true returned |
||||
|
|
||||
|
Test hash generation: |
||||
|
1. Provide known content, verify hash in filename |
||||
|
2. Use crypto.createHash('md5').update(buffer).digest('hex') to compute expected hash |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/save-generation/__tests__/route.test.ts</verify> |
||||
|
<done>All save-generation route tests pass covering save, deduplication, and validation</done> |
||||
|
</task> |
||||
|
|
||||
|
</tasks> |
||||
|
|
||||
|
<verification> |
||||
|
Before declaring plan complete: |
||||
|
- [ ] `npm test -- src/app/api/workflow/__tests__/route.test.ts` passes |
||||
|
- [ ] `npm test -- src/app/api/save-generation/__tests__/route.test.ts` passes |
||||
|
- [ ] Both test files use consistent mocking patterns |
||||
|
- [ ] No TypeScript errors in test files |
||||
|
</verification> |
||||
|
|
||||
|
<success_criteria> |
||||
|
- All tasks completed |
||||
|
- All verification checks pass |
||||
|
- Tests cover validation, success, and error paths |
||||
|
- Mocking patterns established for fs and fetch |
||||
|
</success_criteria> |
||||
|
|
||||
|
<output> |
||||
|
After completion, create `.planning/phases/18-api-route-tests/18-01-SUMMARY.md` |
||||
|
</output> |
||||
@ -0,0 +1,140 @@ |
|||||
|
--- |
||||
|
phase: 18-api-route-tests |
||||
|
plan: 02 |
||||
|
type: execute |
||||
|
--- |
||||
|
|
||||
|
<objective> |
||||
|
Add tests for LLM API route covering Google and OpenAI provider paths. |
||||
|
|
||||
|
Purpose: Test text generation API with provider mocking patterns. |
||||
|
Output: Comprehensive tests for /api/llm route with mocked external APIs. |
||||
|
</objective> |
||||
|
|
||||
|
<execution_context> |
||||
|
~/.claude/get-shit-done/workflows/execute-phase.md |
||||
|
~/.claude/get-shit-done/templates/summary.md |
||||
|
</execution_context> |
||||
|
|
||||
|
<context> |
||||
|
@.planning/PROJECT.md |
||||
|
@.planning/ROADMAP.md |
||||
|
@.planning/STATE.md |
||||
|
|
||||
|
**Route to test:** |
||||
|
@src/app/api/llm/route.ts |
||||
|
|
||||
|
**Test patterns from previous plan:** |
||||
|
@src/app/api/workflow/__tests__/route.test.ts |
||||
|
</context> |
||||
|
|
||||
|
<tasks> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 1: Create LLM route tests for Google provider</name> |
||||
|
<files>src/app/api/llm/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Create tests for /api/llm route Google provider path: |
||||
|
- POST: Generates text with Google/Gemini successfully |
||||
|
- POST: Handles multimodal input (images + prompt) |
||||
|
- POST: Rejects missing prompt |
||||
|
- POST: Rejects missing API key (no env var, no header) |
||||
|
- POST: Returns 429 on rate limit errors |
||||
|
- POST: Returns 500 on API errors |
||||
|
|
||||
|
Mock @google/genai GoogleGenAI class: |
||||
|
```typescript |
||||
|
vi.mock("@google/genai", () => ({ |
||||
|
GoogleGenAI: vi.fn().mockImplementation(() => ({ |
||||
|
models: { |
||||
|
generateContent: vi.fn().mockResolvedValue({ |
||||
|
text: "Generated response text", |
||||
|
}), |
||||
|
}, |
||||
|
})), |
||||
|
})); |
||||
|
``` |
||||
|
|
||||
|
Mock process.env.GEMINI_API_KEY for auth tests. |
||||
|
Test X-Gemini-API-Key header takes precedence over env var. |
||||
|
|
||||
|
Test request structure: |
||||
|
```typescript |
||||
|
const mockRequest = { |
||||
|
json: vi.fn().mockResolvedValue({ |
||||
|
prompt: "Test prompt", |
||||
|
provider: "google", |
||||
|
model: "gemini-2.5-flash", |
||||
|
temperature: 0.7, |
||||
|
maxTokens: 1024, |
||||
|
}), |
||||
|
headers: new Headers(), |
||||
|
} as unknown as NextRequest; |
||||
|
``` |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/llm/__tests__/route.test.ts --testNamePattern="google"</verify> |
||||
|
<done>Google provider tests pass covering success, validation, and error paths</done> |
||||
|
</task> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 2: Add LLM route tests for OpenAI provider</name> |
||||
|
<files>src/app/api/llm/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Add tests for /api/llm route OpenAI provider path to existing test file: |
||||
|
- POST: Generates text with OpenAI successfully |
||||
|
- POST: Handles vision input (images + prompt) |
||||
|
- POST: Rejects unknown provider |
||||
|
- POST: Rejects missing OpenAI API key |
||||
|
- POST: Returns 429 on rate limit errors |
||||
|
- POST: Handles OpenAI API error responses |
||||
|
|
||||
|
Mock global fetch for OpenAI API calls: |
||||
|
```typescript |
||||
|
const mockFetch = vi.fn(); |
||||
|
global.fetch = mockFetch; |
||||
|
|
||||
|
// Success response |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ |
||||
|
choices: [{ message: { content: "OpenAI response" } }], |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
// Error response |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: false, |
||||
|
status: 401, |
||||
|
json: () => Promise.resolve({ |
||||
|
error: { message: "Invalid API key" }, |
||||
|
}), |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Test X-OpenAI-API-Key header takes precedence over env var. |
||||
|
Test that correct model ID is sent to OpenAI API. |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/llm/__tests__/route.test.ts</verify> |
||||
|
<done>All LLM route tests pass covering both providers</done> |
||||
|
</task> |
||||
|
|
||||
|
</tasks> |
||||
|
|
||||
|
<verification> |
||||
|
Before declaring plan complete: |
||||
|
- [ ] `npm test -- src/app/api/llm/__tests__/route.test.ts` passes |
||||
|
- [ ] Tests cover Google and OpenAI providers |
||||
|
- [ ] Error handling tested (rate limits, API errors, missing keys) |
||||
|
- [ ] No TypeScript errors |
||||
|
</verification> |
||||
|
|
||||
|
<success_criteria> |
||||
|
- All tasks completed |
||||
|
- All verification checks pass |
||||
|
- Both provider paths tested |
||||
|
- API mocking patterns work correctly |
||||
|
</success_criteria> |
||||
|
|
||||
|
<output> |
||||
|
After completion, create `.planning/phases/18-api-route-tests/18-02-SUMMARY.md` |
||||
|
</output> |
||||
@ -0,0 +1,125 @@ |
|||||
|
--- |
||||
|
phase: 18-api-route-tests |
||||
|
plan: 03 |
||||
|
type: execute |
||||
|
--- |
||||
|
|
||||
|
<objective> |
||||
|
Add tests for generate API route covering Gemini provider path. |
||||
|
|
||||
|
Purpose: Test image generation with Gemini, the default and most common path. |
||||
|
Output: Tests for /api/generate Gemini path with mocked GoogleGenAI SDK. |
||||
|
</objective> |
||||
|
|
||||
|
<execution_context> |
||||
|
~/.claude/get-shit-done/workflows/execute-phase.md |
||||
|
~/.claude/get-shit-done/templates/summary.md |
||||
|
</execution_context> |
||||
|
|
||||
|
<context> |
||||
|
@.planning/PROJECT.md |
||||
|
@.planning/ROADMAP.md |
||||
|
@.planning/STATE.md |
||||
|
|
||||
|
**Route to test:** |
||||
|
@src/app/api/generate/route.ts |
||||
|
|
||||
|
**Test patterns from previous plans:** |
||||
|
@src/app/api/llm/__tests__/route.test.ts |
||||
|
</context> |
||||
|
|
||||
|
<tasks> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 1: Create generate route tests for Gemini provider</name> |
||||
|
<files>src/app/api/generate/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Create tests for /api/generate route Gemini (default) path: |
||||
|
- POST: Generates image successfully with prompt only |
||||
|
- POST: Generates image with prompt and input images |
||||
|
- POST: Applies aspect ratio config |
||||
|
- POST: Applies resolution config for nano-banana-pro model |
||||
|
- POST: Applies Google Search tool for nano-banana-pro |
||||
|
- POST: Rejects missing prompt (when no images/dynamicInputs) |
||||
|
- POST: Returns 500 when API key missing |
||||
|
- POST: Returns 429 on rate limit errors |
||||
|
- POST: Handles no candidates in response |
||||
|
- POST: Handles text-only response (no image) |
||||
|
|
||||
|
Mock @google/genai GoogleGenAI class: |
||||
|
```typescript |
||||
|
vi.mock("@google/genai", () => ({ |
||||
|
GoogleGenAI: vi.fn().mockImplementation(() => ({ |
||||
|
models: { |
||||
|
generateContent: vi.fn().mockResolvedValue({ |
||||
|
candidates: [{ |
||||
|
content: { |
||||
|
parts: [{ |
||||
|
inlineData: { |
||||
|
mimeType: "image/png", |
||||
|
data: "base64ImageData", |
||||
|
}, |
||||
|
}], |
||||
|
}, |
||||
|
}], |
||||
|
}), |
||||
|
}, |
||||
|
})), |
||||
|
})); |
||||
|
``` |
||||
|
|
||||
|
Test default model selection (nano-banana-pro when not specified). |
||||
|
Test X-Gemini-API-Key header takes precedence over env var. |
||||
|
Test response structure matches GenerateResponse type. |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/generate/__tests__/route.test.ts --testNamePattern="Gemini"</verify> |
||||
|
<done>Gemini provider tests pass covering success, config options, and error paths</done> |
||||
|
</task> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 2: Add generate route validation and edge case tests</name> |
||||
|
<files>src/app/api/generate/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Add validation and edge case tests to existing test file: |
||||
|
- POST: Accepts request with only images (image-to-image) |
||||
|
- POST: Accepts request with dynamicInputs containing prompt |
||||
|
- POST: Accepts request with dynamicInputs containing image frames |
||||
|
- POST: Handles large response payloads (warns but succeeds) |
||||
|
- POST: Extracts MIME type from data URL correctly |
||||
|
- POST: Falls back to image/png for raw base64 |
||||
|
|
||||
|
Test provider routing: |
||||
|
- POST: Routes to Gemini when no selectedModel provided |
||||
|
- POST: Routes to Gemini when selectedModel.provider is "gemini" |
||||
|
|
||||
|
Test the getInputMappingFromSchema helper (if exported or through behavior): |
||||
|
- Verify schema parsing maps generic names to model-specific params |
||||
|
- Test array detection for multi-image inputs |
||||
|
|
||||
|
Do NOT test Replicate/fal paths in this plan - those are in 18-04. |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/generate/__tests__/route.test.ts</verify> |
||||
|
<done>All Gemini path tests pass including validation and edge cases</done> |
||||
|
</task> |
||||
|
|
||||
|
</tasks> |
||||
|
|
||||
|
<verification> |
||||
|
Before declaring plan complete: |
||||
|
- [ ] `npm test -- src/app/api/generate/__tests__/route.test.ts` passes |
||||
|
- [ ] Tests cover Gemini provider path only (Replicate/fal in next plan) |
||||
|
- [ ] Config options tested (aspectRatio, resolution, googleSearch) |
||||
|
- [ ] Error handling tested |
||||
|
- [ ] No TypeScript errors |
||||
|
</verification> |
||||
|
|
||||
|
<success_criteria> |
||||
|
- All tasks completed |
||||
|
- All verification checks pass |
||||
|
- Gemini path thoroughly tested |
||||
|
- Input validation tested |
||||
|
</success_criteria> |
||||
|
|
||||
|
<output> |
||||
|
After completion, create `.planning/phases/18-api-route-tests/18-03-SUMMARY.md` |
||||
|
</output> |
||||
@ -0,0 +1,157 @@ |
|||||
|
--- |
||||
|
phase: 18-api-route-tests |
||||
|
plan: 04 |
||||
|
type: execute |
||||
|
--- |
||||
|
|
||||
|
<objective> |
||||
|
Add tests for generate API route covering Replicate and fal.ai provider paths. |
||||
|
|
||||
|
Purpose: Complete generate route test coverage with external provider mocking. |
||||
|
Output: Tests for /api/generate Replicate and fal.ai paths with mocked fetch calls. |
||||
|
</objective> |
||||
|
|
||||
|
<execution_context> |
||||
|
~/.claude/get-shit-done/workflows/execute-phase.md |
||||
|
~/.claude/get-shit-done/templates/summary.md |
||||
|
</execution_context> |
||||
|
|
||||
|
<context> |
||||
|
@.planning/PROJECT.md |
||||
|
@.planning/ROADMAP.md |
||||
|
@.planning/STATE.md |
||||
|
|
||||
|
**Route to test:** |
||||
|
@src/app/api/generate/route.ts |
||||
|
|
||||
|
**Existing tests to extend:** |
||||
|
@src/app/api/generate/__tests__/route.test.ts |
||||
|
</context> |
||||
|
|
||||
|
<tasks> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 1: Add generate route tests for Replicate provider</name> |
||||
|
<files>src/app/api/generate/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Add tests for /api/generate route Replicate path to existing test file: |
||||
|
- POST: Generates image successfully via Replicate |
||||
|
- POST: Generates video successfully via Replicate |
||||
|
- POST: Returns 401 when Replicate API key missing |
||||
|
- POST: Handles rate limit (429) from Replicate |
||||
|
- POST: Handles prediction failure |
||||
|
- POST: Handles prediction timeout (5 min max) |
||||
|
- POST: Polls for prediction completion |
||||
|
- POST: Returns video URL for large videos (>20MB) |
||||
|
|
||||
|
Mock global fetch for Replicate API calls: |
||||
|
```typescript |
||||
|
// Model info fetch |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ |
||||
|
latest_version: { id: "version123", openapi_schema: {} }, |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
// Create prediction |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ |
||||
|
id: "prediction123", |
||||
|
status: "starting", |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
// Poll prediction (succeeded) |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ |
||||
|
id: "prediction123", |
||||
|
status: "succeeded", |
||||
|
output: ["https://replicate.delivery/output.png"], |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
// Fetch output media |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
headers: new Headers({ "content-type": "image/png" }), |
||||
|
arrayBuffer: () => Promise.resolve(new ArrayBuffer(1024)), |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Test X-Replicate-API-Key header is passed correctly. |
||||
|
Test dynamicInputs are passed to prediction input. |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/generate/__tests__/route.test.ts --testNamePattern="Replicate"</verify> |
||||
|
<done>Replicate provider tests pass covering success, polling, and error paths</done> |
||||
|
</task> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 2: Add generate route tests for fal.ai provider</name> |
||||
|
<files>src/app/api/generate/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Add tests for /api/generate route fal.ai path to existing test file: |
||||
|
- POST: Generates image successfully via fal.ai (images array response) |
||||
|
- POST: Generates video successfully via fal.ai (video object response) |
||||
|
- POST: Works without API key (fal.ai allows unauthenticated) |
||||
|
- POST: Handles rate limit (429) with and without key |
||||
|
- POST: Handles various response formats (image, images, video, output) |
||||
|
- POST: Returns video URL for large videos (>20MB) |
||||
|
- POST: Filters empty dynamicInputs values |
||||
|
|
||||
|
Mock global fetch for fal.ai API calls: |
||||
|
```typescript |
||||
|
// Sync API call (fal.run) |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ |
||||
|
images: [{ url: "https://fal.media/output.png" }], |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
// Fetch output media |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
headers: new Headers({ "content-type": "image/png" }), |
||||
|
arrayBuffer: () => Promise.resolve(new ArrayBuffer(1024)), |
||||
|
}); |
||||
|
|
||||
|
// Video response format |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ |
||||
|
video: { url: "https://fal.media/output.mp4" }, |
||||
|
}), |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Test X-Fal-API-Key header is passed when provided. |
||||
|
Test getFalInputMapping schema parsing (via behavior). |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/generate/__tests__/route.test.ts</verify> |
||||
|
<done>All generate route tests pass covering all three providers</done> |
||||
|
</task> |
||||
|
|
||||
|
</tasks> |
||||
|
|
||||
|
<verification> |
||||
|
Before declaring plan complete: |
||||
|
- [ ] `npm test -- src/app/api/generate/__tests__/route.test.ts` passes |
||||
|
- [ ] Tests cover Replicate and fal.ai provider paths |
||||
|
- [ ] Polling logic tested for Replicate |
||||
|
- [ ] Various response formats tested for fal.ai |
||||
|
- [ ] No TypeScript errors |
||||
|
</verification> |
||||
|
|
||||
|
<success_criteria> |
||||
|
- All tasks completed |
||||
|
- All verification checks pass |
||||
|
- All three providers thoroughly tested |
||||
|
- Error handling tested for each provider |
||||
|
</success_criteria> |
||||
|
|
||||
|
<output> |
||||
|
After completion, create `.planning/phases/18-api-route-tests/18-04-SUMMARY.md` |
||||
|
</output> |
||||
@ -0,0 +1,134 @@ |
|||||
|
--- |
||||
|
phase: 18-api-route-tests |
||||
|
plan: 05 |
||||
|
type: execute |
||||
|
--- |
||||
|
|
||||
|
<objective> |
||||
|
Add tests for models API route covering caching and provider aggregation. |
||||
|
|
||||
|
Purpose: Test model discovery and caching logic for multi-provider support. |
||||
|
Output: Tests for /api/models route with mocked provider APIs and cache behavior. |
||||
|
</objective> |
||||
|
|
||||
|
<execution_context> |
||||
|
~/.claude/get-shit-done/workflows/execute-phase.md |
||||
|
~/.claude/get-shit-done/templates/summary.md |
||||
|
</execution_context> |
||||
|
|
||||
|
<context> |
||||
|
@.planning/PROJECT.md |
||||
|
@.planning/ROADMAP.md |
||||
|
@.planning/STATE.md |
||||
|
|
||||
|
**Route to test:** |
||||
|
@src/app/api/models/route.ts |
||||
|
|
||||
|
**Cache module to mock:** |
||||
|
@src/lib/providers/cache.ts |
||||
|
</context> |
||||
|
|
||||
|
<tasks> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 1: Create models route tests for basic functionality</name> |
||||
|
<files>src/app/api/models/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Create tests for /api/models route core functionality: |
||||
|
- GET: Returns models from fal.ai when no Replicate key (fal.ai works without key) |
||||
|
- GET: Returns models from both providers when both keys present |
||||
|
- GET: Returns 400 when no providers available (Replicate needs key, fal.ai not tested) |
||||
|
- GET: Filters by provider query param |
||||
|
- GET: Filters by capabilities query param |
||||
|
- GET: Searches by query param |
||||
|
- GET: Returns cached=true when all from cache |
||||
|
- GET: Returns cached=false when fresh fetch |
||||
|
|
||||
|
Mock @/lib/providers/cache: |
||||
|
```typescript |
||||
|
vi.mock("@/lib/providers/cache", () => ({ |
||||
|
getCachedModels: vi.fn(), |
||||
|
setCachedModels: vi.fn(), |
||||
|
getCacheKey: vi.fn((provider, search) => `${provider}:${search || ""}`), |
||||
|
})); |
||||
|
``` |
||||
|
|
||||
|
Mock global fetch for provider API calls: |
||||
|
```typescript |
||||
|
// Replicate models response |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ |
||||
|
results: [{ owner: "stability-ai", name: "sdxl", description: "SDXL" }], |
||||
|
next: null, |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
// fal.ai models response |
||||
|
mockFetch.mockResolvedValueOnce({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ |
||||
|
models: [{ |
||||
|
endpoint_id: "fal-ai/flux", |
||||
|
metadata: { display_name: "Flux", category: "text-to-image", description: "" }, |
||||
|
}], |
||||
|
has_more: false, |
||||
|
}), |
||||
|
}); |
||||
|
``` |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/models/__tests__/route.test.ts --testNamePattern="basic"</verify> |
||||
|
<done>Basic models route tests pass covering filtering and caching</done> |
||||
|
</task> |
||||
|
|
||||
|
<task type="auto"> |
||||
|
<name>Task 2: Add models route tests for caching and error handling</name> |
||||
|
<files>src/app/api/models/__tests__/route.test.ts</files> |
||||
|
<action> |
||||
|
Add caching and error tests to existing test file: |
||||
|
- GET: Returns cached models when available (no fetch) |
||||
|
- GET: Bypasses cache when refresh=true |
||||
|
- GET: Client-side filters Replicate models (caches full list, filters on read) |
||||
|
- GET: Handles partial provider failures gracefully |
||||
|
- GET: Returns 500 when all providers fail |
||||
|
- GET: Paginates through Replicate results (max 15 pages) |
||||
|
- GET: Paginates through fal.ai results (max 15 pages) |
||||
|
|
||||
|
Test capability inference for Replicate: |
||||
|
- Video keywords (video, animate, motion) → text-to-video |
||||
|
- Image-to-video keywords (img2vid, i2v) → image-to-video |
||||
|
- Image keywords → text-to-image |
||||
|
|
||||
|
Test fal.ai category mapping: |
||||
|
- Category maps directly to ModelCapability |
||||
|
- Non-relevant categories are filtered out |
||||
|
|
||||
|
Test sorting: |
||||
|
- Models sorted by provider, then by name |
||||
|
</action> |
||||
|
<verify>npm test -- src/app/api/models/__tests__/route.test.ts</verify> |
||||
|
<done>All models route tests pass covering caching, pagination, and error handling</done> |
||||
|
</task> |
||||
|
|
||||
|
</tasks> |
||||
|
|
||||
|
<verification> |
||||
|
Before declaring plan complete: |
||||
|
- [ ] `npm test -- src/app/api/models/__tests__/route.test.ts` passes |
||||
|
- [ ] Cache behavior tested (hit, miss, refresh) |
||||
|
- [ ] Both providers tested |
||||
|
- [ ] Filtering and search tested |
||||
|
- [ ] Error handling tested |
||||
|
- [ ] No TypeScript errors |
||||
|
</verification> |
||||
|
|
||||
|
<success_criteria> |
||||
|
- All tasks completed |
||||
|
- All verification checks pass |
||||
|
- Cache logic thoroughly tested |
||||
|
- Provider aggregation tested |
||||
|
</success_criteria> |
||||
|
|
||||
|
<output> |
||||
|
After completion, create `.planning/phases/18-api-route-tests/18-05-SUMMARY.md` |
||||
|
</output> |
||||
Loading…
Reference in new issue