

















@@ -0,0 +1,113 @@
1+import { describe, expect, it } from "vitest";
2+import {
3+createPostCompactionLoopGuard,
4+type PostCompactionLoopGuard,
5+} from "./post-compaction-loop-guard.js";
6+7+function callOutcome(toolName: string, args: unknown, result: string) {
8+return { toolName, argsHash: JSON.stringify(args), resultHash: result };
9+}
10+11+describe("createPostCompactionLoopGuard", () => {
12+it("is dormant when never armed", () => {
13+const guard = createPostCompactionLoopGuard();
14+const verdict = guard.observe(callOutcome("read", { path: "/x" }, "r1"));
15+expect(verdict.shouldAbort).toBe(false);
16+expect(verdict.armed).toBe(false);
17+});
18+19+it("arms for the configured window after compaction", () => {
20+const guard = createPostCompactionLoopGuard({ windowSize: 3 });
21+guard.armPostCompaction();
22+expect(guard.snapshot().armed).toBe(true);
23+expect(guard.snapshot().remainingAttempts).toBe(3);
24+});
25+26+it("decrements remainingAttempts on each observation", () => {
27+const guard = createPostCompactionLoopGuard({ windowSize: 3 });
28+guard.armPostCompaction();
29+guard.observe(callOutcome("read", { path: "/x" }, "r1"));
30+expect(guard.snapshot().remainingAttempts).toBe(2);
31+guard.observe(callOutcome("read", { path: "/y" }, "r2"));
32+expect(guard.snapshot().remainingAttempts).toBe(1);
33+guard.observe(callOutcome("read", { path: "/z" }, "r3"));
34+expect(guard.snapshot().remainingAttempts).toBe(0);
35+expect(guard.snapshot().armed).toBe(false);
36+});
37+38+it("aborts on the windowSize-th identical (tool,args,result) call within the window", () => {
39+const guard = createPostCompactionLoopGuard({ windowSize: 3 });
40+guard.armPostCompaction();
41+expect(
42+guard.observe(callOutcome("gateway", { action: "lookup", path: "x" }, "r1")).shouldAbort,
43+).toBe(false);
44+expect(
45+guard.observe(callOutcome("gateway", { action: "lookup", path: "x" }, "r1")).shouldAbort,
46+).toBe(false);
47+const third = guard.observe(callOutcome("gateway", { action: "lookup", path: "x" }, "r1"));
48+expect(third.shouldAbort).toBe(true);
49+if (third.shouldAbort) {
50+expect(third.detector).toBe("compaction_loop_persisted");
51+expect(third.count).toBe(3);
52+expect(third.toolName).toBe("gateway");
53+}
54+});
55+56+it("does NOT abort when the result hash changes (progress was made)", () => {
57+const guard = createPostCompactionLoopGuard({ windowSize: 3 });
58+guard.armPostCompaction();
59+guard.observe(callOutcome("read", { path: "/x" }, "r1"));
60+guard.observe(callOutcome("read", { path: "/x" }, "r2"));
61+const third = guard.observe(callOutcome("read", { path: "/x" }, "r3"));
62+expect(third.shouldAbort).toBe(false);
63+});
64+65+it("does NOT abort when the args hash changes", () => {
66+const guard = createPostCompactionLoopGuard({ windowSize: 3 });
67+guard.armPostCompaction();
68+guard.observe(callOutcome("read", { path: "/a" }, "r1"));
69+guard.observe(callOutcome("read", { path: "/b" }, "r1"));
70+const third = guard.observe(callOutcome("read", { path: "/c" }, "r1"));
71+expect(third.shouldAbort).toBe(false);
72+});
73+74+it("does NOT abort outside the window", () => {
75+const guard = createPostCompactionLoopGuard({ windowSize: 2 });
76+guard.armPostCompaction();
77+guard.observe(callOutcome("read", { path: "/x" }, "r1"));
78+guard.observe(callOutcome("read", { path: "/x" }, "r1"));
79+expect(guard.snapshot().armed).toBe(false);
80+const after = guard.observe(callOutcome("read", { path: "/x" }, "r1"));
81+expect(after.shouldAbort).toBe(false);
82+});
83+84+it("re-arms when armPostCompaction is called again (multiple compactions per run)", () => {
85+const guard = createPostCompactionLoopGuard({ windowSize: 2 });
86+guard.armPostCompaction();
87+guard.observe(callOutcome("read", { path: "/x" }, "r1"));
88+guard.observe(callOutcome("read", { path: "/x" }, "r1"));
89+expect(guard.snapshot().armed).toBe(false);
90+guard.armPostCompaction();
91+expect(guard.snapshot().armed).toBe(true);
92+expect(guard.snapshot().remainingAttempts).toBe(2);
93+});
94+95+it("respects enabled: false (always returns shouldAbort: false even when armed)", () => {
96+const guard = createPostCompactionLoopGuard({ enabled: false, windowSize: 3 });
97+guard.armPostCompaction();
98+guard.observe(callOutcome("gateway", { x: 1 }, "r1"));
99+guard.observe(callOutcome("gateway", { x: 1 }, "r1"));
100+const third = guard.observe(callOutcome("gateway", { x: 1 }, "r1"));
101+expect(third.shouldAbort).toBe(false);
102+});
103+104+it("disarms after observing windowSize calls regardless of verdict", () => {
105+const guard = createPostCompactionLoopGuard({ windowSize: 3 });
106+guard.armPostCompaction();
107+guard.observe(callOutcome("read", { path: "/a" }, "r1"));
108+guard.observe(callOutcome("write", { path: "/b" }, "r2"));
109+guard.observe(callOutcome("exec", { cmd: "ls" }, "r3"));
110+expect(guard.snapshot().armed).toBe(false);
111+expect(guard.snapshot().remainingAttempts).toBe(0);
112+});
113+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。