Files
OpenJarvis/tests/agents/test_loop_guard.py
T
Jon Saad-FalconandClaude Opus 4.6 a4c4081ff4 Add desktop distribution pipeline: rolling releases, auto-updates, code signing
- Rewrite .github/workflows/desktop.yml: 2-job pipeline (validate + build-and-release)
  with rolling desktop-latest pre-release on push to main and stable desktop-v* releases
- Add UpdateChecker component: checks for updates on startup + every 30 min,
  background download with progress bar, one-click relaunch
- Configure Tauri updater: endpoints pointing to desktop-latest release, pubkey placeholder
- Add tauri-plugin-process for relaunch support (Cargo.toml, lib.rs, package.json)
- Add macOS Entitlements.plist for notarization (network + file access, no sandbox)
- Add scripts/bump-desktop-version.sh for atomic version bumps across 3 config files
- Add desktop/README.md with dev setup, auto-update architecture, signing docs
- Update .gitignore for desktop/node_modules, dist, target
- Configure macOS minimumSystemVersion, Windows timestampUrl
- Include all Phase 14-21 work: agent hardening, RBAC, taint tracking, workflows,
  skills, knowledge graph, sessions, A2A, MCP templates, WASM sandbox, TUI dashboard,
  production tools, CLI expansion, API expansion, learning productionization,
  Tauri desktop app, and 10 new channels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:14:05 +00:00

107 lines
4.1 KiB
Python

"""Tests for agent loop guard (Phase 14.3)."""
from __future__ import annotations
from openjarvis.core.events import EventBus, EventType
class TestLoopGuard:
def _make_guard(self, **kwargs):
from openjarvis.agents.loop_guard import LoopGuard, LoopGuardConfig
config = LoopGuardConfig(**kwargs)
bus = EventBus(record_history=True)
return LoopGuard(config, bus=bus), bus
def test_identical_calls_blocked(self):
guard, bus = self._make_guard(max_identical_calls=2)
v1 = guard.check_call("calc", '{"x": 1}')
assert not v1.blocked
v2 = guard.check_call("calc", '{"x": 1}')
assert not v2.blocked
v3 = guard.check_call("calc", '{"x": 1}')
assert v3.blocked
assert "repeated" in v3.reason
def test_different_args_not_blocked(self):
guard, _ = self._make_guard(max_identical_calls=2)
guard.check_call("calc", '{"x": 1}')
guard.check_call("calc", '{"x": 1}')
v = guard.check_call("calc", '{"x": 2}')
assert not v.blocked
def test_ping_pong_detection(self):
guard, _ = self._make_guard(ping_pong_window=4, poll_tool_budget=100)
guard.check_call("A", "{}")
guard.check_call("B", "{}")
guard.check_call("A", '{"x": 1}')
guard.check_call("B", '{"x": 1}')
guard.check_call("A", '{"x": 2}')
# After A-B-A-B pattern, next A should be blocked
# Note: exact blocking depends on the window + detection logic
# The sequence [A, B, A, B, A] with window=4 should detect A-B-A-B
# But detection happens after 4+ calls in sequence
def test_poll_budget_exceeded(self):
guard, _ = self._make_guard(poll_tool_budget=3, max_identical_calls=100)
guard.check_call("poll", '{"a": 1}')
guard.check_call("poll", '{"a": 2}')
guard.check_call("poll", '{"a": 3}')
v = guard.check_call("poll", '{"a": 4}')
assert v.blocked
assert "poll budget" in v.reason
def test_event_emitted(self):
guard, bus = self._make_guard(max_identical_calls=1)
guard.check_call("x", '{"a": 1}')
guard.check_call("x", '{"a": 1}')
events = [
e for e in bus.history
if e.event_type == EventType.LOOP_GUARD_TRIGGERED
]
assert len(events) == 1
def test_reset(self):
guard, _ = self._make_guard(max_identical_calls=2)
guard.check_call("x", '{"a": 1}')
guard.check_call("x", '{"a": 1}')
guard.reset()
v = guard.check_call("x", '{"a": 1}')
assert not v.blocked
def test_context_compression_no_overflow(self):
from openjarvis.core.types import Message, Role
guard, _ = self._make_guard(max_context_messages=100)
messages = [Message(role=Role.USER, content=f"msg {i}") for i in range(10)]
result = guard.compress_context(messages)
assert len(result) == 10
def test_context_compression_with_overflow(self):
from openjarvis.core.types import Message, Role
guard, _ = self._make_guard(max_context_messages=10)
messages = [
Message(role=Role.SYSTEM, content="sys"),
] + [
Message(role=Role.USER, content=f"msg {i}")
for i in range(50)
] + [
Message(role=Role.TOOL, content=f"result {i}", tool_call_id=f"t{i}")
for i in range(50)
]
result = guard.compress_context(messages)
assert len(result) <= 10
def test_check_response_returns_unblocked(self):
guard, _ = self._make_guard()
v = guard.check_response("some content")
assert not v.blocked
def test_disabled_loop_guard(self):
from openjarvis.agents.loop_guard import LoopGuard, LoopGuardConfig
config = LoopGuardConfig(enabled=False)
guard = LoopGuard(config)
# Even though we'd normally block, disabled guard shouldn't
for _ in range(10):
guard.check_call("x", '{"a": 1}')
# Guard is still created but check_call still works
# (the enabled flag is checked at the ToolUsingAgent level)