openclaw/src/imessage/monitor/reflection-guard.test.ts
OfflynAI adb9234d03
fix(imessage): prevent echo loop from leaking internal metadata and amplifying NO_REPLY into queue overflow (#33295)
* fix(imessage): prevent echo loop from leaking internal metadata and amplifying NO_REPLY into queue overflow

- Add outbound sanitization at channel boundary (sanitize-outbound.ts):
  strips thinking/reasoning tags, relevant-memories tags, model-specific
  separators (+#+#), and assistant role markers before iMessage delivery

- Add inbound reflection guard (reflection-guard.ts): detects and drops
  messages containing assistant-internal markers that indicate a reflected
  outbound message, preventing recursive echo amplification

- Harden echo cache: increase text TTL from 5s to 30s to catch delayed
  reflections that previously expired before the echo could be detected

- Add loop rate limiter (loop-rate-limiter.ts): per-conversation rapid-fire
  detection that suppresses conversations exceeding threshold within a
  time window, acting as a safety net against amplification

Closes #33281

* fix(imessage): address review — stricter reflection regex, loop-aware rate limiter

- Reflection guard: require closing > bracket on thinking/final/memory
  tag patterns to prevent false-positives on user phrases like
  '<final answer>' or '<thought experiment>' (#33295 review)

- Rate limiter: only record echo/reflection/from-me drops instead of
  all dispatches, so the limiter acts as a loop-specific escalation
  mechanism rather than a general throttle on normal conversation
  velocity (#33295 review)

* Changelog: add iMessage echo-loop hardening entry

* iMessage: restore short echo-text TTL

* iMessage: ignore reflection markers in code

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-03-06 19:19:57 -05:00

108 lines
4.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { detectReflectedContent } from "./reflection-guard.js";
describe("detectReflectedContent", () => {
it("returns false for empty text", () => {
expect(detectReflectedContent("").isReflection).toBe(false);
});
it("returns false for normal user text", () => {
const result = detectReflectedContent("Hey, what's the weather today?");
expect(result.isReflection).toBe(false);
expect(result.matchedLabels).toEqual([]);
});
it("detects +#+#+#+# separator pattern", () => {
const result = detectReflectedContent("NO_REPLY +#+#+#+#+#+assistant to=final");
expect(result.isReflection).toBe(true);
expect(result.matchedLabels).toContain("internal-separator");
});
it("detects assistant to=final marker", () => {
const result = detectReflectedContent("some text assistant to=final rest");
expect(result.isReflection).toBe(true);
expect(result.matchedLabels).toContain("assistant-role-marker");
});
it("detects <thinking> tags", () => {
const result = detectReflectedContent("<thinking>internal reasoning</thinking>");
expect(result.isReflection).toBe(true);
expect(result.matchedLabels).toContain("thinking-tag");
});
it("detects <thought> tags", () => {
const result = detectReflectedContent("<thought>secret</thought>");
expect(result.isReflection).toBe(true);
expect(result.matchedLabels).toContain("thinking-tag");
});
it("detects <relevant_memories> tags", () => {
const result = detectReflectedContent("<relevant_memories>data</relevant_memories>");
expect(result.isReflection).toBe(true);
expect(result.matchedLabels).toContain("relevant-memories-tag");
});
it("detects <final> tags", () => {
const result = detectReflectedContent("<final>visible</final>");
expect(result.isReflection).toBe(true);
expect(result.matchedLabels).toContain("final-tag");
});
it("returns multiple matched labels for combined markers", () => {
const text = "NO_REPLY +#+#+#+# <thinking>step</thinking> assistant to=final";
const result = detectReflectedContent(text);
expect(result.isReflection).toBe(true);
expect(result.matchedLabels.length).toBeGreaterThanOrEqual(3);
});
it("ignores reflection markers inside inline code", () => {
const result = detectReflectedContent(
"Please keep `<thinking>debug trace</thinking>` in the example output",
);
expect(result.isReflection).toBe(false);
expect(result.matchedLabels).toEqual([]);
});
it("ignores reflection markers inside fenced code blocks", () => {
const result = detectReflectedContent(
[
"User pasted a repro snippet:",
"```xml",
"<relevant_memories>cached</relevant_memories>",
"assistant to=final",
"```",
].join("\n"),
);
expect(result.isReflection).toBe(false);
expect(result.matchedLabels).toEqual([]);
});
it("still flags markers that appear outside code blocks", () => {
const result = detectReflectedContent(
["```xml", "<thinking>inside code</thinking>", "```", "", "assistant to=final"].join("\n"),
);
expect(result.isReflection).toBe(true);
expect(result.matchedLabels).toContain("assistant-role-marker");
});
it("does not flag normal code discussion about thinking", () => {
const result = detectReflectedContent("I was thinking about your question");
expect(result.isReflection).toBe(false);
});
it("flags '<final answer>' as reflection when it forms a complete tag", () => {
const result = detectReflectedContent("Here is my <final answer>");
expect(result.isReflection).toBe(true);
});
it("does not flag partial tag without closing bracket", () => {
const result = detectReflectedContent("I sent a <final draft, see below");
expect(result.isReflection).toBe(false);
});
it("does not flag '<thought experiment>' phrase without closing bracket", () => {
const result = detectReflectedContent("This is a <thought experiment I ran");
expect(result.isReflection).toBe(false);
});
});