Merge branch 'main' into bb/gui

This commit is contained in:
emozilla
2026-05-20 16:01:41 -04:00
72 changed files with 2726 additions and 742 deletions
-2
View File
@@ -110,8 +110,6 @@ class TestFlushDeduplication:
db = SessionDB(db_path=db_path)
agent = self._make_agent(db)
# Stub out _save_session_log to avoid file I/O
agent._save_session_log = MagicMock()
conversation_history = [{"role": "user", "content": "old"}]
messages = list(conversation_history) + [
@@ -52,7 +52,7 @@ def _make_agent(monkeypatch, api_mode, provider, response_fn):
kw.update(skip_context_files=True, skip_memory=True, max_iterations=4)
super().__init__(*a, **kw)
self._cleanup_task_resources = self._persist_session = lambda *a, **k: None
self._save_trajectory = self._save_session_log = lambda *a, **k: None
self._save_trajectory = lambda *a, **k: None
def run_conversation(self, msg, conversation_history=None, task_id=None):
self._interruptible_api_call = lambda kw: response_fn()
@@ -9,11 +9,7 @@ def _agent_with_stubbed_persistence():
agent._persist_user_message_override = None
agent._session_db = None
agent._session_messages = []
agent.saved_session_logs = []
agent.flushed_session_db_messages = []
agent._save_session_log = lambda messages: agent.saved_session_logs.append(
[m.copy() for m in messages]
)
agent._flush_messages_to_session_db = lambda messages, conversation_history=None: (
agent.flushed_session_db_messages.append([m.copy() for m in messages])
)
@@ -60,7 +56,7 @@ def test_persist_session_strips_trailing_empty_recovery_scaffolding():
assert messages == [
{"role": "user", "content": "run the task"},
]
assert agent.saved_session_logs[-1] == messages
assert agent.flushed_session_db_messages[-1] == messages
assert all(not msg.get("_empty_recovery_synthetic") for msg in messages)
@@ -77,7 +73,7 @@ def test_persist_session_keeps_unmarked_terminal_empty_response():
{"role": "user", "content": "run the task"},
{"role": "assistant", "content": "(empty)"},
]
assert agent.saved_session_logs[-1] == messages
assert agent.flushed_session_db_messages[-1] == messages
def test_persist_session_strips_marked_terminal_empty_sentinel():
@@ -94,5 +90,5 @@ def test_persist_session_strips_marked_terminal_empty_sentinel():
AIAgent._persist_session(agent, messages, conversation_history=[])
assert messages == [{"role": "user", "content": "continue"}]
assert agent.saved_session_logs[-1] == messages
assert agent.flushed_session_db_messages[-1] == messages
assert all(not msg.get("_empty_terminal_sentinel") for msg in messages)
+43 -36
View File
@@ -554,23 +554,50 @@ class TestExtractReasoning:
assert result == "from structured field"
class TestCleanSessionContent:
def test_none_passthrough(self):
assert AIAgent._clean_session_content(None) is None
class TestSessionJsonSnapshotOptIn:
"""Regression: per-session JSON snapshot writer is opt-in via config.
def test_scratchpad_converted(self):
text = "<REASONING_SCRATCHPAD>think</REASONING_SCRATCHPAD> answer"
result = AIAgent._clean_session_content(text)
assert "<REASONING_SCRATCHPAD>" not in result
assert "<think>" in result
state.db is canonical (PR #29182). ``sessions.write_json_snapshots``
defaults to False, so the agent must NOT write ``session_{sid}.json``
files by default — that behavior caused multi-GB sessions directories
on heavy users. Users can opt back in for external tooling that reads
the JSON files directly.
"""
def test_extra_newlines_cleaned(self):
text = "\n\n\n<think>x</think>\n\n\nafter"
result = AIAgent._clean_session_content(text)
# Should not have excessive newlines around think block
assert "\n\n\n" not in result
# Content after think block must be preserved
assert "after" in result
def test_session_json_disabled_by_default(self, agent):
# Default config: writer is gated off.
assert getattr(agent, "_session_json_enabled", False) is False, (
"sessions.write_json_snapshots must default to False"
)
def test_save_session_log_noops_when_disabled(self, agent, tmp_path):
# When disabled, calling the method must not write any file even
# if logs_dir is writable and messages are non-empty.
agent._session_json_enabled = False
agent.logs_dir = tmp_path
agent._session_messages = [{"role": "user", "content": "hello"}]
agent._save_session_log()
# No session_*.json must appear under logs_dir.
assert list(tmp_path.glob("session_*.json")) == []
def test_save_session_log_writes_when_enabled(self, agent, tmp_path):
# Opt-in path: with the flag on and a session_id, the writer must
# produce ``session_{sid}.json`` under logs_dir.
agent._session_json_enabled = True
agent.logs_dir = tmp_path
messages = [{"role": "user", "content": "hello"}]
agent._save_session_log(messages)
expected = tmp_path / f"session_{agent.session_id}.json"
assert expected.exists(), (
"Opt-in writer must produce session_{sid}.json under logs_dir"
)
def test_logs_dir_retained_for_request_dumps(self, agent):
# logs_dir is kept unconditionally because
# agent_runtime_helpers.dump_api_request_debug still writes
# request_dump_*.json there (debug breadcrumb path), independent of
# the session JSON opt-in.
assert hasattr(agent, "logs_dir")
class TestGetMessagesUpToLastAssistant:
@@ -1901,7 +1928,6 @@ class TestExecuteToolCalls:
agent._interruptible_api_call = _fake_api_call
agent._persist_session = lambda *args, **kwargs: None
agent._save_trajectory = lambda *args, **kwargs: None
agent._save_session_log = lambda *args, **kwargs: None
captured = io.StringIO()
agent._print_fn = lambda *args, **kw: print(*args, file=captured, **kw)
@@ -4253,22 +4279,6 @@ class TestSafeWriter:
assert inner.getvalue() == "test"
class TestSaveSessionLogAtomicWrite:
def test_uses_shared_atomic_json_helper(self, agent, tmp_path):
agent.session_log_file = tmp_path / "session.json"
messages = [{"role": "user", "content": "hello"}]
with patch("run_agent.atomic_json_write", create=True) as mock_atomic_write:
agent._save_session_log(messages)
mock_atomic_write.assert_called_once()
call_args = mock_atomic_write.call_args
assert call_args.args[0] == agent.session_log_file
payload = call_args.args[1]
assert payload["session_id"] == agent.session_id
assert payload["messages"] == messages
assert call_args.kwargs["indent"] == 2
assert call_args.kwargs["default"] is str
# ===================================================================
@@ -5056,12 +5066,9 @@ class TestPersistUserMessageOverride:
{"role": "assistant", "content": "Hi!"},
]
with patch.object(agent, "_save_session_log") as mock_save:
agent._persist_session(messages, [])
agent._persist_session(messages, [])
assert messages[0]["content"] == "Hello there"
saved_messages = mock_save.call_args.args[0]
assert saved_messages[0]["content"] == "Hello there"
first_db_write = agent._session_db.append_message.call_args_list[0].kwargs
assert first_db_write["content"] == "Hello there"
@@ -54,7 +54,6 @@ def _build_agent(monkeypatch):
agent._cleanup_task_resources = lambda task_id: None
agent._persist_session = lambda messages, history=None: None
agent._save_trajectory = lambda messages, user_message, completed: None
agent._save_session_log = lambda messages: None
return agent
@@ -75,7 +74,6 @@ def _build_copilot_agent(monkeypatch, *, model="gpt-5.4"):
agent._cleanup_task_resources = lambda task_id: None
agent._persist_session = lambda messages, history=None: None
agent._save_trajectory = lambda messages, user_message, completed: None
agent._save_session_log = lambda messages: None
return agent
@@ -335,7 +333,6 @@ def test_build_api_kwargs_codex_clamps_minimal_effort(monkeypatch):
agent._cleanup_task_resources = lambda task_id: None
agent._persist_session = lambda messages, history=None: None
agent._save_trajectory = lambda messages, user_message, completed: None
agent._save_session_log = lambda messages: None
kwargs = agent._build_api_kwargs(
[
@@ -365,7 +362,6 @@ def test_build_api_kwargs_codex_preserves_supported_efforts(monkeypatch):
agent._cleanup_task_resources = lambda task_id: None
agent._persist_session = lambda messages, history=None: None
agent._save_trajectory = lambda messages, user_message, completed: None
agent._save_session_log = lambda messages: None
kwargs = agent._build_api_kwargs(
[
@@ -594,7 +590,6 @@ def _build_xai_oauth_agent(monkeypatch):
agent._cleanup_task_resources = lambda task_id: None
agent._persist_session = lambda messages, history=None: None
agent._save_trajectory = lambda messages, user_message, completed: None
agent._save_session_log = lambda messages: None
return agent