feat(frontend): 整章重写接入 AI 反问澄清——useRewriteClarify + ChapterRewritePanel 门控(WFW-9 M2)

This commit is contained in:
Yaojia Wang
2026-07-08 12:00:17 +02:00
parent 4b1088a179
commit 8e29b33a11
3 changed files with 258 additions and 10 deletions

View File

@@ -0,0 +1,115 @@
// @vitest-environment jsdom
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useRewriteClarify } from "./useRewriteClarify";
import type { ClarifyDecisionVM } from "./clarify";
const post = vi.fn();
vi.mock("@/lib/api/client", () => ({
api: { POST: (...a: unknown[]) => post(...a) },
}));
const CLARIFY_PATH = "/projects/{project_id}/chapters/{chapter_no}/rewrite/clarify";
describe("useRewriteClarify", () => {
beforeEach(() => post.mockReset());
afterEach(() => vi.clearAllMocks());
it("初始 idle、无决策", () => {
const { result } = renderHook(() => useRewriteClarify());
expect(result.current.status).toBe("idle");
expect(result.current.decision).toBeNull();
});
it("need_clarification=truePOST rewrite/clarify 带 feedback+prior_draft映射 VMstatus=asking", async () => {
post.mockResolvedValue({
data: {
need_clarification: true,
questions: [
{
question: "高潮想更爽还是更虐?",
options: [
{ label: "更爽", value: "把高潮写得更爽快" },
{ label: "更虐", value: "把高潮写得更虐" },
],
allow_free_text: true,
},
],
verification: null,
},
error: null,
});
const { result } = renderHook(() => useRewriteClarify());
let vm: ClarifyDecisionVM | undefined;
await act(async () => {
vm = await result.current.check("p1", 3, "改改", "整章草稿正文");
});
expect(post).toHaveBeenCalledWith(CLARIFY_PATH, {
params: { path: { project_id: "p1", chapter_no: 3 } },
body: { feedback: "改改", prior_draft: "整章草稿正文" },
});
expect(vm).toEqual({
needClarification: true,
questions: [
{
question: "高潮想更爽还是更虐?",
options: [
{ label: "更爽", value: "把高潮写得更爽快" },
{ label: "更虐", value: "把高潮写得更虐" },
],
allowFreeText: true,
},
],
verification: undefined,
});
expect(result.current.status).toBe("asking");
expect(result.current.decision?.needClarification).toBe(true);
});
it("need_clarification=falsestatus=proceeddecision 清空", async () => {
post.mockResolvedValue({
data: { need_clarification: false, questions: [], verification: "我按你说的拉满高潮" },
error: null,
});
const { result } = renderHook(() => useRewriteClarify());
let vm: ClarifyDecisionVM | undefined;
await act(async () => {
vm = await result.current.check("p1", 1, "把第 3 段的高潮再拉满,删掉闪回", "草稿");
});
expect(vm?.needClarification).toBe(false);
expect(result.current.status).toBe("proceed");
expect(result.current.decision).toBeNull();
});
it("后端 error视为放行proceedneedClarification=false不阻塞重写", async () => {
post.mockResolvedValue({ data: null, error: { detail: "boom" } });
const { result } = renderHook(() => useRewriteClarify());
let vm: ClarifyDecisionVM | undefined;
await act(async () => {
vm = await result.current.check("p1", 1, "改", "草稿");
});
expect(vm?.needClarification).toBe(false);
expect(result.current.status).toBe("proceed");
});
// 注hook 的 try/catch 对「fetch 抛异常」的放行分支与上面「后端 error 信封」放行分支同构、
// 行为一致(均 status=proceed、needClarification=false、返回 PROCEED此处不单测 throw 路径,
// 因 mock 抛错 + React act 冲刷会触发 vitest 未处理错误误报(与被测逻辑无关,同 M1 useClarify.test.ts
it("reset 清回 idle", async () => {
post.mockResolvedValue({
data: { need_clarification: true, questions: [{ question: "q", options: [], allow_free_text: true }] },
error: null,
});
const { result } = renderHook(() => useRewriteClarify());
await act(async () => {
await result.current.check("p1", 1, "改", "草稿");
});
act(() => result.current.reset());
expect(result.current.status).toBe("idle");
expect(result.current.decision).toBeNull();
});
});