Spaces:
Running
Running
| """Tests for dee/core/agent.py — the Turing OpenRouter orchestrator. | |
| No real network calls: requests.post is monkeypatched. Real end-to-end | |
| verification against the live OpenRouter API is done manually via curl | |
| with a real OPENROUTER_API_KEY, not in the automated suite. | |
| """ | |
| import pytest | |
| from dee.core import agent | |
| class _FakeResponse: | |
| def __init__(self, status_code=200, payload=None, text=""): | |
| self.status_code = status_code | |
| self._payload = payload or {} | |
| self.text = text | |
| def json(self): | |
| return self._payload | |
| def _ok_payload(content="Hello from the model.", cost=0.001, prompt_tokens=0): | |
| return { | |
| "choices": [{"message": {"content": content}, "finish_reason": "stop"}], | |
| "usage": {"total_cost": cost, "prompt_tokens": prompt_tokens}, | |
| } | |
| def _tool_call_payload(call_id="call_1", name="design_crispr_guides", arguments='{"sequence": "ACGT"}', cost=0.001): | |
| return { | |
| "choices": [{ | |
| "message": { | |
| "content": None, | |
| "tool_calls": [{ | |
| "id": call_id, | |
| "type": "function", | |
| "function": {"name": name, "arguments": arguments}, | |
| }], | |
| }, | |
| "finish_reason": "tool_calls", | |
| }], | |
| "usage": {"total_cost": cost}, | |
| } | |
| def test_config_from_env_reads_defaults(monkeypatch): | |
| monkeypatch.setenv("OPENROUTER_API_KEY", "sk-test") | |
| monkeypatch.delenv("AGENT_MODEL", raising=False) | |
| monkeypatch.delenv("AGENT_MAX_STEPS", raising=False) | |
| monkeypatch.delenv("AGENT_MAX_COST_USD", raising=False) | |
| config = agent.OpenRouterConfig.from_env() | |
| assert config is not None | |
| assert config.api_key == "sk-test" | |
| assert config.model == "google/gemini-3.5-flash" | |
| assert config.max_steps == 8 | |
| assert config.max_cost_usd == 0.5 | |
| def test_config_from_env_missing_key_returns_none(monkeypatch): | |
| monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) | |
| assert agent.OpenRouterConfig.from_env() is None | |
| def test_config_from_env_respects_overrides(monkeypatch): | |
| monkeypatch.setenv("OPENROUTER_API_KEY", "sk-test") | |
| monkeypatch.setenv("AGENT_MODEL", "some/other-model") | |
| monkeypatch.setenv("AGENT_MAX_STEPS", "3") | |
| monkeypatch.setenv("AGENT_MAX_COST_USD", "1.25") | |
| config = agent.OpenRouterConfig.from_env() | |
| assert config.model == "some/other-model" | |
| assert config.max_steps == 3 | |
| assert config.max_cost_usd == 1.25 | |
| def test_create_session_defaults(): | |
| session = agent.create_session() | |
| assert session.phase == "design" | |
| assert session.seed == 42 | |
| assert session.history == [] | |
| assert agent.get_session(session.session_id) is session | |
| def test_get_session_missing_returns_none(): | |
| assert agent.get_session("does-not-exist") is None | |
| def test_run_agent_step_success(monkeypatch): | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| assert url == "https://openrouter.ai/api/v1/chat/completions" | |
| assert headers["Authorization"] == "Bearer sk-test" | |
| assert json["model"] == "google/gemini-3.5-flash" | |
| # System prompt + the one user message, no prior history. | |
| assert json["messages"][0]["role"] == "system" | |
| assert json["messages"][-1] == {"role": "user", "content": "hi"} | |
| return _FakeResponse(200, _ok_payload("hello back")) | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| config = agent.OpenRouterConfig( | |
| api_key="sk-test", model="google/gemini-3.5-flash", | |
| max_steps=8, max_cost_usd=0.5, | |
| ) | |
| session = agent.create_session() | |
| result = agent.run_agent_step(config, session, "hi", False) | |
| assert result["ok"] is True | |
| assert result["assistant_message"] == "hello back" | |
| assert result["phase"] == "design" | |
| # History persisted on the session for the next turn. | |
| assert session.history == [ | |
| {"role": "user", "content": "hi"}, | |
| {"role": "assistant", "content": "hello back"}, | |
| ] | |
| def test_run_agent_step_reports_context_window_usage(monkeypatch): | |
| monkeypatch.setattr(agent.requests, "post", | |
| lambda *a, **k: _FakeResponse(200, _ok_payload("hello back", prompt_tokens=12345))) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| result = agent.run_agent_step(config, session, "hi", False) | |
| assert result["context_tokens_used"] == 12345 | |
| assert result["context_tokens_limit"] == agent.CONTEXT_WINDOW_TOKENS | |
| def test_run_agent_step_second_turn_includes_history(monkeypatch): | |
| seen = {} | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| seen["messages"] = json["messages"] | |
| return _FakeResponse(200, _ok_payload("second reply")) | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| config = agent.OpenRouterConfig( | |
| api_key="sk-test", model="m", max_steps=8, max_cost_usd=0.5, | |
| ) | |
| session = agent.create_session() | |
| session.history = [ | |
| {"role": "user", "content": "first"}, | |
| {"role": "assistant", "content": "first reply"}, | |
| ] | |
| agent.run_agent_step(config, session, "second", False) | |
| roles = [m["role"] for m in seen["messages"]] | |
| assert roles == ["system", "user", "assistant", "user"] | |
| def test_run_agent_step_http_error_raises_agent_error(monkeypatch): | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| return _FakeResponse(500, text="upstream exploded") | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| with pytest.raises(agent.AgentError) as excinfo: | |
| agent.run_agent_step(config, session, "hi", False) | |
| assert excinfo.value.kind == "agent_error" | |
| def test_run_agent_step_insufficient_credits_raises_friendly_unavailable_error(monkeypatch): | |
| # OpenRouter's exact error shape for an out-of-credits account/key — | |
| # https://openrouter.ai/docs, confirmed 2026-07-10. | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| return _FakeResponse(402, payload={"error": { | |
| "code": 402, | |
| "message": "Your account or API key has insufficient credits. Add more credits and retry the request.", | |
| }}, text='{"error": {"code": 402, "message": "insufficient credits"}}') | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| with pytest.raises(agent.AgentError) as excinfo: | |
| agent.run_agent_step(config, session, "hi", False) | |
| assert excinfo.value.kind == "agent_unavailable" | |
| # Never surface billing language to the end user. | |
| message = str(excinfo.value) | |
| assert "credit" not in message.lower() | |
| assert "insufficient" not in message.lower() | |
| assert "development" in message.lower() | |
| def test_run_agent_step_empty_choices_raises(monkeypatch): | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| return _FakeResponse(200, {"choices": []}) | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| with pytest.raises(agent.AgentError): | |
| agent.run_agent_step(config, session, "hi", False) | |
| def test_run_agent_step_cost_cap_exceeded_raises(monkeypatch): | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| return _FakeResponse(200, _ok_payload("expensive", cost=5.0)) | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| with pytest.raises(agent.AgentError): | |
| agent.run_agent_step(config, session, "hi", False) | |
| # Nothing should be persisted for a call that got rejected on cost. | |
| assert session.history == [] | |
| def test_run_agent_step_network_error_raises_agent_error(monkeypatch): | |
| import requests as requests_lib | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| raise requests_lib.ConnectionError("dns broke") | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| with pytest.raises(agent.AgentError): | |
| agent.run_agent_step(config, session, "hi", False) | |
| def test_run_agent_step_never_leaks_secret_in_error_message(monkeypatch): | |
| """Regression test for a real incident (2026-07-08): a malformed API key | |
| made the underlying HTTP client raise an exception whose message embeds | |
| the raw header value VERBATIM (a stdlib/urllib3 behavior, not a bug in | |
| this codebase) — that string reached the client through this route's | |
| error response, live, in production. Reproduces that exact failure shape | |
| and asserts the secret never appears anywhere in the raised message.""" | |
| secret = "sk-or-v1-should-never-appear-in-any-error-message" | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| # Exactly the real failure: an exception whose str() embeds the | |
| # Authorization header value, malformed key and all. | |
| raise ValueError( | |
| f"Invalid leading whitespace, reserved character(s), or return " | |
| f"character(s) in header value: 'Bearer {secret}\\n\\n'" | |
| ) | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| config = agent.OpenRouterConfig(api_key=secret, model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| with pytest.raises(agent.AgentError) as excinfo: | |
| agent.run_agent_step(config, session, "hi", False) | |
| assert secret not in str(excinfo.value) | |
| def test_run_agent_step_malformed_json_never_leaks_response_text(monkeypatch): | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| class _BadJson(_FakeResponse): | |
| def json(self): | |
| raise ValueError("not json: token=super-secret-should-not-leak") | |
| return _BadJson(200, text="token=super-secret-should-not-leak") | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| with pytest.raises(agent.AgentError) as excinfo: | |
| agent.run_agent_step(config, session, "hi", False) | |
| assert "super-secret" not in str(excinfo.value) | |
| def test_run_agent_step_executes_tool_call_then_returns_final_reply(monkeypatch): | |
| calls = [ | |
| _tool_call_payload(arguments='{"sequence": "ACGTACGT"}'), | |
| _ok_payload("Here are the top guides."), | |
| ] | |
| def fake_post(url, headers=None, json=None, timeout=None): | |
| return _FakeResponse(200, calls.pop(0)) | |
| monkeypatch.setattr(agent.requests, "post", fake_post) | |
| seen_calls = [] | |
| def fake_execute_tool(name, args, auth_anonymous): | |
| seen_calls.append((name, args, auth_anonymous)) | |
| return {"ok": True, "n_guides": 1, "guides": [{"spacer": "ACGTACGTACGTACGTACGT"}]} | |
| monkeypatch.setattr(agent._tools, "execute_tool", fake_execute_tool) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| result = agent.run_agent_step(config, session, "design me some guides", False) | |
| assert seen_calls == [("design_crispr_guides", {"sequence": "ACGTACGT"}, False)] | |
| assert result["assistant_message"] == "Here are the top guides." | |
| assert result["tool_result"] == {"ok": True, "n_guides": 1, "guides": [{"spacer": "ACGTACGTACGTACGTACGT"}]} | |
| assert result["tool_name"] == "design_crispr_guides" | |
| # Full turn — user ask, the assistant's tool-call, the tool's own | |
| # result, and the final reply — is what the NEXT turn replays as | |
| # context, so all four must be persisted in order. | |
| roles = [m["role"] for m in session.history] | |
| assert roles == ["user", "assistant", "tool", "assistant"] | |
| assert session.history[2]["tool_call_id"] == "call_1" | |
| def test_run_agent_step_passes_auth_anonymous_true_to_tool(monkeypatch): | |
| calls = [_tool_call_payload(), _ok_payload("done")] | |
| monkeypatch.setattr(agent.requests, "post", lambda *a, **k: _FakeResponse(200, calls.pop(0))) | |
| seen = {} | |
| monkeypatch.setattr(agent._tools, "execute_tool", | |
| lambda name, args, auth_anonymous: seen.setdefault("anon", auth_anonymous) or {"ok": False}) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| agent.run_agent_step(config, session, "hi", True) | |
| assert seen["anon"] is True | |
| def test_run_agent_step_cost_cap_checked_across_multiple_calls(monkeypatch): | |
| # Neither call alone exceeds the 0.5 cap, but the tool-call round trip's | |
| # SECOND OpenRouter call pushes the running total over it — must still | |
| # be caught, not just checked once against a single call's cost. | |
| calls = [ | |
| _tool_call_payload(cost=0.3), | |
| _ok_payload("final", cost=0.3), | |
| ] | |
| monkeypatch.setattr(agent.requests, "post", lambda *a, **k: _FakeResponse(200, calls.pop(0))) | |
| monkeypatch.setattr(agent._tools, "execute_tool", lambda *a, **k: {"ok": True}) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=8, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| with pytest.raises(agent.AgentError): | |
| agent.run_agent_step(config, session, "hi", False) | |
| def test_run_agent_step_exhausts_max_steps_returns_fallback_message(monkeypatch): | |
| # The model keeps requesting tool calls and never gives a final | |
| # content-only reply — must degrade gracefully within max_steps calls, | |
| # not hang or raise. | |
| monkeypatch.setattr(agent.requests, "post", lambda *a, **k: _FakeResponse(200, _tool_call_payload(cost=0.0))) | |
| monkeypatch.setattr(agent._tools, "execute_tool", lambda *a, **k: {"ok": True}) | |
| config = agent.OpenRouterConfig(api_key="k", model="m", max_steps=3, max_cost_usd=0.5) | |
| session = agent.create_session() | |
| result = agent.run_agent_step(config, session, "hi", False) | |
| assert result["ok"] is True | |
| assert "ran out of steps" in result["assistant_message"] | |