Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

101 lines
3.5 KiB
Python

"""Tests for agent stream quality helpers (loop detection)."""
from __future__ import annotations
import pytest
from tools.system.fleet_monitoring.quality import LoopDetector, _shingle_fingerprint
class TestShingleFingerprint:
def test_stable_for_identical_shingle(self) -> None:
s = ("hello", "world", "again")
assert _shingle_fingerprint(s) == _shingle_fingerprint(s)
def test_distinct_for_different_shingles(self) -> None:
a = _shingle_fingerprint(("a", "bc"))
b = _shingle_fingerprint(("ab", "c"))
assert a != b
class TestLoopDetector:
def test_stores_window_configuration(self) -> None:
det = LoopDetector(window=17, threshold=1, shingle_size=2)
assert det._window == 17
assert det._shingle_hashes.maxlen == 17
def test_defaults_non_looping_on_varied_stream(self) -> None:
det = LoopDetector(window=20, threshold=4, shingle_size=3)
phrases = [
"read the config file",
"then check docker logs",
"finally curl the health endpoint",
]
for _ in range(15):
for p in phrases:
det.observe(p + "\n")
assert det.is_looping is False
def test_repeated_shingle_triggers_loop(self) -> None:
det = LoopDetector(window=20, threshold=4, shingle_size=3)
# Identical length-3 shingles: mimics "I'll help you" style repeats in output.
for _ in range(30):
det.observe("a a a ")
assert det.is_looping is True
def test_alternating_two_shingles_triggers_loop(self) -> None:
det = LoopDetector(window=12, threshold=3, shingle_size=2)
for _ in range(40):
det.observe("alpha ")
det.observe("beta ")
assert det.is_looping is True
def test_clear_resets_state(self) -> None:
det = LoopDetector(window=20, threshold=4, shingle_size=3)
for _ in range(80):
det.observe("spam eggs ham ")
assert det.is_looping is True
det.clear()
assert det.is_looping is False
det.observe("fresh tokens here now")
assert det.is_looping is False
def test_at_threshold_not_looping(self) -> None:
det = LoopDetector(window=50, threshold=4, shingle_size=2)
# Two "a a " chunks emit three (a, a) shingles; one more lone "a " emits a fourth — still == threshold.
det.observe("a a ")
det.observe("a a ")
det.observe("a ")
assert det.is_looping is False
def test_one_past_threshold_loops(self) -> None:
det = LoopDetector(window=50, threshold=4, shingle_size=2)
det.observe("a a ")
det.observe("a a ")
det.observe("a ")
det.observe("a ")
assert det.is_looping is True
def test_sliding_window_recovers_when_repetition_ages_out(self) -> None:
det = LoopDetector(window=6, threshold=2, shingle_size=2)
for _ in range(10):
det.observe("x y ")
assert det.is_looping is True
for i in range(30):
det.observe(f"noise{i} token ")
assert det.is_looping is False
@pytest.mark.parametrize(
("window", "threshold", "shingle_size"),
[
(0, 4, 3),
(10, -1, 3),
(10, 4, 0),
],
)
def test_invalid_constructor_raises(
self, window: int, threshold: int, shingle_size: int
) -> None:
with pytest.raises(ValueError):
LoopDetector(window=window, threshold=threshold, shingle_size=shingle_size)