- 薄自建 LLM 网关:OpenAI 兼容适配器(DeepSeek) + instructor 结构化输出 + usage_ledger 记账 + 档位路由 - 记忆服务 assemble:确定性选择(显式+主角+近况) + 渲染卡 + 缓存断点(中性文本) - LangGraph 写章节点 + Postgres checkpointer + SSE 归一(token/done/error) - API:立项 + 写章 draft(SSE) + PUT 自动保存 + 提供商凭据(Fernet 加密/测试连接) - 前端:AppShell + 作品库 + 5 步立项向导 + 写作工作台(流式打字机+自动保存) + 设置页 - M1 E2E:真实 DB + mock 网关零 token 走通闭环
82 lines
2.3 KiB
TypeScript
82 lines
2.3 KiB
TypeScript
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: " ",
|
|
theme: "抗争",
|
|
};
|
|
expect(toCreateRequest(form)).toEqual({
|
|
title: "逐光而行",
|
|
genre: "玄幻",
|
|
logline: null,
|
|
premise: null,
|
|
theme: "抗争",
|
|
selling_points: ["逆袭", "系统流"],
|
|
structure: "三幕",
|
|
});
|
|
});
|
|
});
|
|
|
|
// 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");
|
|
});
|
|
});
|