import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; 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 { createSqliteRepository } from "../../../lib/db/sqlite-repository"; import { HUMAN_COPY_RUBRIC_V1, scoreHumanCopyResult, } from "../../../lib/calibration/scoring"; import { GET as listCases } from "../cases/route"; import { GET as getCase, PATCH as patchCase } from "../cases/[caseId]/route"; import { POST as archiveCase } from "../cases/[caseId]/archive/route"; import { POST as restoreCase } from "../cases/[caseId]/restore/route"; import { POST as rerunCase } from "../cases/[caseId]/rerun/route"; import { GET as listVersionPublications, POST as createVersionPublication, } from "../cases/[caseId]/versions/[versionId]/publications/route"; import { POST as recordPerformance } from "../publications/[publicationId]/performance/route"; describe("case APIs", () => { let tempDir: string; const originalDataDir = process.env.APP_DATA_DIR; const originalApiKey = process.env.API_ACCESS_KEY; const originalAuthDisabled = process.env.API_AUTH_DISABLED; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "geo-cases-api-")); process.env.APP_DATA_DIR = tempDir; process.env.API_ACCESS_KEY = "test-key"; process.env.API_AUTH_DISABLED = "false"; }); afterEach(() => { 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 }); }); it("lists, reads, patches, archives, and restores cases", async () => { const repository = createSqliteRepository(); const created = await repository.createOptimizationCase({ case_type: "human_copy", title: "人味文案优化:朋友圈", summary: "原文", publish_target: "朋友圈", source_excerpt: "原文", }); const patchResponse = await patchCase( request({ customer_name: "客户A", brand_name: "品牌B", project_tags: ["朋友圈"], notes: "保留口语。", }), params({ caseId: created.id }), ); expect(patchResponse.status).toBe(200); const archiveResponse = await archiveCase( request({}), params({ caseId: created.id }), ); expect(archiveResponse.status).toBe(200); const listResponse = await listCases(request({})); const listBody = (await listResponse.json()) as { cases: unknown[] }; expect(listBody.cases).toHaveLength(0); const restoreResponse = await restoreCase( request({}), params({ caseId: created.id }), ); expect(restoreResponse.status).toBe(200); const detailResponse = await getCase( request({}), params({ caseId: created.id }), ); const detailBody = (await detailResponse.json()) as { case: { customer_name: string; project_tags: string[] }; }; expect(detailBody.case.customer_name).toBe("客户A"); expect(detailBody.case.project_tags).toEqual(["朋友圈"]); }); it("creates publication records for result versions", async () => { const repository = createSqliteRepository(); const created = await repository.createOptimizationCase({ case_type: "human_copy", title: "人味文案优化:私域", summary: "原文", publish_target: "私域", source_excerpt: "原文", }); const version = await repository.createOptimizationResultVersion({ case_id: created.id, case_type: "human_copy", status: "optimized", article_job_id: null, article_revision: null, result_summary: "优化后文案", payload: { optimized_text: "优化后文案", change_notes: [], ai_taste_checks: [], warnings: [], }, process_summary: [], llm_audit_summary: [], error_stage: null, error_summary: null, }); const createResponse = await createVersionPublication( request({ publish_target: "私域", url: "https://example.com/private", published_at: "2026-07-08T12:00:00.000Z", notes: "客户发布", }), params({ caseId: created.id, versionId: version.id }), ); expect(createResponse.status).toBe(201); const listResponse = await listVersionPublications( request({}), params({ caseId: created.id, versionId: version.id }), ); const listBody = (await listResponse.json()) as { publications: Array<{ publish_target: string }>; }; expect(listBody.publications).toEqual([ expect.objectContaining({ publish_target: "私域" }), ]); }); it("records manual performance for a result-version publication without scoring", async () => { const repository = createSqliteRepository(); const created = await repository.createOptimizationCase({ case_type: "human_copy", title: "人味文案优化:私域", summary: "原文", publish_target: "私域", source_excerpt: "原文", }); const version = await repository.createOptimizationResultVersion({ case_id: created.id, case_type: "human_copy", status: "optimized", article_job_id: null, article_revision: null, result_summary: "优化后文案", payload: { optimized_text: "优化后文案", change_notes: [], ai_taste_checks: [], warnings: [], }, process_summary: [], llm_audit_summary: [], error_stage: null, error_summary: null, }); const publication = await repository.createPublicationRecord({ result_version_id: version.id, job_id: null, revision: null, publish_target: "私域", url: "https://example.com/private", published_at: "2026-07-08T12:00:00.000Z", status: "published", notes: "", }); const response = await recordPerformance( request({ window_label: "T+7d", views: "1200", feedback_summary: "客户反馈更自然", }), params({ publicationId: publication.id }), ); const body = (await response.json()) as { snapshot?: { metrics: { views: number } }; calibrationEvent: null; }; expect(response.status).toBe(201); expect(body.snapshot?.metrics.views).toBe(1200); expect(body.calibrationEvent).toBeNull(); }); it("creates calibration events for scored human-copy result-version performance", async () => { const repository = createSqliteRepository(); const created = await repository.createOptimizationCase({ case_type: "human_copy", title: "人味文案优化:私域", summary: "原文", publish_target: "私域", source_excerpt: "原文", }); const result = { optimized_text: "优化后文案", change_notes: [], ai_taste_checks: [], warnings: [], }; const version = await repository.createOptimizationResultVersion({ case_id: created.id, case_type: "human_copy", status: "optimized", article_job_id: null, article_revision: null, result_summary: "优化后文案", payload: result, process_summary: [], llm_audit_summary: [], error_stage: null, error_summary: null, }); await repository.saveRubricVersion(HUMAN_COPY_RUBRIC_V1); await repository.saveScoringRun( scoreHumanCopyResult({ resultVersionId: version.id, result, }), ); const publication = await repository.createPublicationRecord({ result_version_id: version.id, job_id: null, revision: null, publish_target: "私域", url: "https://example.com/private", published_at: "2026-07-08T12:00:00.000Z", status: "published", notes: "", }); const response = await recordPerformance( request({ window_label: "T+7d", views: "1200", feedback_summary: "客户反馈更自然", }), params({ publicationId: publication.id }), ); const body = (await response.json()) as { calibrationEvent: { observations: string[] } | null; }; expect(response.status).toBe(201); expect(body.calibrationEvent?.observations.join(" ")).toContain("综合评分"); }); it("reruns an article case into a new result version and article job", async () => { const repository = createSqliteRepository(); const created = await repository.createOptimizationCase({ case_type: "article", title: "GEO 指南", summary: "原文摘要", publish_target: "official_site", source_excerpt: "原文摘要", }); const sourceJob = await repository.createArticleJob({ case_id: created.id, source_title: "GEO 指南", source_body: "Example Technology Co., Ltd. has 8 years of GEO experience.", image_inputs: [{ type: "description", content: "dashboard" }], publish_platform: "official_site", user_instructions: "保持事实准确", }); await repository.saveFactCard(sourceJob.id, confirmedFactCard); await repository.saveCaseInput({ case_id: created.id, case_type: "article", article_job_id: sourceJob.id, payload: { source_title: "GEO 指南", source_body: "Example Technology Co., Ltd. has 8 years of GEO experience.", image_inputs: [{ type: "description", content: "dashboard" }], publish_platform: "official_site", user_instructions: "保持事实准确", }, }); await repository.createOptimizationResultVersion({ case_id: created.id, case_type: "article", status: "optimized", article_job_id: null, article_revision: null, result_summary: "第一版", payload: null, process_summary: [], llm_audit_summary: [], error_stage: null, error_summary: null, }); llmMocks.generateValidatedJson .mockResolvedValueOnce({ title: "第二版 GEO 指南", summary: "第二版 official site GEO optimization 摘要", body_markdown: "## 第二版\nExample Technology Co., Ltd. has 8 years of GEO optimization experience.", image_suggestions: [], changed_sections: ["标题"], requires_user_confirmation: [], }) .mockResolvedValueOnce({ overall_status: "pass", checks: [], }); const response = await rerunCase(request({}), params({ caseId: created.id })); const body = (await response.json()) as { result_version: { version: number }; optimizedArticle: { title: string; job_id: string }; qaReport: { overall_status: string }; exportPaths: Record; }; expect(response.status).toBe(201); expect(body.result_version.version).toBe(2); expect(body.optimizedArticle.title).toBe("第二版 GEO 指南"); expect(body.qaReport.overall_status).toBe("pass"); expect(body.exportPaths.markdown).toContain(body.optimizedArticle.job_id); await expect(repository.getArticleJob(body.optimizedArticle.job_id)).resolves.toMatchObject({ case_id: created.id, status: "optimized", }); }); it("reruns a human-copy case into a new result version", async () => { const repository = createSqliteRepository(); const created = await repository.createOptimizationCase({ case_type: "human_copy", title: "人味文案优化:朋友圈", summary: "原文", publish_target: "朋友圈", source_excerpt: "原文", }); await repository.saveCaseInput({ case_id: created.id, case_type: "human_copy", article_job_id: null, payload: { source_text: "原文", goal: "自然一点", intensity: "light", user_instructions: "", publish_target: "朋友圈", }, }); await repository.createOptimizationResultVersion({ case_id: created.id, case_type: "human_copy", status: "optimized", article_job_id: null, article_revision: null, result_summary: "第一版", payload: { optimized_text: "第一版", change_notes: [], ai_taste_checks: [], warnings: [], }, process_summary: [], llm_audit_summary: [], error_stage: null, error_summary: null, }); llmMocks.generateValidatedJson.mockResolvedValueOnce({ optimized_text: "第二版", change_notes: [], ai_taste_checks: [], warnings: [], }); const response = await rerunCase(request({}), params({ caseId: created.id })); const body = (await response.json()) as { result_version: { id: string; version: number }; result: { optimized_text: string }; }; expect(response.status).toBe(201); expect(body.result_version.version).toBe(2); expect(body.result.optimized_text).toBe("第二版"); await expect( repository.getLatestScoringRunForResultVersion(body.result_version.id), ).resolves.toMatchObject({ case_type: "human_copy", rubric_version_id: HUMAN_COPY_RUBRIC_V1.id, }); }); }); const confirmedFactCard = { company_full_name: "Example Technology Co., Ltd.", company_short_names: ["Example Tech"], brand_names: ["Example"], product_names: ["Example GEO"], target_industry: "GEO optimization", target_audience: "Marketing teams", experience_years: 8, core_claims: ["8 years of GEO optimization experience"], forbidden_claims: ["industry first"], image_topics: ["dashboard"], uncertain_items: [], is_ready_for_optimization: true, confirmed_by_user: true, } as const; function request(body: unknown, options: { apiKey?: string | null } = {}) { const headers: Record = { "content-type": "application/json" }; const apiKey = options.apiKey === undefined ? "test-key" : options.apiKey; if (apiKey) headers["x-api-key"] = apiKey; return new Request("http://localhost/api/cases", { method: "POST", body: JSON.stringify(body), headers, }); } function params>(values: T) { return { params: Promise.resolve(values) }; }