Files
writer-work-flow/apps/web/lib/workbench/useContinue.test.ts
Yaojia Wang 9195cf36f3 feat(frontend): 编辑器内「续写」——读本章前文生成多候选,点选插入正文
Phase 1(写作工作台重构):编辑器「续写」按钮复用 continue 生成器(with_prior_chapter
自动读该章已写正文承接),每次产出一条候选并累加成多候选(useContinue,已单测),
「再来一个」可多生成几版;作者点某条「插入正文」才追加到章末(HITL,不静默写入)。
续写前 flush 自动保存,确保读到最新草稿。润色/续写结果卡互斥不并占。
2026-07-07 19:41:57 +02:00

107 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment jsdom
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useContinue } from "./useContinue";
const post = vi.fn();
const toast = vi.fn();
vi.mock("@/lib/api/client", () => ({
api: { POST: (...a: unknown[]) => post(...a) },
}));
vi.mock("@/components/Toast", () => ({ useToast: () => toast }));
const GEN_PATH = "/projects/{project_id}/skills/{tool_key}/generate";
describe("useContinue", () => {
beforeEach(() => {
post.mockReset();
toast.mockReset();
});
afterEach(() => vi.clearAllMocks());
it("初始为 idle无候选", () => {
const { result } = renderHook(() => useContinue());
expect(result.current.status).toBe("idle");
expect(result.current.candidates).toEqual([]);
});
it("续写:调 continue 生成器读该章前文产出一条候选status=ready", async () => {
post.mockResolvedValue({
data: { output_kind: "ContinuationResult", preview: { text: "续写正文一" } },
error: null,
});
const { result } = renderHook(() => useContinue());
await act(async () => {
await result.current.generate("p1", 3);
});
expect(post).toHaveBeenCalledWith(GEN_PATH, {
params: { path: { project_id: "p1", tool_key: "continue" } },
body: { brief: "", chapter_no: 3 },
});
expect(result.current.status).toBe("ready");
expect(result.current.candidates).toEqual(["续写正文一"]);
});
it("多候选:再次生成累加候选,不覆盖前一条", async () => {
post
.mockResolvedValueOnce({ data: { preview: { text: "候选一" } }, error: null })
.mockResolvedValueOnce({ data: { preview: { text: "候选二" } }, error: null });
const { result } = renderHook(() => useContinue());
await act(async () => {
await result.current.generate("p1", 1);
});
await act(async () => {
await result.current.generate("p1", 1);
});
expect(result.current.candidates).toEqual(["候选一", "候选二"]);
});
it("产出为空不加候选、提示重试、status=ready", async () => {
post.mockResolvedValue({ data: { preview: { text: "" } }, error: null });
const { result } = renderHook(() => useContinue());
await act(async () => {
await result.current.generate("p1", 1);
});
expect(result.current.candidates).toEqual([]);
expect(result.current.status).toBe("ready");
expect(toast).toHaveBeenCalledWith(expect.any(String), "info");
});
it("后端返回 errorstatus=error、弹错误 toast、候选不变", async () => {
post.mockResolvedValue({ data: null, error: { detail: "配额不足" } });
const { result } = renderHook(() => useContinue());
await act(async () => {
await result.current.generate("p1", 1);
});
expect(result.current.status).toBe("error");
expect(result.current.candidates).toEqual([]);
expect(toast).toHaveBeenCalledWith(expect.any(String), "error");
});
it("请求抛异常status=error、弹网络异常 toast", async () => {
post.mockRejectedValue(new Error("network down"));
const { result } = renderHook(() => useContinue());
await act(async () => {
await result.current.generate("p1", 1);
});
expect(result.current.status).toBe("error");
expect(toast).toHaveBeenCalledWith("续写请求异常,请检查网络。", "error");
});
it("reset 清回 idle 与空候选", async () => {
post.mockResolvedValue({ data: { preview: { text: "候选" } }, error: null });
const { result } = renderHook(() => useContinue());
await act(async () => {
await result.current.generate("p1", 1);
});
act(() => result.current.reset());
expect(result.current.status).toBe("idle");
expect(result.current.candidates).toEqual([]);
});
});