Files
writer-work-flow/apps/web/lib/wizard/wizard.test.ts
Yaojia Wang 2fe3bedfba fix(qa): 修 QA C1/H1/H2——写章/规则缺项目校验 + 立项向导字段覆盖
C1 (CRITICAL) stream_draft:对不存在 project 流式写章先 fail-fast 404,
  否则非法 id 静默烧一次付费/限流 LLM 调用并返 200。在触网关前查 project_repo.get。
H2 (HIGH) create_rule:给不存在 project 加规则原 FK 违例逃逸成 500 → 改为入库前
  校验项目存在返 404(仿 chain/_require_project)。
H1 (HIGH) ProjectWizard:第3步「立意」与第4步「主角/金手指」原共用 form.premise
  互相覆盖丢数据 → 新增独立 form.protagonist,toCreateRequest 合并两段进 premise
  (M1 projects 表仍只有 premise,不编造 API)。

回归测试:
- test_projects.py:stream_draft 不存在 project → 404 且网关零调用;已有 draft
  用例改 seed 真项目。
- test_rules.py:create_rule 不存在 project → 404 不写库;已有用例 seed 真项目。
- wizard.test.ts:premise+protagonist 合并不互相覆盖(2 例)。
门禁绿:ruff/format clean · mypy 210 · pytest 749 · 前端 tsc/lint/vitest 干净。
2026-06-24 17:17:35 +02:00

107 lines
3.3 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.

import { describe, expect, it } from "vitest";
import {
canAdvance,
canSubmit,
clampStep,
emptyWizardForm,
toCreateRequest,
WIZARD_STEPS,
type WizardForm,
} from "./wizard";
describe("wizard step gating", () => {
it("blocks advancing past step 1 without a title", () => {
expect(canAdvance(1, emptyWizardForm)).toBe(false);
});
it("allows advancing step 1 once title is set", () => {
expect(canAdvance(1, { ...emptyWizardForm, title: "逐光而行" })).toBe(true);
});
it("allows advancing later steps even when fields are empty", () => {
expect(canAdvance(3, emptyWizardForm)).toBe(true);
});
it("clamps step within bounds", () => {
expect(clampStep(0)).toBe(1);
expect(clampStep(WIZARD_STEPS + 3)).toBe(WIZARD_STEPS);
expect(clampStep(3)).toBe(3);
});
it("requires a title to submit", () => {
expect(canSubmit(emptyWizardForm)).toBe(false);
expect(canSubmit({ ...emptyWizardForm, title: "x" })).toBe(true);
});
});
describe("toCreateRequest", () => {
it("maps form to snake_case request, nulling empty optionals", () => {
const form: WizardForm = {
title: " 逐光而行 ",
genre: "玄幻",
logline: "",
sellingPoints: ["逆袭", "系统流"],
structure: "三幕",
premise: " ",
protagonist: "",
theme: "抗争",
};
expect(toCreateRequest(form)).toEqual({
title: "逐光而行",
genre: "玄幻",
logline: null,
premise: null,
theme: "抗争",
selling_points: ["逆袭", "系统流"],
structure: "三幕",
});
});
// QA H1 回归立意premise与主角/金手指protagonist是两个独立输入
// 不能互相覆盖——提交时各占一段合并进 premise。
it("merges premise and protagonist into premise without overwriting", () => {
const form: WizardForm = {
...emptyWizardForm,
title: "书",
premise: "凡人逆袭的代价",
protagonist: "主角林川,金手指=吞噬术,限制:每次吞噬折寿",
};
const req = toCreateRequest(form);
expect(req.premise).toBe(
"凡人逆袭的代价\n\n主角/金手指:主角林川,金手指=吞噬术,限制:每次吞噬折寿",
);
});
it("keeps protagonist alone when premise is empty", () => {
const form: WizardForm = {
...emptyWizardForm,
title: "书",
protagonist: "主角设定",
};
expect(toCreateRequest(form).premise).toBe("主角/金手指:主角设定");
});
});
// wizard→create 流程:归一后的请求体经 mock 客户端提交,返回新建项目 id。
describe("wizard create flow (mocked client)", () => {
it("submits the normalized body and resolves a project id", async () => {
const post = async (
_path: string,
opts: { body: ReturnType<typeof toCreateRequest> },
): Promise<{ data: { id: string }; error: null }> => {
expect(opts.body.title).toBe("青冥录");
expect(opts.body.selling_points).toEqual(["群像"]);
return { data: { id: "proj-123" }, error: null };
};
const form: WizardForm = {
...emptyWizardForm,
title: "青冥录",
sellingPoints: ["群像"],
};
const { data } = await post("/projects", { body: toCreateRequest(form) });
expect(data.id).toBe("proj-123");
});
});