From f90ddab454a7230ae912c4ead0b20af1048416b9 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 16 Jun 2026 22:58:28 +0800 Subject: [PATCH] test: cover llm workflow through api --- README.md | 7 +++-- src/app/api/__tests__/jobs.test.ts | 50 +++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7a241fc..302ea7d 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,11 @@ OPENAI_API_KEY= OPENAI_MODEL=gpt-4.1-mini ``` -When no API key is configured, deterministic local fallbacks keep the workflow -usable for tests and local review. +When a DeepSeek or OpenAI-compatible key is configured, the workflow uses the +provider for fact extraction, article optimization, QA enrichment, and targeted +rewrite. Every LLM response is validated with Zod before use. When no API key is +configured, or when the provider response is invalid, deterministic local +fallbacks keep the workflow usable for tests and local review. All API requests require the configured access key: diff --git a/src/app/api/__tests__/jobs.test.ts b/src/app/api/__tests__/jobs.test.ts index 0056f45..a80e141 100644 --- a/src/app/api/__tests__/jobs.test.ts +++ b/src/app/api/__tests__/jobs.test.ts @@ -2,7 +2,21 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const llmMocks = vi.hoisted(() => ({ + generateValidatedJson: vi.fn(), +})); + +vi.mock("../../../lib/llm/client", async () => { + const actual = await vi.importActual( + "../../../lib/llm/client", + ); + return { + ...actual, + generateValidatedJson: llmMocks.generateValidatedJson, + }; +}); import { POST as confirmFactCard } from "../jobs/[jobId]/confirm-fact-card/route"; import { GET as downloadExport } from "../jobs/[jobId]/exports/[fileName]/route"; @@ -52,6 +66,7 @@ describe("job API routes", () => { process.env.APP_DATA_DIR = originalDataDir; process.env.API_ACCESS_KEY = originalApiKey; process.env.API_AUTH_DISABLED = originalAuthDisabled; + llmMocks.generateValidatedJson.mockReset(); rmSync(tempDir, { recursive: true, force: true }); }); @@ -131,6 +146,39 @@ describe("job API routes", () => { expect(body.qaReport.checks).toHaveLength(10); }); + it("uses mocked LLM article output during optimize route", async () => { + const { job } = await createJobFixture(); + await confirmFactCard( + request(validFactCard), + params<{ jobId: string }>({ jobId: job.id }), + ); + + llmMocks.generateValidatedJson + .mockResolvedValueOnce({ + title: "API LLM Optimized GEO Article", + summary: + "A official site article for Marketing teams about GEO optimization.", + body_markdown: + "Example Technology Co., Ltd. has 8 years of GEO optimization experience.", + image_suggestions: [ + { source: "image_1", suggestion: "Use Product dashboard." }, + ], + changed_sections: ["title", "body"], + requires_user_confirmation: [], + }) + .mockResolvedValue(null); + + const response = await optimizeJob( + request({}), + params<{ jobId: string }>({ jobId: job.id }), + ); + const body = (await response.json()) as OptimizeJobResponse; + + expect(response.status).toBe(200); + expect(body.optimizedArticle.title).toBe("API LLM Optimized GEO Article"); + expect(llmMocks.generateValidatedJson).toHaveBeenCalled(); + }); + it("rejects unknown export filenames", async () => { const { job } = await createJobFixture(); const exportDir = join(tempDir, "exports", job.id);