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)
This commit is contained in:
101
apps/web/lib/review/decisions.test.ts
Normal file
101
apps/web/lib/review/decisions.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user