Files
writer-work-flow/apps/web/lib/review/decisions.test.ts
Yaojia Wang 68f194a043 feat: M2 — 写→审(一致性)→裁决→验收(事务);未决冲突禁验收
- 续审 Agent 声明(AgentSpec) + 结构化输出契约(ContinuityReview/Conflict 五类)
- LangGraph 并行审子图(可扩四审) + collect 落 chapter_reviews 留痕 + review SSE(section/conflict)
- 验收-side Repository:章节 accepted 版本晋升 + digest append-only + 审稿留痕/裁决
- API:review(SSE) + reviews 历史 + accept(单原子事务:晋升 version + 终稿 digest + 裁决留痕)
- 冲突 gate:未决裁决拦截(CONFLICT_UNRESOLVED);digest 从终稿提炼(不变量#4)
- 前端:审稿报告页 + 冲突就地标注 + 裁决(采纳/忽略/手改) + 未决禁验收 + 「本次将更新」清单
- M2 E2E:真实 DB + 多档位 mock 网关零 token 走通 写→审→裁决→验收→摘要入库
- 多 agent 协同台账(PROGRESS.md) + 共享记忆(memory/contracts·decisions·gotchas)
2026-06-18 11:38:28 +02:00

102 lines
3.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
allResolved,
buildAcceptRequest,
emptyDecisions,
missingConflictIndices,
setNote,
setVerdict,
unresolvedIndices,
} from "./decisions";
describe("emptyDecisions", () => {
it("creates N unresolved drafts", () => {
expect(emptyDecisions(2)).toEqual([
{ verdict: null, note: "" },
{ verdict: null, note: "" },
]);
});
});
describe("setVerdict / setNote (immutable)", () => {
it("sets verdict at index without mutating others", () => {
const drafts = emptyDecisions(2);
const next = setVerdict(drafts, 1, "ignore");
expect(next[1]?.verdict).toBe("ignore");
expect(next[0]?.verdict).toBeNull();
expect(drafts[1]?.verdict).toBeNull(); // 原数组不变
});
it("sets note at index", () => {
const drafts = emptyDecisions(1);
const next = setNote(drafts, 0, "手改说明");
expect(next[0]?.note).toBe("手改说明");
expect(drafts[0]?.note).toBe("");
});
});
describe("allResolved / unresolvedIndices", () => {
it("zero conflicts pass through (allResolved true)", () => {
expect(allResolved([])).toBe(true);
expect(unresolvedIndices([])).toEqual([]);
});
it("requires every conflict to have a verdict", () => {
let drafts = emptyDecisions(3);
expect(allResolved(drafts)).toBe(false);
expect(unresolvedIndices(drafts)).toEqual([0, 1, 2]);
drafts = setVerdict(drafts, 0, "accept");
drafts = setVerdict(drafts, 2, "manual");
expect(allResolved(drafts)).toBe(false);
expect(unresolvedIndices(drafts)).toEqual([1]);
drafts = setVerdict(drafts, 1, "ignore");
expect(allResolved(drafts)).toBe(true);
expect(unresolvedIndices(drafts)).toEqual([]);
});
});
describe("buildAcceptRequest", () => {
it("includes only resolved decisions and trims notes", () => {
let drafts = emptyDecisions(3);
drafts = setVerdict(drafts, 0, "accept");
drafts = setVerdict(drafts, 2, "manual");
drafts = setNote(drafts, 2, " 改成后天淬炼 ");
// index 1 仍未决 → 不应进入请求
const req = buildAcceptRequest("终稿正文", drafts);
expect(req.final_text).toBe("终稿正文");
expect(req.decisions).toEqual([
{ conflict_index: 0, verdict: "accept" },
{ conflict_index: 2, verdict: "manual", note: "改成后天淬炼" },
]);
});
it("omits empty/whitespace-only notes", () => {
let drafts = emptyDecisions(1);
drafts = setVerdict(drafts, 0, "ignore");
drafts = setNote(drafts, 0, " ");
const req = buildAcceptRequest("t", drafts);
expect(req.decisions).toEqual([{ conflict_index: 0, verdict: "ignore" }]);
});
});
describe("missingConflictIndices", () => {
it("extracts a set from 409 details", () => {
const set = missingConflictIndices({
missing_conflict_indices: [1, 3],
conflict_count: 4,
});
expect(set.has(1)).toBe(true);
expect(set.has(3)).toBe(true);
expect(set.has(0)).toBe(false);
});
it("returns empty set for malformed/missing details", () => {
expect(missingConflictIndices(null).size).toBe(0);
expect(missingConflictIndices({}).size).toBe(0);
expect(missingConflictIndices({ missing_conflict_indices: "x" }).size).toBe(
0,
);
});
});