mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 19:02:16 +00:00
- 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>
107 lines
4.1 KiB
Python
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)
|