feat(#152): Implement session rotation (TDD)
Implement session rotation that spawns fresh agents when context reaches 95% threshold. TDD Process: 1. RED: Write comprehensive tests (all initially fail) 2. GREEN: Implement trigger_rotation method (all tests pass) Changes: - Add SessionRotation dataclass to track rotation metrics - Implement trigger_rotation method in ContextMonitor - Add 6 new unit tests covering all acceptance criteria Rotation process: 1. Get current context usage metrics 2. Close current agent session 3. Spawn new agent with same type 4. Transfer next issue to new agent 5. Log rotation event with metrics Test Results: - All 47 tests pass (34 context_monitor + 13 context_compaction) - 97% coverage on context_monitor.py (exceeds 85% requirement) - 97% coverage on context_compaction.py (exceeds 85% requirement) Prevents context exhaustion by starting fresh when compaction is insufficient. Acceptance Criteria (All Met): ✓ Rotation triggered at 95% context threshold ✓ Current session closed cleanly ✓ New agent spawned with same type ✓ Next issue transferred to new agent ✓ Rotation logged with session IDs and context metrics ✓ Unit tests with 85%+ coverage Fixes #152 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -431,6 +431,184 @@ class TestContextMonitor:
|
||||
assert result.success is False
|
||||
assert result.error_message == "API timeout during compaction"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_rotation_closes_current_session(
|
||||
self, mock_claude_api: AsyncMock
|
||||
) -> None:
|
||||
"""Should close current agent session when rotation is triggered."""
|
||||
monitor = ContextMonitor(api_client=mock_claude_api, poll_interval=0.1)
|
||||
|
||||
# Mock context usage at 96% (above ROTATE_THRESHOLD)
|
||||
mock_claude_api.get_context_usage.return_value = {
|
||||
"used_tokens": 192000,
|
||||
"total_tokens": 200000,
|
||||
}
|
||||
|
||||
# Mock close_session API
|
||||
mock_claude_api.close_session = AsyncMock()
|
||||
|
||||
# Trigger rotation
|
||||
result = await monitor.trigger_rotation(
|
||||
agent_id="agent-1",
|
||||
agent_type="sonnet",
|
||||
next_issue_number=42,
|
||||
)
|
||||
|
||||
# Verify session was closed
|
||||
mock_claude_api.close_session.assert_called_once_with("agent-1")
|
||||
assert result.success is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_rotation_spawns_new_agent(
|
||||
self, mock_claude_api: AsyncMock
|
||||
) -> None:
|
||||
"""Should spawn new agent with same type during rotation."""
|
||||
monitor = ContextMonitor(api_client=mock_claude_api, poll_interval=0.1)
|
||||
|
||||
# Mock context usage at 96%
|
||||
mock_claude_api.get_context_usage.return_value = {
|
||||
"used_tokens": 192000,
|
||||
"total_tokens": 200000,
|
||||
}
|
||||
|
||||
# Mock API calls
|
||||
mock_claude_api.close_session = AsyncMock()
|
||||
mock_claude_api.spawn_agent = AsyncMock(return_value={"agent_id": "agent-2"})
|
||||
|
||||
# Trigger rotation
|
||||
result = await monitor.trigger_rotation(
|
||||
agent_id="agent-1",
|
||||
agent_type="opus",
|
||||
next_issue_number=99,
|
||||
)
|
||||
|
||||
# Verify new agent was spawned with same type
|
||||
mock_claude_api.spawn_agent.assert_called_once_with(
|
||||
agent_type="opus",
|
||||
issue_number=99,
|
||||
)
|
||||
assert result.new_agent_id == "agent-2"
|
||||
assert result.success is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_rotation_logs_metrics(
|
||||
self, mock_claude_api: AsyncMock
|
||||
) -> None:
|
||||
"""Should log rotation with session IDs and context metrics."""
|
||||
monitor = ContextMonitor(api_client=mock_claude_api, poll_interval=0.1)
|
||||
|
||||
# Mock context usage at 97%
|
||||
mock_claude_api.get_context_usage.return_value = {
|
||||
"used_tokens": 194000,
|
||||
"total_tokens": 200000,
|
||||
}
|
||||
|
||||
# Mock API calls
|
||||
mock_claude_api.close_session = AsyncMock()
|
||||
mock_claude_api.spawn_agent = AsyncMock(return_value={"agent_id": "agent-2"})
|
||||
|
||||
# Trigger rotation
|
||||
result = await monitor.trigger_rotation(
|
||||
agent_id="agent-1",
|
||||
agent_type="haiku",
|
||||
next_issue_number=123,
|
||||
)
|
||||
|
||||
# Verify result contains metrics
|
||||
assert result.old_agent_id == "agent-1"
|
||||
assert result.new_agent_id == "agent-2"
|
||||
assert result.agent_type == "haiku"
|
||||
assert result.next_issue_number == 123
|
||||
assert result.context_before_tokens == 194000
|
||||
assert result.context_before_percent == 97.0
|
||||
assert result.success is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_rotation_transfers_issue(
|
||||
self, mock_claude_api: AsyncMock
|
||||
) -> None:
|
||||
"""Should transfer next issue to new agent during rotation."""
|
||||
monitor = ContextMonitor(api_client=mock_claude_api, poll_interval=0.1)
|
||||
|
||||
# Mock context usage at 95%
|
||||
mock_claude_api.get_context_usage.return_value = {
|
||||
"used_tokens": 190000,
|
||||
"total_tokens": 200000,
|
||||
}
|
||||
|
||||
# Mock API calls
|
||||
mock_claude_api.close_session = AsyncMock()
|
||||
mock_claude_api.spawn_agent = AsyncMock(return_value={"agent_id": "agent-5"})
|
||||
|
||||
# Trigger rotation
|
||||
result = await monitor.trigger_rotation(
|
||||
agent_id="agent-4",
|
||||
agent_type="sonnet",
|
||||
next_issue_number=77,
|
||||
)
|
||||
|
||||
# Verify issue was transferred to new agent
|
||||
assert result.next_issue_number == 77
|
||||
mock_claude_api.spawn_agent.assert_called_once_with(
|
||||
agent_type="sonnet",
|
||||
issue_number=77,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_rotation_handles_failure(
|
||||
self, mock_claude_api: AsyncMock
|
||||
) -> None:
|
||||
"""Should handle rotation failure and return error details."""
|
||||
monitor = ContextMonitor(api_client=mock_claude_api, poll_interval=0.1)
|
||||
|
||||
# Mock context usage
|
||||
mock_claude_api.get_context_usage.return_value = {
|
||||
"used_tokens": 190000,
|
||||
"total_tokens": 200000,
|
||||
}
|
||||
|
||||
# Mock API failure
|
||||
mock_claude_api.close_session = AsyncMock(side_effect=Exception("Session close failed"))
|
||||
|
||||
# Trigger rotation
|
||||
result = await monitor.trigger_rotation(
|
||||
agent_id="agent-1",
|
||||
agent_type="sonnet",
|
||||
next_issue_number=42,
|
||||
)
|
||||
|
||||
# Verify failure is reported
|
||||
assert result.success is False
|
||||
assert "Session close failed" in result.error_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rotation_triggered_at_95_percent(
|
||||
self, mock_claude_api: AsyncMock
|
||||
) -> None:
|
||||
"""Should trigger rotation when context reaches exactly 95%."""
|
||||
monitor = ContextMonitor(api_client=mock_claude_api, poll_interval=0.1)
|
||||
|
||||
# Mock 95% usage (exactly at ROTATE_THRESHOLD)
|
||||
mock_claude_api.get_context_usage.return_value = {
|
||||
"used_tokens": 190000,
|
||||
"total_tokens": 200000,
|
||||
}
|
||||
|
||||
# Mock API calls
|
||||
mock_claude_api.close_session = AsyncMock()
|
||||
mock_claude_api.spawn_agent = AsyncMock(return_value={"agent_id": "agent-2"})
|
||||
|
||||
# Trigger rotation
|
||||
result = await monitor.trigger_rotation(
|
||||
agent_id="agent-1",
|
||||
agent_type="sonnet",
|
||||
next_issue_number=1,
|
||||
)
|
||||
|
||||
# Verify rotation was successful at exactly 95%
|
||||
assert result.success is True
|
||||
assert result.context_before_percent == 95.0
|
||||
|
||||
|
||||
class TestIssueMetadata:
|
||||
"""Test IssueMetadata model."""
|
||||
|
||||
Reference in New Issue
Block a user