- 伏笔账本:纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE) + ForeshadowLedger repo;验收后到期扫描(BackgroundTask 自建 session 置 OVERDUE);登记/状态变更端点 - 节奏 + 三审齐:foreshadow-analyst + pace-checker 并入 LangGraph 并行审(REVIEW_SPECS),collect 分列落 chapter_reviews(conflicts/foreshadow_sug/pace),review SSE 加 foreshadow/pace 事件 - 大纲:outliner Agent 产 OutlineResult(含 foreshadow_windows),POST /outline 逐章 upsert outline 表;GET /foreshadow?status= 看板 - 前端:伏笔四泳道看板(OVERDUE 琥珀) + 大纲编辑器(窗口徽标) + 节奏节拍图(▁▃▅) + 审稿页消费 foreshadow/pace SSE - bugfix(T3.8):并行三审共用请求 session 记账触发 'Session is already flushing' → foreshadow/pace 静默丢失;SqlAlchemyLedgerSink.record 改 add-only(靠端点/事务 commit),加并发回归测试 - M3 E2E:真实 DB + mock 网关零 token 走通 埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕;E2E 暴露并钉住上述 bug - 门禁绿:mypy 111 / pytest 228(0 xfailed) / alembic 无漂移;前端 gen:api/lint/tsc/vitest 69/build
135 lines
4.1 KiB
TypeScript
135 lines
4.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { ForeshadowView } from "@/lib/api/types";
|
|
import {
|
|
buildRegisterRequest,
|
|
buildTransitionRequest,
|
|
extractReason,
|
|
groupByStatus,
|
|
isWindowApproaching,
|
|
validationReasonMessage,
|
|
} from "./board";
|
|
|
|
const view = (over: Partial<ForeshadowView>): ForeshadowView => ({
|
|
code: "F-001",
|
|
title: "线索",
|
|
status: "OPEN",
|
|
...over,
|
|
});
|
|
|
|
describe("groupByStatus", () => {
|
|
it("splits items into four lanes preserving order", () => {
|
|
const lanes = groupByStatus([
|
|
view({ code: "F-001", status: "OPEN" }),
|
|
view({ code: "F-002", status: "OVERDUE" }),
|
|
view({ code: "F-003", status: "OPEN" }),
|
|
view({ code: "F-004", status: "CLOSED" }),
|
|
view({ code: "F-005", status: "PARTIAL" }),
|
|
]);
|
|
expect(lanes.OPEN.map((v) => v.code)).toEqual(["F-001", "F-003"]);
|
|
expect(lanes.OVERDUE.map((v) => v.code)).toEqual(["F-002"]);
|
|
expect(lanes.CLOSED.map((v) => v.code)).toEqual(["F-004"]);
|
|
expect(lanes.PARTIAL.map((v) => v.code)).toEqual(["F-005"]);
|
|
});
|
|
|
|
it("falls back unknown status to OPEN and handles undefined", () => {
|
|
const lanes = groupByStatus([view({ status: "WEIRD" })]);
|
|
expect(lanes.OPEN).toHaveLength(1);
|
|
expect(groupByStatus(undefined).OPEN).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("isWindowApproaching", () => {
|
|
it("is true when current chapter is inside [from,to] for OPEN/PARTIAL", () => {
|
|
const f = view({
|
|
status: "PARTIAL",
|
|
expected_close_from: 40,
|
|
expected_close_to: 60,
|
|
});
|
|
expect(isWindowApproaching(f, 50)).toBe(true);
|
|
expect(isWindowApproaching(f, 39)).toBe(false);
|
|
expect(isWindowApproaching(f, 61)).toBe(false);
|
|
});
|
|
|
|
it("never flags CLOSED/OVERDUE or items without a window", () => {
|
|
expect(
|
|
isWindowApproaching(
|
|
view({ status: "CLOSED", expected_close_to: 60 }),
|
|
50,
|
|
),
|
|
).toBe(false);
|
|
expect(isWindowApproaching(view({ status: "OPEN" }), 50)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("buildRegisterRequest", () => {
|
|
it("trims and omits empty optionals", () => {
|
|
expect(
|
|
buildRegisterRequest({
|
|
projectId: "p1",
|
|
code: " F-007 ",
|
|
title: " 旧誓约 ",
|
|
content: " ",
|
|
expectedCloseTo: 110,
|
|
}),
|
|
).toEqual({ code: "F-007", title: "旧誓约", expected_close_to: 110 });
|
|
});
|
|
|
|
it("keeps numeric and non-empty string optionals", () => {
|
|
expect(
|
|
buildRegisterRequest({
|
|
projectId: "p1",
|
|
code: "F-1",
|
|
title: "t",
|
|
plantedAt: 8,
|
|
content: "线索",
|
|
expectedCloseFrom: 40,
|
|
importance: "主线",
|
|
}),
|
|
).toEqual({
|
|
code: "F-1",
|
|
title: "t",
|
|
planted_at: 8,
|
|
content: "线索",
|
|
expected_close_from: 40,
|
|
importance: "主线",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("buildTransitionRequest", () => {
|
|
it("includes only provided fields", () => {
|
|
expect(buildTransitionRequest({ projectId: "p1", toStatus: "PARTIAL" })).toEqual({
|
|
to_status: "PARTIAL",
|
|
});
|
|
expect(
|
|
buildTransitionRequest({
|
|
projectId: "p1",
|
|
progressEntry: { chapter: 23, note: "发光" },
|
|
}),
|
|
).toEqual({ progress_entry: { chapter: 23, note: "发光" } });
|
|
expect(buildTransitionRequest({ projectId: "p1" })).toEqual({});
|
|
expect(
|
|
buildTransitionRequest({ projectId: "p1", progressEntry: {} }),
|
|
).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("validationReasonMessage / extractReason", () => {
|
|
it("maps known reasons to friendly text", () => {
|
|
expect(validationReasonMessage("duplicate")).toContain("已存在");
|
|
expect(validationReasonMessage("invalid_transition")).toContain("终态");
|
|
expect(validationReasonMessage("empty_update")).toContain("进展");
|
|
expect(validationReasonMessage("invalid_status")).toContain("筛选");
|
|
expect(validationReasonMessage(undefined)).toContain("校验");
|
|
});
|
|
|
|
it("extracts details.reason from envelope", () => {
|
|
expect(
|
|
extractReason({ error: { details: { reason: "duplicate" } } }),
|
|
).toBe("duplicate");
|
|
expect(extractReason({ error: { details: null } })).toBeUndefined();
|
|
expect(extractReason("oops")).toBeUndefined();
|
|
});
|
|
});
|