M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。 M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变); 网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则; 前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。 K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径; coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。 本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400); GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化; 字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。 门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
1.8 KiB
TypeScript
69 lines
1.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
||
|
||
import { fetchDraft, fetchOutline } from "./server";
|
||
|
||
// 纯加载/降级逻辑(mock fetch,无网络/DOM):大纲解包/降级、草稿 404→null。
|
||
function mockFetch(status: number, body: unknown): void {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async () =>
|
||
new Response(JSON.stringify(body), {
|
||
status,
|
||
headers: { "content-type": "application/json" },
|
||
}),
|
||
),
|
||
);
|
||
}
|
||
|
||
afterEach(() => {
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
describe("fetchOutline", () => {
|
||
it("returns the persisted chapters on 200", async () => {
|
||
mockFetch(200, { chapters: [{ no: 1, volume: 1, beats: ["开场"] }] });
|
||
|
||
const chapters = await fetchOutline("p1");
|
||
|
||
expect(chapters).toHaveLength(1);
|
||
expect(chapters[0].no).toBe(1);
|
||
});
|
||
|
||
it("returns empty array when the project has no outline", async () => {
|
||
mockFetch(200, { chapters: [] });
|
||
|
||
expect(await fetchOutline("p1")).toEqual([]);
|
||
});
|
||
|
||
it("falls back to empty array on any error (does not throw)", async () => {
|
||
mockFetch(404, { error: { code: "NOT_FOUND", message: "no project" } });
|
||
|
||
expect(await fetchOutline("missing")).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe("fetchDraft", () => {
|
||
it("returns the saved draft view on 200", async () => {
|
||
mockFetch(200, {
|
||
project_id: "p1",
|
||
chapter_no: 1,
|
||
volume: 1,
|
||
status: "draft",
|
||
version: 2,
|
||
content: "第一章正文",
|
||
length: 5,
|
||
});
|
||
|
||
const draft = await fetchDraft("p1", 1);
|
||
|
||
expect(draft?.content).toBe("第一章正文");
|
||
expect(draft?.version).toBe(2);
|
||
});
|
||
|
||
it("returns null when no draft exists (404 → empty editor)", async () => {
|
||
mockFetch(404, { error: { code: "NOT_FOUND", message: "no draft" } });
|
||
|
||
expect(await fetchDraft("p1", 9)).toBeNull();
|
||
});
|
||
});
|