diff --git a/src/app/api/documents/[id]/labels/route.ts b/src/app/api/documents/[id]/labels/route.ts index ffcb91cbe..789363399 100644 --- a/src/app/api/documents/[id]/labels/route.ts +++ b/src/app/api/documents/[id]/labels/route.ts @@ -8,7 +8,6 @@ import { createAdminClient } from "@/lib/supabase/admin"; import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; import type { DocumentLabel, DocumentLabelType } from "@/lib/types"; import { parseJsonBody } from "@/lib/validation/body"; -import { parseRouteParams } from "@/lib/validation/params"; export const runtime = "nodejs"; @@ -41,9 +40,6 @@ const manualLabelUpdateSchema = manualLabelSchema.extend({ const manualLabelDeleteSchema = z.object({ labelId: z.string().uuid(), }); -const labelsRouteParamsSchema = z.object({ - id: z.string().uuid(), -}); function parseManualLabel(input: z.infer) { const normalized = normalizeDocumentLabelForStorage({ @@ -88,8 +84,7 @@ async function selectLabels(supabase: ReturnType, docu export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) { try { - const { id: rawId } = await params; - const { id } = parseRouteParams({ id: rawId }, labelsRouteParamsSchema, "Invalid document id."); + const { id } = await params; if (isDemoMode()) { return NextResponse.json({ error: "Demo documents cannot be curated." }, { status: 400 }); } @@ -146,8 +141,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) { try { - const { id: rawId } = await params; - const { id } = parseRouteParams({ id: rawId }, labelsRouteParamsSchema, "Invalid document id."); + const { id } = await params; if (isDemoMode()) { return NextResponse.json({ error: "Demo documents cannot be curated." }, { status: 400 }); } @@ -212,8 +206,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) { try { - const { id: rawId } = await params; - const { id } = parseRouteParams({ id: rawId }, labelsRouteParamsSchema, "Invalid document id."); + const { id } = await params; if (isDemoMode()) { return NextResponse.json({ error: "Demo documents cannot be curated." }, { status: 400 }); } diff --git a/src/app/api/documents/[id]/reindex/route.ts b/src/app/api/documents/[id]/reindex/route.ts index 2c34d38c9..88c4b85f8 100644 --- a/src/app/api/documents/[id]/reindex/route.ts +++ b/src/app/api/documents/[id]/reindex/route.ts @@ -198,6 +198,6 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: return NextResponse.json({ job }, { status: 201 }); } catch (error) { if (error instanceof AuthenticationError) return unauthorizedResponse(); - return jsonError(error); + return jsonError(error, 400); } } diff --git a/src/app/api/documents/[id]/summarize/route.ts b/src/app/api/documents/[id]/summarize/route.ts index 0c6e2594b..f779aa8e6 100644 --- a/src/app/api/documents/[id]/summarize/route.ts +++ b/src/app/api/documents/[id]/summarize/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from "next/server"; -import { z } from "zod"; import { demoSummary, getDemoDocument } from "@/lib/demo-data"; import { isDemoMode } from "@/lib/env"; import { summarizeDocument } from "@/lib/rag"; @@ -7,18 +6,12 @@ import { jsonError } from "@/lib/http"; import { consumeApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit"; import { createAdminClient } from "@/lib/supabase/admin"; import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; -import { parseRouteParams } from "@/lib/validation/params"; export const runtime = "nodejs"; -const summarizeRouteParamsSchema = z.object({ - id: z.string().uuid(), -}); - export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) { try { - const { id: rawId } = await params; - const { id } = parseRouteParams({ id: rawId }, summarizeRouteParamsSchema, "Invalid document id."); + const { id } = await params; if (isDemoMode()) { if (!getDemoDocument(id)) { return NextResponse.json({ error: "Demo document not found." }, { status: 404 }); @@ -39,6 +32,6 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: if (error instanceof Error && error.message === "Document not found.") { return NextResponse.json({ error: "Document not found." }, { status: 404 }); } - return jsonError(error); + return jsonError(error, 400); } } diff --git a/src/app/api/ingestion/batches/route.ts b/src/app/api/ingestion/batches/route.ts index 82fbfdfe8..a57c3386c 100644 --- a/src/app/api/ingestion/batches/route.ts +++ b/src/app/api/ingestion/batches/route.ts @@ -1,19 +1,13 @@ import { NextResponse } from "next/server"; -import { z } from "zod"; import { isDemoMode } from "@/lib/env"; import { jsonError } from "@/lib/http"; import { createAdminClient } from "@/lib/supabase/admin"; import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; -import { parseRequestQuery, queryInteger } from "@/lib/validation/query"; export const runtime = "nodejs"; const ACTIVE_BATCH_STATUSES = new Set(["queued", "processing"]); const ACTIVE_INDEXING_POLL_MS = 5_000; -const ingestionBatchesQuerySchema = z.object({ - limit: queryInteger({ fallback: 20, min: 1, max: 200 }), - offset: queryInteger({ fallback: 0, min: 0, max: 10_000 }), -}); type BatchRow = Record & { status?: string | null }; @@ -46,34 +40,19 @@ function batchesResponse(batches: BatchRow[], extra: Record = { export async function GET(request: Request) { try { - const { limit, offset } = parseRequestQuery(request, ingestionBatchesQuerySchema, "Invalid ingestion batches query."); - if (isDemoMode()) { - return batchesResponse([], { - demoMode: true, - pagination: { limit, offset, total: 0, nextOffset: offset, hasMore: false }, - }); - } + if (isDemoMode()) return batchesResponse([], { demoMode: true }); const supabase = createAdminClient(); const user = await requireAuthenticatedUser(request, supabase); - const { data, error, count } = await supabase + const { data, error } = await supabase .from("import_batches") - .select("*", { count: "exact" }) + .select("*") .eq("owner_id", user.id) .order("created_at", { ascending: false }) - .range(offset, offset + limit - 1); + .limit(20); if (error) throw new Error(error.message); - const batches = (data ?? []) as unknown as BatchRow[]; - return batchesResponse(batches, { - pagination: { - limit, - offset, - total: count ?? batches.length, - nextOffset: offset + batches.length, - hasMore: count === null ? batches.length === limit : offset + batches.length < count, - }, - }); + return batchesResponse((data ?? []) as unknown as BatchRow[]); } catch (error) { if (error instanceof AuthenticationError) return unauthorizedResponse(); return jsonError(error); diff --git a/src/app/api/ingestion/jobs/[id]/retry/route.ts b/src/app/api/ingestion/jobs/[id]/retry/route.ts index 4e6b4285f..5510fd1b2 100644 --- a/src/app/api/ingestion/jobs/[id]/retry/route.ts +++ b/src/app/api/ingestion/jobs/[id]/retry/route.ts @@ -1,23 +1,16 @@ import { NextResponse } from "next/server"; -import { z } from "zod"; import { env, isDemoMode } from "@/lib/env"; import { jsonError } from "@/lib/http"; import { createAdminClient } from "@/lib/supabase/admin"; import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; -import { parseRouteParams } from "@/lib/validation/params"; export const runtime = "nodejs"; -const ingestionRetryRouteParamsSchema = z.object({ - id: z.string().uuid(), -}); - export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) { try { if (isDemoMode()) return NextResponse.json({ error: "Retry is unavailable in demo mode." }, { status: 400 }); - const { id: rawId } = await params; - const { id } = parseRouteParams({ id: rawId }, ingestionRetryRouteParamsSchema, "Invalid ingestion job id."); + const { id } = await params; const supabase = createAdminClient(); const user = await requireAuthenticatedUser(request, supabase); @@ -86,6 +79,6 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: return NextResponse.json({ job: data }); } catch (error) { if (error instanceof AuthenticationError) return unauthorizedResponse(); - return jsonError(error); + return jsonError(error, 400); } } diff --git a/src/app/api/ingestion/jobs/route.ts b/src/app/api/ingestion/jobs/route.ts index d71ef0fd2..8977d71fb 100644 --- a/src/app/api/ingestion/jobs/route.ts +++ b/src/app/api/ingestion/jobs/route.ts @@ -4,7 +4,7 @@ import { isDemoMode } from "@/lib/env"; import { jsonError } from "@/lib/http"; import { createAdminClient } from "@/lib/supabase/admin"; import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; -import { optionalUuidQuery, parseRequestQuery, queryInteger } from "@/lib/validation/query"; +import { optionalUuidQuery, parseRequestQuery } from "@/lib/validation/query"; export const runtime = "nodejs"; @@ -13,8 +13,6 @@ const ACTIVE_INDEXING_POLL_MS = 5_000; const ingestionJobsQuerySchema = z.object({ batchId: optionalUuidQuery(), - limit: queryInteger({ fallback: 100, min: 1, max: 200 }), - offset: queryInteger({ fallback: 0, min: 0, max: 10_000 }), }); type JobRow = Record & { status?: string | null }; @@ -48,38 +46,24 @@ function jobsResponse(jobs: JobRow[], extra: Record = {}) { export async function GET(request: Request) { try { - const { batchId, limit, offset } = parseRequestQuery(request, ingestionJobsQuerySchema, "Invalid ingestion jobs query."); - if (isDemoMode()) { - return jobsResponse([], { - demoMode: true, - pagination: { limit, offset, total: 0, nextOffset: offset, hasMore: false }, - }); - } + if (isDemoMode()) return jobsResponse([], { demoMode: true }); const supabase = createAdminClient(); const user = await requireAuthenticatedUser(request, supabase); + const { batchId } = parseRequestQuery(request, ingestionJobsQuerySchema, "Invalid ingestion jobs query."); let query = supabase .from("ingestion_jobs") - .select("*, documents!inner(title,file_name,status,owner_id)", { count: "exact" }) + .select("*, documents!inner(title,file_name,status,owner_id)") .eq("documents.owner_id", user.id) .order("created_at", { ascending: false }) - .range(offset, offset + limit - 1); + .limit(100); if (batchId) query = query.eq("batch_id", batchId); - const { data, error, count } = await query; + const { data, error } = await query; if (error) throw new Error(error.message); - const jobs = (data ?? []) as unknown as JobRow[]; - return jobsResponse(jobs, { - pagination: { - limit, - offset, - total: count ?? jobs.length, - nextOffset: offset + jobs.length, - hasMore: count === null ? jobs.length === limit : offset + jobs.length < count, - }, - }); + return jobsResponse((data ?? []) as unknown as JobRow[]); } catch (error) { if (error instanceof AuthenticationError) return unauthorizedResponse(); return jsonError(error); diff --git a/src/app/api/jobs/route.ts b/src/app/api/jobs/route.ts index a1f39092f..e8f3ca6af 100644 --- a/src/app/api/jobs/route.ts +++ b/src/app/api/jobs/route.ts @@ -1,21 +1,15 @@ import { NextResponse } from "next/server"; -import { z } from "zod"; import { demoJobs } from "@/lib/demo-data"; import { isDemoMode } from "@/lib/env"; import { jsonError } from "@/lib/http"; import { createAdminClient } from "@/lib/supabase/admin"; import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; -import { parseRequestQuery, queryInteger } from "@/lib/validation/query"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const ACTIVE_JOB_STATUSES = new Set(["pending", "processing"]); const ACTIVE_INDEXING_POLL_MS = 5_000; -const jobsQuerySchema = z.object({ - limit: queryInteger({ fallback: 30, min: 1, max: 200 }), - offset: queryInteger({ fallback: 0, min: 0, max: 10_000 }), -}); type JobRow = Record & { status?: string | null }; @@ -48,41 +42,21 @@ function jobsResponse(jobs: JobRow[], extra: Record = {}) { export async function GET(request: Request) { try { - const { limit, offset } = parseRequestQuery(request, jobsQuerySchema, "Invalid jobs query."); if (isDemoMode()) { - const jobs = demoJobs.slice(offset, offset + limit); - return jobsResponse(jobs, { - demoMode: true, - pagination: { - limit, - offset, - total: demoJobs.length, - nextOffset: offset + jobs.length, - hasMore: offset + jobs.length < demoJobs.length, - }, - }); + return jobsResponse(demoJobs, { demoMode: true }); } const supabase = createAdminClient(); const user = await requireAuthenticatedUser(request, supabase); - const { data, error, count } = await supabase + const { data, error } = await supabase .from("ingestion_jobs") - .select("*, documents!inner(title,file_name,status)", { count: "exact" }) + .select("*, documents!inner(title,file_name,status)") .eq("documents.owner_id", user.id) .order("created_at", { ascending: false }) - .range(offset, offset + limit - 1); + .limit(30); if (error) throw new Error(error.message); - const jobs = (data ?? []) as unknown as JobRow[]; - return jobsResponse(jobs, { - pagination: { - limit, - offset, - total: count ?? jobs.length, - nextOffset: offset + jobs.length, - hasMore: count === null ? jobs.length === limit : offset + jobs.length < count, - }, - }); + return jobsResponse((data ?? []) as unknown as JobRow[]); } catch (error) { if (error instanceof AuthenticationError) { return unauthorizedResponse(); diff --git a/src/app/api/search/interaction/route.ts b/src/app/api/search/interaction/route.ts index 8b1d6db78..1cbf061d2 100644 --- a/src/app/api/search/interaction/route.ts +++ b/src/app/api/search/interaction/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; import { normalizedClinicalSearchTokens } from "@/lib/clinical-search"; import { isDemoMode } from "@/lib/env"; -import { jsonError } from "@/lib/http"; +import { PublicApiError } from "@/lib/http"; import { normalizedQueryTextForStorage, queryDerivedTokensForStorage, @@ -114,6 +114,12 @@ export async function POST(request: Request) { if (error instanceof serverAuth.AuthenticationError) { return serverAuth.unauthorizedResponse(error); } - return jsonError(error); + if (error instanceof z.ZodError) { + return NextResponse.json({ ok: false }, { status: 400 }); + } + if (error instanceof PublicApiError) { + return NextResponse.json({ ok: false }, { status: error.status }); + } + return NextResponse.json({ ok: false }, { status: 500 }); } } diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index b118535cb..6b1e425fd 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -174,6 +174,6 @@ export async function POST(request: Request) { return unauthorizedResponse(); } - return jsonError(error); + return jsonError(error, 400); } } diff --git a/tests/api-route-coverage.test.ts b/tests/api-route-coverage.test.ts index 14ba98fba..1b2490dcf 100644 --- a/tests/api-route-coverage.test.ts +++ b/tests/api-route-coverage.test.ts @@ -21,16 +21,15 @@ type BatchRow = { status: string; }; -function createQueryMock(result: { data: T; error: { message: string } | null; count?: number | null }) { +function createQueryMock(result: { data: T; error: { message: string } | null }) { const chain = { select: null as unknown as ReturnType, eq: null as unknown as ReturnType, in: null as unknown as ReturnType, order: null as unknown as ReturnType, limit: null as unknown as ReturnType, - range: null as unknown as ReturnType, then: ( - resolve: (value: { data: T; error: { message: string } | null; count?: number | null }) => void, + resolve: (value: { data: T; error: { message: string } | null }) => void, reject?: (reason?: unknown) => void, ) => Promise.resolve(result).then(resolve, reject), } as { @@ -39,9 +38,8 @@ function createQueryMock(result: { data: T; error: { message: string } | null in: ReturnType; order: ReturnType; limit: ReturnType; - range: ReturnType; then: ( - resolve: (value: { data: T; error: { message: string } | null; count?: number | null }) => void, + resolve: (value: { data: T; error: { message: string } | null }) => void, reject?: (reason?: unknown) => void, ) => Promise; }; @@ -51,7 +49,6 @@ function createQueryMock(result: { data: T; error: { message: string } | null chain.in = vi.fn(() => chain); chain.order = vi.fn(() => chain); chain.limit = vi.fn(() => chain); - chain.range = vi.fn(() => chain); return chain; } @@ -146,13 +143,6 @@ describe("/api/ingestion/jobs", () => { hasActiveJobs: false, pollAfterMs: null, demoMode: true, - pagination: { - limit: 100, - offset: 0, - total: 0, - nextOffset: 0, - hasMore: false, - }, }); expect(response.headers.get("x-indexing-active")).toBe("false"); expect(createAdminClient).not.toHaveBeenCalled(); diff --git a/tests/api-validation-contract.test.ts b/tests/api-validation-contract.test.ts index b1b86e2ae..00a107f10 100644 --- a/tests/api-validation-contract.test.ts +++ b/tests/api-validation-contract.test.ts @@ -352,147 +352,6 @@ describe("API validation contracts", () => { expect(client.calls[0].filters).not.toContainEqual({ column: "batch_id", value: "" }); }); - it("returns pagination metadata for jobs feeds", async () => { - const client = createSupabaseMock((call) => { - if (call.table === "ingestion_jobs") return ok([{ id: "job-2", status: "pending" }], 5); - return ok([]); - }); - mockRuntime(client); - const jobsRoute = await import("../src/app/api/jobs/route"); - const ingestionJobsRoute = await import("../src/app/api/ingestion/jobs/route"); - const batchesRoute = await import("../src/app/api/ingestion/batches/route"); - - const jobsResponse = await jobsRoute.GET(authenticatedRequest("/api/jobs?limit=2&offset=1")); - const ingestionJobsResponse = await ingestionJobsRoute.GET(authenticatedRequest("/api/ingestion/jobs?limit=2&offset=1")); - const batchesResponse = await batchesRoute.GET(authenticatedRequest("/api/ingestion/batches?limit=2&offset=1")); - - expect(jobsResponse.status).toBe(200); - expect(await payload(jobsResponse)).toMatchObject({ - pagination: { limit: 2, offset: 1, total: 5, nextOffset: 2, hasMore: true }, - }); - expect(client.calls[0]).toMatchObject({ table: "ingestion_jobs", range: { from: 1, to: 2 } }); - - expect(ingestionJobsResponse.status).toBe(200); - expect(await payload(ingestionJobsResponse)).toMatchObject({ - pagination: { limit: 2, offset: 1, total: 5, nextOffset: 2, hasMore: true }, - }); - expect(client.calls[1]).toMatchObject({ table: "ingestion_jobs", range: { from: 1, to: 2 } }); - - expect(batchesResponse.status).toBe(200); - expect(await payload(batchesResponse)).toMatchObject({ - pagination: { limit: 2, offset: 1, total: 0, nextOffset: 1, hasMore: false }, - }); - expect(client.calls[2]).toMatchObject({ table: "import_batches", range: { from: 1, to: 2 } }); - }); - - it("validates UUID route params for retry, summarize, and labels endpoints", async () => { - const client = createSupabaseMock(); - mockRuntime(client); - const retryRoute = await import("../src/app/api/ingestion/jobs/[id]/retry/route"); - const summarizeRoute = await import("../src/app/api/documents/[id]/summarize/route"); - const labelsRoute = await import("../src/app/api/documents/[id]/labels/route"); - - const retryResponse = await retryRoute.POST(authenticatedRequest("/api/ingestion/jobs/not-a-uuid/retry", { method: "POST" }), { - params: Promise.resolve({ id: "not-a-uuid" }), - }); - const summarizeResponse = await summarizeRoute.POST( - authenticatedRequest("/api/documents/not-a-uuid/summarize", { method: "POST" }), - { params: Promise.resolve({ id: "not-a-uuid" }) }, - ); - const labelsResponse = await labelsRoute.POST( - authenticatedRequest("/api/documents/not-a-uuid/labels", { - method: "POST", - body: JSON.stringify({ label: "Lithium", label_type: "medication" }), - }), - { params: Promise.resolve({ id: "not-a-uuid" }) }, - ); - - expect(retryResponse.status).toBe(400); - expect(await payload(retryResponse)).toEqual({ error: "Invalid ingestion job id." }); - expect(summarizeResponse.status).toBe(400); - expect(await payload(summarizeResponse)).toEqual({ error: "Invalid document id." }); - expect(labelsResponse.status).toBe(400); - expect(await payload(labelsResponse)).toEqual({ error: "Invalid document id." }); - expect(client.from).not.toHaveBeenCalled(); - }); - - it("returns 5xx envelopes for internal failures in upload, retry, reindex, and summarize routes", async () => { - const availableRateLimit = { - data: [ - { - limited: false, - limit_value: 100, - remaining: 99, - retry_after_seconds: 60, - reset_at: new Date(Date.now() + 60_000).toISOString(), - }, - ], - error: null, - }; - const uploadClient = createSupabaseMock((call) => { - if (call.table === "documents" && call.operation === "select" && call.maybeSingle) return ok(null); - if (call.table === "documents" && call.operation === "insert" && call.single) { - return { data: null, error: { message: "insert failed" } }; - } - return ok([]); - }); - mockRuntime(uploadClient); - const uploadRoute = await import("../src/app/api/upload/route"); - const formData = new FormData(); - formData.set("file", new File(["%PDF-1.7"], "guideline.pdf", { type: "application/pdf" })); - const uploadResponse = await uploadRoute.POST(authenticatedRequest("/api/upload", { method: "POST", body: formData })); - expect(uploadResponse.status).toBe(500); - expect(await payload(uploadResponse)).toEqual({ error: "Request failed." }); - - const retryClient = createSupabaseMock((call) => { - if (call.table === "ingestion_jobs" && call.operation === "select" && call.maybeSingle) { - return { data: null, error: { message: "query failed" } }; - } - return ok([]); - }); - mockRuntime(retryClient); - const retryRoute = await import("../src/app/api/ingestion/jobs/[id]/retry/route"); - const retryResponse = await retryRoute.POST(authenticatedRequest(`/api/ingestion/jobs/${documentId}/retry`, { method: "POST" }), { - params: Promise.resolve({ id: documentId }), - }); - expect(retryResponse.status).toBe(500); - expect(await payload(retryResponse)).toEqual({ error: "Request failed." }); - - const reindexClient = createSupabaseMock((call) => { - if (call.table === "documents" && call.operation === "select" && call.maybeSingle) { - return { data: null, error: { message: "query failed" } }; - } - return ok([]); - }); - reindexClient.rpc.mockImplementation(async () => availableRateLimit); - mockRuntime(reindexClient); - const reindexRoute = await import("../src/app/api/documents/[id]/reindex/route"); - const reindexResponse = await reindexRoute.POST( - authenticatedRequest(`/api/documents/${documentId}/reindex`, { - method: "POST", - body: JSON.stringify({ mode: "full" }), - }), - { params: Promise.resolve({ id: documentId }) }, - ); - expect(reindexResponse.status).toBe(500); - expect(await payload(reindexResponse)).toEqual({ error: "Request failed." }); - - const summarizeClient = createSupabaseMock(); - summarizeClient.rpc.mockImplementation(async () => availableRateLimit); - const summarizeDocument = vi.fn(async () => { - throw new Error("Upstream unavailable"); - }); - mockRuntime(summarizeClient); - vi.doMock("@/lib/rag", () => ({ summarizeDocument })); - const summarizeRoute = await import("../src/app/api/documents/[id]/summarize/route"); - const summarizeResponse = await summarizeRoute.POST( - authenticatedRequest(`/api/documents/${documentId}/summarize`, { method: "POST" }), - { params: Promise.resolve({ id: documentId }) }, - ); - expect(summarizeResponse.status).toBe(500); - expect(await payload(summarizeResponse)).toEqual({ error: "Request failed." }); - }); - it("rejects invalid upload metadata before storage upload or database writes", async () => { const client = createSupabaseMock(); mockRuntime(client); diff --git a/tests/private-access-routes.test.ts b/tests/private-access-routes.test.ts index 2cff6e9a7..ce2291fc7 100644 --- a/tests/private-access-routes.test.ts +++ b/tests/private-access-routes.test.ts @@ -996,13 +996,7 @@ describe("private document API access", () => { // update resolves with no row → refuse with 409. const client = createSupabaseMock((call) => { if (call.table === "ingestion_jobs" && call.operation === "select") { - return ok({ - id: "99999999-9999-4999-8999-999999999999", - document_id: documentId, - batch_id: null, - status: "processing", - locked_at: null, - }); + return ok({ id: "job-1", document_id: documentId, batch_id: null, status: "processing", locked_at: null }); } if (call.table === "ingestion_jobs" && call.operation === "update") { // Guard rejected the reset: no row affected. @@ -1013,12 +1007,9 @@ describe("private document API access", () => { mockRuntime(client); const { POST } = await import("../src/app/api/ingestion/jobs/[id]/retry/route"); - const response = await POST( - authenticatedRequest("/api/ingestion/jobs/99999999-9999-4999-8999-999999999999/retry", { method: "POST" }), - { - params: Promise.resolve({ id: "99999999-9999-4999-8999-999999999999" }), - }, - ); + const response = await POST(authenticatedRequest(`/api/ingestion/jobs/job-1/retry`, { method: "POST" }), { + params: Promise.resolve({ id: "job-1" }), + }); expect(response.status).toBe(409); expect(String((await payload(response)).error)).toContain("still being processed"); @@ -1033,30 +1024,21 @@ describe("private document API access", () => { it("re-queues a stale/non-processing job without resetting the live index (IDX-C3, IDX-H1, B6)", async () => { const client = createSupabaseMock((call) => { if (call.table === "ingestion_jobs" && call.operation === "select") { - return ok({ - id: "99999999-9999-4999-8999-999999999999", - document_id: documentId, - batch_id: null, - status: "failed", - locked_at: null, - }); + return ok({ id: "job-1", document_id: documentId, batch_id: null, status: "failed", locked_at: null }); } if (call.table === "documents" && call.operation === "update") return ok([]); if (call.table === "ingestion_jobs" && call.operation === "update") { // Guard allowed the reset: one row affected. - return ok({ id: "99999999-9999-4999-8999-999999999999", document_id: documentId, status: "pending" }); + return ok({ id: "job-1", document_id: documentId, status: "pending" }); } return ok([]); }); mockRuntime(client); const { POST } = await import("../src/app/api/ingestion/jobs/[id]/retry/route"); - const response = await POST( - authenticatedRequest("/api/ingestion/jobs/99999999-9999-4999-8999-999999999999/retry", { method: "POST" }), - { - params: Promise.resolve({ id: "99999999-9999-4999-8999-999999999999" }), - }, - ); + const response = await POST(authenticatedRequest(`/api/ingestion/jobs/job-1/retry`, { method: "POST" }), { + params: Promise.resolve({ id: "job-1" }), + }); const documentUpdate = client.calls.find((call) => call.table === "documents" && call.operation === "update"); expect(response.status).toBe(200); @@ -1507,7 +1489,7 @@ describe("private document API access", () => { ); const uploadPath = client.storageMocks.upload.mock.calls[0]?.[0] as string; - expect(response.status).toBe(500); + expect(response.status).toBe(400); expect(client.storageMocks.remove).toHaveBeenCalledWith([uploadPath]); }); @@ -1534,7 +1516,7 @@ describe("private document API access", () => { ); const uploadPath = client.storageMocks.upload.mock.calls[0]?.[0] as string; - expect(response.status).toBe(500); + expect(response.status).toBe(400); expect(client.storageMocks.remove).toHaveBeenCalledWith([uploadPath]); }); diff --git a/tests/search-interaction-route.test.ts b/tests/search-interaction-route.test.ts index 0c34f0bad..e4c5d3d43 100644 --- a/tests/search-interaction-route.test.ts +++ b/tests/search-interaction-route.test.ts @@ -63,36 +63,7 @@ describe("/api/search/interaction", () => { const response = await POST(request({ query: "", documentId: "not-a-document-id" })); expect(response.status).toBe(400); - await expect(response.json()).resolves.toEqual({ error: "Invalid interaction request." }); - }); - - it("returns the shared server-error envelope when persistence fails", async () => { - const documentLookup = { - select: vi.fn(() => documentLookup), - eq: vi.fn(() => documentLookup), - maybeSingle: vi.fn(async () => ({ data: null, error: { message: "db unavailable" } })), - }; - const client = { - from: vi.fn(() => documentLookup), - }; - vi.doMock("@/lib/env", () => ({ env: { RAG_PERSIST_RAW_QUERY_TEXT: false }, isDemoMode: () => false })); - vi.doMock("@/lib/supabase/admin", () => ({ createAdminClient: () => client })); - vi.doMock("@/lib/supabase/auth", () => ({ - AuthenticationError: class AuthenticationError extends Error {}, - requireAuthenticatedUser: vi.fn(async () => ({ id: userId })), - unauthorizedResponse: () => Response.json({ error: "Authentication required." }, { status: 401 }), - })); - const { POST } = await import("../src/app/api/search/interaction/route"); - - const response = await POST( - request({ - query: "clozapine monitoring", - documentId, - }), - ); - - expect(response.status).toBe(500); - await expect(response.json()).resolves.toEqual({ error: "Request failed." }); + await expect(response.json()).resolves.toEqual({ ok: false }); }); it("stores owned clicked document and chunk ids with sanitized labels", async () => {