diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 0512c6c759e80..0bfc6bb2b1d35 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -6,6 +6,9 @@ import path from 'path' export default defineConfig({ base: './', plugins: [react(), tailwindcss()], + test: { + environment: 'jsdom' + }, build: { // Keep desktop packaging stable: Shiki ships many dynamic chunks by // default, and electron-builder can OOM scanning thousands of files. diff --git a/run_agent.py b/run_agent.py index 7e3a211caff12..ac0a33cc9158a 100644 --- a/run_agent.py +++ b/run_agent.py @@ -5324,7 +5324,13 @@ def chat(self, message: str, stream_callback: Optional[callable] = None) -> str: str: Final assistant response """ result = self.run_conversation(message, stream_callback=stream_callback) - return result["final_response"] + # run_conversation has early-return / error paths (interrupted, retries + # exhausted, policy/billing bail) that omit "final_response". Honor the + # documented `-> str` contract instead of crashing with KeyError, so a + # run that never produced a final turn is recorded as a clean empty + # result (treated as a failed run upstream) rather than a harness Python + # traceback that gets misclassified as a launcher/harness fault. + return result.get("final_response") or "" def _run_codex_app_server_turn( self, @@ -5517,10 +5523,11 @@ def main( print(f"šŸ“ž API Calls: {result['api_calls']}") print(f"šŸ’¬ Messages: {len(result['messages'])}") - if result['final_response']: + _final_response = result.get('final_response') + if _final_response: print("\nšŸŽÆ FINAL RESPONSE:") print("-" * 30) - print(result['final_response']) + print(_final_response) # Save sample trajectory to UUID-named file if requested if save_sample: diff --git a/tests/test_chat_final_response_guard.py b/tests/test_chat_final_response_guard.py new file mode 100644 index 0000000000000..0f84a042081ca --- /dev/null +++ b/tests/test_chat_final_response_guard.py @@ -0,0 +1,37 @@ +"""Tests for AIAgent.chat() final_response guard. + +run_agent.py:chat() must honor its ``-> str`` contract even when +run_conversation returns an early-exit / error dict that omits +``"final_response"`` (interrupted, retries exhausted, policy/billing bail). +Before the guard this raised ``KeyError: 'final_response'``, which crashed the +dispatched ``hermes -z`` goal worker and was misclassified upstream as a +harness fault instead of a clean "no final response → failed run". +""" + +from unittest.mock import patch + +from run_agent import AIAgent + + +def _agent_without_init() -> AIAgent: + # Bypass __init__ (heavy: providers/config/IO). We only exercise chat()'s + # handling of the run_conversation return dict. + return AIAgent.__new__(AIAgent) + + +def test_chat_returns_empty_string_when_final_response_key_missing(): + agent = _agent_without_init() + with patch.object(agent, "run_conversation", return_value={"completed": False, "api_calls": 1}): + assert agent.chat("hi") == "" + + +def test_chat_returns_empty_string_when_final_response_is_none(): + agent = _agent_without_init() + with patch.object(agent, "run_conversation", return_value={"final_response": None}): + assert agent.chat("hi") == "" + + +def test_chat_returns_final_response_when_present(): + agent = _agent_without_init() + with patch.object(agent, "run_conversation", return_value={"final_response": "the answer"}): + assert agent.chat("hi") == "the answer"