- 续审 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)
102 lines
3.2 KiB
TypeScript
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,
|
|
);
|
|
});
|
|
});
|