"""Tests for Codex worker JSONL path resolution (cic-e24).
Validates that:
1. get_jsonl_path() returns None (not wrong file) when Codex marker discovery fails
2. get_jsonl_path() uses cached codex_jsonl_path when available
3. is_idle() uses get_jsonl_path() without blind fallback
4. codex_jsonl_path is persisted in to_dict() for event log recovery
5. RecoveredSession includes codex_jsonl_path from event data
"""
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from maniple_mcp.registry import (
ManagedSession,
RecoveredSession,
SessionStatus,
TerminalId,
)
class TestCodexGetJsonlPathNoBlindFallback:
"""get_jsonl_path() must not return wrong session data for Codex workers."""
def _make_codex_session(self, session_id="test-abc1", codex_jsonl_path=None):
"""Create a ManagedSession configured as a Codex worker."""
mock_terminal = MagicMock()
session = ManagedSession(
session_id=session_id,
terminal_session=mock_terminal,
project_path="/test/project",
agent_type="codex",
)
if codex_jsonl_path:
session.codex_jsonl_path = codex_jsonl_path
return session
@patch("maniple_mcp.registry.find_codex_session_by_internal_id")
def test_returns_none_when_discovery_fails(self, mock_find):
"""get_jsonl_path returns None when marker discovery fails, not a random file."""
mock_find.return_value = None
session = self._make_codex_session()
result = session.get_jsonl_path()
assert result is None
# Verify it used generous max_age
mock_find.assert_called_once_with("test-abc1", max_age_seconds=86400)
@patch("maniple_mcp.registry.find_codex_session_by_internal_id")
def test_uses_cached_path_when_available(self, mock_find):
"""get_jsonl_path returns cached codex_jsonl_path without re-discovery."""
cached_path = Path("/tmp/test-codex-session.jsonl")
cached_path.touch()
try:
session = self._make_codex_session(codex_jsonl_path=cached_path)
result = session.get_jsonl_path()
assert result == cached_path
# Should NOT call find_codex_session_by_internal_id when cache is valid
mock_find.assert_not_called()
finally:
cached_path.unlink(missing_ok=True)
@patch("maniple_mcp.registry.find_codex_session_by_internal_id")
def test_falls_through_when_cached_path_missing(self, mock_find):
"""get_jsonl_path tries re-discovery when cached file no longer exists."""
mock_find.return_value = None
nonexistent_path = Path("/tmp/nonexistent-codex-session.jsonl")
session = self._make_codex_session(codex_jsonl_path=nonexistent_path)
result = session.get_jsonl_path()
assert result is None
# Should try marker-based discovery when cached path doesn't exist
mock_find.assert_called_once()
@patch("maniple_mcp.registry.find_codex_session_by_internal_id")
def test_caches_path_on_successful_discovery(self, mock_find):
"""get_jsonl_path caches the path when marker discovery succeeds."""
mock_match = MagicMock()
mock_match.jsonl_path = Path("/tmp/discovered-codex.jsonl")
mock_find.return_value = mock_match
session = self._make_codex_session()
result = session.get_jsonl_path()
assert result == Path("/tmp/discovered-codex.jsonl")
assert session.codex_jsonl_path == Path("/tmp/discovered-codex.jsonl")
@patch("maniple_mcp.registry.find_codex_session_by_internal_id")
def test_uses_86400_max_age_for_rediscovery(self, mock_find):
"""get_jsonl_path uses 24h max_age (not 600s) for marker discovery."""
mock_find.return_value = None
session = self._make_codex_session()
session.get_jsonl_path()
mock_find.assert_called_once_with("test-abc1", max_age_seconds=86400)
class TestCodexIsIdleNoBlindFallback:
"""is_idle() for Codex workers must not use blind fallback."""
def _make_codex_session(self, session_id="test-abc1"):
"""Create a ManagedSession configured as a Codex worker."""
mock_terminal = MagicMock()
return ManagedSession(
session_id=session_id,
terminal_session=mock_terminal,
project_path="/test/project",
agent_type="codex",
)
@patch("maniple_mcp.registry.find_codex_session_by_internal_id")
def test_returns_false_when_no_session_file(self, mock_find):
"""is_idle returns False (not True from wrong file) when discovery fails."""
mock_find.return_value = None
session = self._make_codex_session()
result = session.is_idle()
assert result is False
@patch("maniple_mcp.idle_detection.is_codex_idle")
@patch("maniple_mcp.registry.find_codex_session_by_internal_id")
def test_uses_correct_session_file(self, mock_find, mock_is_idle):
"""is_idle uses the cached/discovered path, not a random recent file."""
mock_find.return_value = None
session = self._make_codex_session()
# Create a temp file for the cached path
cached_path = Path("/tmp/test-correct-session.jsonl")
cached_path.touch()
try:
session.codex_jsonl_path = cached_path
mock_is_idle.return_value = True
result = session.is_idle()
assert result is True
mock_is_idle.assert_called_once_with(cached_path)
# Should NOT have called find_codex_session_by_internal_id
mock_find.assert_not_called()
finally:
cached_path.unlink(missing_ok=True)
class TestCodexJsonlPathInToDict:
"""codex_jsonl_path must be serialized for event log persistence."""
def test_to_dict_includes_codex_jsonl_path_when_set(self):
"""ManagedSession.to_dict() includes codex_jsonl_path as string."""
mock_terminal = MagicMock()
session = ManagedSession(
session_id="test-123",
terminal_session=mock_terminal,
project_path="/test/path",
agent_type="codex",
)
session.codex_jsonl_path = Path("/tmp/codex-session.jsonl")
d = session.to_dict()
assert d["codex_jsonl_path"] == "/tmp/codex-session.jsonl"
def test_to_dict_includes_none_codex_jsonl_path(self):
"""ManagedSession.to_dict() includes None when codex_jsonl_path not set."""
mock_terminal = MagicMock()
session = ManagedSession(
session_id="test-123",
terminal_session=mock_terminal,
project_path="/test/path",
agent_type="claude",
)
d = session.to_dict()
assert d["codex_jsonl_path"] is None
class TestRecoveredSessionCodexJsonlPath:
"""RecoveredSession must persist codex_jsonl_path from event data."""
def test_recovered_session_includes_codex_jsonl_path(self):
"""RecoveredSession stores codex_jsonl_path from snapshot data."""
now = datetime.now()
session = RecoveredSession(
session_id="abc12345",
name="Rick",
project_path="/test/project",
terminal_id=TerminalId("tmux", "%1"),
agent_type="codex",
status=SessionStatus.READY,
last_activity=now,
created_at=now,
event_state="idle",
recovered_at=now,
last_event_ts=now,
codex_jsonl_path="/codex/sessions/2026/02/05/rollout-abc.jsonl",
)
assert session.codex_jsonl_path == "/codex/sessions/2026/02/05/rollout-abc.jsonl"
def test_recovered_session_to_dict_includes_codex_jsonl_path(self):
"""RecoveredSession.to_dict() includes codex_jsonl_path."""
now = datetime.now()
session = RecoveredSession(
session_id="abc12345",
name="Rick",
project_path="/test/project",
terminal_id=None,
agent_type="codex",
status=SessionStatus.READY,
last_activity=now,
created_at=now,
event_state="idle",
recovered_at=now,
last_event_ts=now,
codex_jsonl_path="/codex/sessions/rollout.jsonl",
)
d = session.to_dict()
assert d["codex_jsonl_path"] == "/codex/sessions/rollout.jsonl"
def test_recovered_session_default_codex_jsonl_path_is_none(self):
"""RecoveredSession.codex_jsonl_path defaults to None."""
now = datetime.now()
session = RecoveredSession(
session_id="abc12345",
name="Morty",
project_path="/test/project",
terminal_id=None,
agent_type="claude",
status=SessionStatus.READY,
last_activity=now,
created_at=now,
event_state="idle",
recovered_at=now,
last_event_ts=now,
)
assert session.codex_jsonl_path is None
assert session.to_dict()["codex_jsonl_path"] is None
class TestRecoverFromEventsCodexJsonlPath:
"""recover_from_events must propagate codex_jsonl_path to RecoveredSession."""
def test_codex_jsonl_path_recovered_from_snapshot(self):
"""codex_jsonl_path in worker snapshot data populates RecoveredSession."""
from maniple_mcp.registry import SessionRegistry
registry = SessionRegistry()
snapshot = {
"ts": "2026-02-05T12:00:00Z",
"workers": [
{
"session_id": "codex-worker-1",
"name": "Rick",
"project_path": "/test/project",
"agent_type": "codex",
"state": "idle",
"codex_jsonl_path": "/codex/sessions/2026/02/05/rollout-xyz.jsonl",
"created_at": "2026-02-05T11:00:00Z",
"last_activity": "2026-02-05T11:30:00Z",
},
],
}
report = registry.recover_from_events(snapshot, [])
assert report.added == 1
recovered = registry._recovered_sessions["codex-worker-1"]
assert recovered.codex_jsonl_path == "/codex/sessions/2026/02/05/rollout-xyz.jsonl"
assert recovered.agent_type == "codex"