Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
133 KiB
JSON
1 line
133 KiB
JSON
{"repo": "binz2008-star/rico-your-ai-intelligent-job-hunt-partner-in-the-uae", "files": {"tests/test_reasoning_provider_hardening.py": {"5a5153614dd7e092f93d49abd09c928d32fcb456": {"t": 1785747500.1527667, "n": 32885, "h": "5f054e2af562ca74", "c": "\"\"\"\ntests/test_reasoning_provider_hardening.py\n\nDeterministic proof for the remaining reasoning-provider-observability items and\nthe nine merge-gate conditions. No live provider, no real DB, no credentials, no\nnetwork — every provider/stream/models call is mocked.\n\nCoverage map (items → tests):\n 1. fixed error-category vocabulary + categoriser → TestErrorCategoriser\n 2. /models pre-check (advisory, fail-soft, short cache) → TestModelsPrecheck\n 3. /ready endpoint (503 when no reachable valid model) → TestReadyEndpoint\n 4. no-silent-None guard on the HuggingFace cascade → TestHuggingFaceCascadeGuard\n 5. complete health fields + per-attempt elapsed → TestHealthFieldsComplete\n 6. deterministic tests for all of the above → (this whole file)\n\nMerge-gate map (each proven, never asserted in prose):\n G1 model identifier attempted → test_gate1_model_identifier_attempted\n G2 duration of each attempt → test_gate2_duration_of_each_attempt\n G3 failure classified (timeout/400/ → test_gate3_* (categoriser matrix)\n 429/5xx/connection)\n G4 fallback engaged on primary → test_gate4_fallback_engaged_when_primary_timed_out\n timeout\n G5 which model served the success → test_gate5_which_model_served\n G6 heavy prompt not \"unavailable\" → test_gate6_heavy_prompt_tries_fallback_before_unavailable\n before fallback tried\n G7 /health degraded not ok on loss → test_gate7_health_degraded_not_ok\n G8 no secret/PII in log or health → test_gate8_no_secret_in_log_or_health\n G9 stream emits MULTIPLE incremental → TestStreamIsIncrementalNotBlob\n frames, first before last\n\"\"\"\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport time\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nos.environ.setdefault(\"JWT_SECRET\", \"x\" * 32)\n\nimport src.rico_openai_runtime as rt\n\n\n# ── isolation ─────────────────────────────────────────────────────────────────\n\n@pytest.fixture(autouse=True)\ndef _reset_module_state():\n saved_outcome = dict(rt._LAST_REASONING_OUTCOME)\n saved_models = dict(rt._MODELS_STATUS)\n for k in rt._LAST_REASONING_OUTCOME:\n rt._LAST_REASONING_OUTCOME[k] = None\n rt._MODELS_STATUS.update({\n \"provider\": None, \"reachable\": None, \"available_models\": None,\n \"error_category\": None, \"checked_at\": None, \"_checked_monotonic\": None,\n })\n yield\n rt._LAST_REASONING_OUTCOME.update(saved_outcome)\n rt._MODELS_STATUS.update(saved_models)\n\n\nclass _FakeErr(Exception):\n def __init__(self, msg, status_code=None, code=None):\n super().__init__(msg)\n self.status_code = status_code\n self.code = code\n\n\ndef _deepseek_env(monkeypatch, *, hf=False):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.setenv(\"DEEPSEEK_API_KEY\", \"sk-fake-deepseek-key-for-tests\")\n if hf:\n monkeypatch.setenv(\"HF_TOKEN\", \"hf-fake-token-for-tests\")\n else:\n for k in (\"HF_TOKEN\", \"HF_API_TOKEN\", \"HF_API_KEY\", \"HUGGINGFACE_API_KEY\"):\n monkeypatch.delenv(k, raising=False)\n\n\n# ── Item 1: fixed error-category vocabulary + categoriser ─────────────────────\n\nclass TestErrorCategoriser:\n def test_vocabulary_is_the_exact_fixed_set(self):\n assert rt.ERROR_CATEGORIES == (\n \"not_configured\", \"invalid_configuration\", \"unsupported_model\",\n \"authentication\", \"insufficient_balance\", \"rate_limited\", \"timeout\",\n \"connection\", \"provider_4xx\", \"provider_5xx\", \"response_contract\", \"unknown\",\n )\n\n def test_every_result_is_in_the_vocabulary(self):\n samples = [\n dict(status_code=429), dict(status_code=401), dict(status_code=403),\n dict(status_code=402), dict(status_code=400), dict(status_code=404),\n dict(status_code=418), dict(status_code=500), dict(status_code=503),\n dict(error_type=\"ReadTimeout\"), dict(error_type=\"APIConnectionError\"),\n dict(error_type=\"EmptyStream\"), dict(key_present=False), dict(),\n ]\n for s in samples:\n assert rt.categorize_error(**s) in rt.ERROR_CATEGORIES\n\n # G3: the failure is classified into distinct, correct buckets.\n def test_gate3_timeout(self):\n assert rt.categorize_error(error_type=\"ReadTimeout\", message=\"read timed out\") == \"timeout\"\n assert rt.categorize_error(error_type=\"TimeoutError\") == \"timeout\"\n\n def test_gate3_connection(self):\n assert rt.categorize_error(error_type=\"APIConnectionError\") == \"connection\"\n assert rt.categorize_error(message=\"Connection refused\") == \"connection\"\n\n def test_gate3_400_unsupported_vs_invalid(self):\n # 400 that names a retired/unknown model → unsupported_model (deterministic-ish)\n assert rt.categorize_error(status_code=400, provider_error_code=\"model_not_found\") == \"unsupported_model\"\n assert rt.categorize_error(status_code=400, message=\"Model deepseek-chat does not exist\") == \"unsupported_model\"\n # 400 that is a plain malformed request → invalid_configuration\n assert rt.categorize_error(status_code=400, message=\"invalid parameter foo\") == \"invalid_configuration\"\n\n def test_gate3_429_rate_limited(self):\n assert rt.categorize_error(status_code=429) == \"rate_limited\"\n\n def test_gate3_5xx(self):\n assert rt.categorize_error(status_code=500) == \"provider_5xx\"\n assert rt.categorize_error(status_code=503) == \"provider_5xx\"\n\n def test_authentication_and_balance_and_notconfigured(self):\n assert rt.categorize_error(status_code=401) == \"authentication\"\n assert rt.categorize_error(status_code=403) == \"authentication\"\n assert rt.categorize_error(status_code=402) == \"insufficient_balance\"\n assert rt.categorize_error(message=\"Insufficient Balance\") == \"insufficient_balance\"\n assert rt.categorize_error(key_present=False) == \"not_configured\"\n\n def test_response_contract_and_other_4xx_and_unknown(self):\n assert rt.categorize_error(error_type=\"EmptyStream\") == \"response_contract\"\n assert rt.categorize_error(message=\"returned empty text\") == \"response_contract\"\n assert rt.categorize_error(status_code=418) == \"provider_4xx\"\n assert rt.categorize_error() == \"unknown\"\n\n def test_canonical_category_is_wired_into_failure_payload_not_raw_class(self, monkeypatch):\n _deepseek_env(monkeypatch)\n fake_client = MagicMock()\n fake_client.chat.completions.create.side_effect = _FakeErr(\n \"Model not found\", status_code=400, code=\"model_not_found\")\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n # canonical vocabulary value, not the exception class name\n assert result[\"error_category\"] == \"unsupported_model\"\n assert result[\"error_category\"] in rt.ERROR_CATEGORIES\n # health surfaces the canonical category too\n assert rt.get_reasoning_health()[\"error_category\"] == \"unsupported_model\"\n\n\n# ── Item 2: /models pre-check (advisory, fail-soft, short cache) ───────────────\n\ndef _fake_models_client(ids):\n client = MagicMock()\n listing = MagicMock()\n listing.data = [MagicMock(id=i) for i in ids]\n client.models.list.return_value = listing\n return client\n\n\nclass TestModelsPrecheck:\n def test_probe_populates_available_set(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client(\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is True\n assert rt.is_model_supported(\"some-retired-id\") is False\n assert rt.get_models_status()[\"reachable\"] is True\n assert rt.get_models_status()[\"available_count\"] == 2\n\n def test_advisory_skip_drops_absent_but_never_empties(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([\"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n # v4-flash absent → dropped; v4-pro kept\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == [\"deepseek-v4-pro\"]\n # if ALL are absent, the chain is returned untouched (never refuse to serve)\n assert rt._advisory_chain([\"x-only\", \"y-only\"]) == [\"x-only\", \"y-only\"]\n\n def test_failsoft_on_probe_error_serves_chain_untouched(self, monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.models.list.side_effect = _FakeErr(\"boom\", status_code=None)\n with patch.object(rt, \"_build_client\", return_value=client):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt._MODELS_STATUS[\"reachable\"] is False\n assert rt._MODELS_STATUS[\"available_models\"] is None\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None # unknown, not False\n assert rt._advisory_chain([\"deepseek-v4-flash\"]) == [\"deepseek-v4-flash\"]\n\n def test_empty_data_array_carries_on_untouched(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n # empty list must NOT be read as \"every model unsupported\"\n assert rt._MODELS_STATUS[\"available_models\"] is None\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == \\\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_not_configured_when_key_absent(self, monkeypatch):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.delenv(\"DEEPSEEK_API_KEY\", raising=False)\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt._MODELS_STATUS[\"reachable\"] is False\n assert rt._MODELS_STATUS[\"error_category\"] == \"not_configured\"\n\n def test_advisory_verdict_expires_after_short_window(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([\"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is False # fresh → advisory active\n # age the probe past the advisory window → verdict expires to \"unknown\"\n rt._MODELS_STATUS[\"_checked_monotonic\"] = time.monotonic() - (rt._MODELS_ADVISORY_WINDOW_SECONDS + 1)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == \\\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_chat_path_never_probes_models(self, monkeypatch):\n # A user chat turn must NEVER trigger the /models discovery round-trip.\n _deepseek_env(monkeypatch)\n client = _fake_models_client([\"deepseek-v4-flash\"])\n client.chat.completions.create.return_value = _chat_response(\"hello\")\n with patch.object(rt, \"_build_client\", return_value=client):\n rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n client.models.list.assert_not_called()\n\n\ndef _chat_response(text):\n resp = MagicMock()\n msg = MagicMock()\n msg.content = text\n choice = MagicMock()\n choice.message = msg\n resp.choices = [choice]\n return resp\n\n\n# ── Item 3: /ready endpoint ───────────────────────────────────────────────────\n\n@pytest.fixture(scope=\"module\")\ndef client():\n from fastapi.testclient import TestClient\n from src.api.app import app\n return TestClient(app, raise_server_exceptions=False)\n\n\nclass TestReadyEndpoint:\n def test_ready_200_when_configured_and_models_unknown(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n r = client.get(\"/ready\")\n assert r.status_code == 200\n assert r.json()[\"ready\"] is True\n\n def test_ready_503_when_not_configured(self, client, monkeypatch):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.delenv(\"DEEPSEEK_API_KEY\", raising=False)\n r = client.get(\"/ready\")\n assert r.status_code == 503\n body = r.json()\n assert body[\"ready\"] is False\n assert \"not_configured\" in body[\"reasons\"]\n\n def test_ready_503_when_no_chain_model_is_valid(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n # a FRESH successful probe that lists none of the resolved chain\n rt._MODELS_STATUS.update({\n \"provider\": \"deepseek\", \"reachable\": True,\n \"available_models\": frozenset({\"totally-different-model\"}),\n \"error_category\": None, \"checked_at\": rt._now_iso(),\n \"_checked_monotonic\": time.monotonic(),\n })\n r = client.get(\"/ready\")\n assert r.status_code == 503\n assert \"no_supported_model\" in r.json()[\"reasons\"]\n\n def test_ready_is_cache_only_no_upstream_probe(self, client, monkeypatch):\n # An inbound /ready must never trigger an outbound (billable) provider call.\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"refresh_model_availability\") as probe:\n client.get(\"/ready\")\n probe.assert_not_called()\n\n def test_health_stays_200_even_when_degraded(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n status_code=400, model_attempted=\"deepseek-v4-flash\",\n )\n r = client.get(\"/health\")\n assert r.status_code == 200 # never restart-loops Render's liveness probe\n assert r.json()[\"status\"] == \"degraded\"\n\n\n# ── Item 4: no-silent-None guard on the HuggingFace cascade ───────────────────\n\nclass TestHuggingFaceCascadeGuard:\n def _agent(self):\n from src.rico_openai_agent import RicoOpenAIAgent\n return RicoOpenAIAgent()\n\n def test_hf_returns_none_records_engaged_but_failed_and_logs(self, monkeypatch, caplog):\n _deepseek_env(monkeypatch, hf=True)\n agent = self._agent()\n # premium (deepseek) fails; HF backup returns None (silent failure)\n fail = {\"success\": False, \"text\": rt._FALLBACK_TEXT, \"error\": \"x\",\n \"error_code\": \"reasoning_provider_unavailable\"}\n with (\n patch(\"src.rico_openai_agent.call_openai_minimal\", return_value=fail),\n patch.object(agent, \"_call_hf_free\", return_value=None),\n caplog.at_level(logging.ERROR),\n ):\n agent.respond(\"give me one CV tip\")\n h = rt.get_reasoning_health()\n assert h[\"fallback_engaged\"] is True\n assert h[\"fallback_succeeded\"] is False\n assert any(\"reasoning_fallback_empty\" in rec.message for rec in caplog.records)\n\n def test_hf_success_records_engaged_and_succeeded(self, monkeypatch):\n _deepseek_env(monkeypatch, hf=True)\n agent = self._agent()\n fail = {\"success\": False, \"text\": rt._FALLBACK_TEXT}\n good = {\"type\": \"hf_response\", \"message\": \"Use metric numbers.\", \"provider\": \"huggingface\",\n \"model\": \"HuggingFaceH4/zephyr-7b-beta\"}\n with (\n patch(\"src.rico_openai_agent.call_openai_minimal\", return_value=fail),\n patch.object(agent, \"_call_hf_free\", return_value=good),\n ):\n out = agent.respond(\"give me one CV tip\")\n assert out[\"provider\"] == \"huggingface\"\n h = rt.get_reasoning_health()\n assert h[\"fallback_engaged\"] is True\n assert h[\"fallback_succeeded\"] is True\n\n def test_guard_does_not_run_inside_call_openai_minimal(self, monkeypatch):\n # call_openai_minimal must NOT touch the HF fallback bookkeeping.\n _deepseek_env(monkeypatch, hf=True)\n fake_client = MagicMock()\n fake_client.chat.completions.create.side_effect = _FakeErr(\"boom\", status_code=500)\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert rt._LAST_REASONING_OUTCOME[\"fallback_engaged\"] is None\n\n\n# ── Item 5: complete health fields + per-attempt elapsed ──────────────────────\n\nclass TestHealthFieldsComplete:\n def test_health_has_all_required_fields(self, monkeypatch):\n _deepseek_env(monkeypatch, hf=True)\n h = rt.get_reasoning_health()\n for key in (\"provider\", \"configured\", \"model_chain\", \"fallback_exists\",\n \"reachable\", \"last_checked_at\", \"last_success_at\", \"last_status\",\n \"error_category\", \"attempts\", \"last_elapsed_ms\", \"models_precheck\",\n \"last_stream_frames\"):\n assert key in h, f\"missing health field: {key}\"\n assert h[\"model_chain\"] == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n assert h[\"fallback_exists\"] is True\n\n def test_no_secret_in_health_output(self, monkeypatch):\n import json\n _deepseek_env(monkeypatch)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"authentication\",\n status_code=401, message=\"Bad key sk-fake-deepseek-key-for-tests in body\",\n model_attempted=\"deepseek-v4-flash\",\n )\n blob = json.dumps(rt.get_reasoning_health())\n assert \"sk-fake-deepseek-key-for-tests\" not in blob\n assert \"sk-***REDACTED***\" in blob or \"REDACTED\" in blob\n\n # G1 + G2 + G5: model attempted, per-attempt duration, model that served.\n def test_gate1_model_identifier_attempted(self, monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = _chat_response(\"ok\")\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert result[\"attempts\"][0][\"model\"] == \"deepseek-v4-flash\"\n assert rt.get_reasoning_health()[\"last_model_attempted\"] == \"deepseek-v4-flash\"\n\n def test_gate2_duration_of_each_attempt(self, monkeypatch):\n _deepseek_env(monkeypatch)\n calls = []\n\n def create(model, **kw):\n calls.append(model)\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"Model not found\", status_code=400, code=\"model_not_found\")\n return _chat_response(\"served\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n attempts = result[\"attempts\"]\n assert len(attempts) == 2\n for a in attempts:\n assert isinstance(a[\"elapsed_ms\"], int) and a[\"elapsed_ms\"] >= 0\n # failed attempt carries a canonical category, success carries None\n assert attempts[0][\"category\"] == \"unsupported_model\"\n assert attempts[0][\"ok\"] is False\n assert attempts[1][\"ok\"] is True\n\n def test_gate5_which_model_served(self, monkeypatch):\n _deepseek_env(monkeypatch)\n\n def create(model, **kw):\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"nope\", status_code=400, code=\"model_not_found\")\n return _chat_response(\"from pro\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert result[\"model\"] == \"deepseek-v4-pro\" # the model that actually served\n assert result[\"text\"] == \"from pro\"\n\n\n# ── Merge gates 4, 6, 7, 8 ────────────────────────────────────────────────────\n\nclass TestMergeGates:\n def test_gate4_fallback_engaged_when_primary_timed_out(self, monkeypatch):\n # Non-stream: a primary TIMEOUT must trigger the hop to the fallback model.\n _deepseek_env(monkeypatch)\n seen = []\n\n def create(model, **kw):\n seen.append(model)\n if model == \"deepseek-v4-flash\":\n raise TimeoutError(\"read timed out\")\n return _chat_response(\"recovered\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"heavy\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert seen == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n assert result[\"attempts\"][0][\"category\"] == \"timeout\"\n\n def test_gate6_heavy_prompt_tries_fallback_before_unavailable(self, monkeypatch):\n # A heavy prompt whose primary fails must NOT return the unavailable text\n # until the fallback has actually been attempted.\n _deepseek_env(monkeypatch)\n\n def create(model, **kw):\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"overloaded\", status_code=503)\n return _chat_response(\"A thorough three-part answer.\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"write me three cover letters\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert result[\"text\"] != rt._FALLBACK_TEXT\n assert [a[\"model\"] for a in result[\"attempts\"]] == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_gate7_health_degraded_not_ok(self, monkeypatch):\n _deepseek_env(monkeypatch) # no HF fallback\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n status_code=400, model_attempted=\"deepseek-v4-flash\",\n )\n assert rt.get_reasoning_health()[\"degraded\"] is True\n\n def test_gate7_failed_fallback_still_degraded_even_with_hf_configured(self, monkeypatch):\n # A backup that fails invisibly must still ring the alarm.\n _deepseek_env(monkeypatch, hf=True)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n model_attempted=\"deepseek-v4-flash\",\n )\n rt.record_fallback_outcome(engaged=True, succeeded=False,\n error_category=\"response_contract\")\n assert rt.get_reasoning_health()[\"degraded\"] is True\n\n def test_gate8_no_secret_in_log_or_health(self, monkeypatch, caplog):\n # Two distinct guarantees:\n # (a) an API key that leaks into an upstream error string is REDACTED in\n # both the log and the health output;\n # (b) the user's own prompt text and profile content are NEVER echoed\n # into a log or the health output (Rico never routes user content\n # into either).\n _deepseek_env(monkeypatch)\n secret = \"sk-live-DEEPSEEKKEY0123456789abcdef\"\n prompt_pii = \"Roben Edwan wants a compliance officer role and earns 45000 AED\"\n profile_pii = \"PASSPORT A1234567 : private banking client list\"\n fake_client = MagicMock()\n # Provider echoes the key in its error body (the exact leak we redact).\n fake_client.chat.completions.create.side_effect = _FakeErr(\n f\"401 Unauthorized: bad key {secret}\", status_code=401)\n with caplog.at_level(logging.DEBUG):\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n rt.call_openai_minimal(prompt_pii, profile_context=profile_pii, provider=\"deepseek\")\n\n import json\n health_blob = json.dumps(rt.get_reasoning_health())\n log_blob = \"\\n\".join(r.getMessage() for r in caplog.records)\n\n # (a) the key (and any partial key) never appears un-redacted.\n assert secret not in health_blob\n assert secret not in log_blob\n assert \"DEEPSEEKKEY\" not in health_blob\n assert \"DEEPSEEKKEY\" not in log_blob\n # (b) neither the user's prompt nor profile content is ever logged or surfaced.\n for pii in (prompt_pii, profile_pii, \"Roben Edwan\", \"PASSPORT A1234567\", \"45000 AED\"):\n assert pii not in health_blob, f\"user content leaked into health: {pii!r}\"\n assert pii not in log_blob, f\"user content leaked into log: {pii!r}\"\n\n\n# ── Gate 9: the stream is incremental, NOT a single buffered blob ─────────────\n\nclass _Delta:\n def __init__(self, content):\n self.content = content\n\n\nclass _Choice:\n def __init__(self, content):\n self.delta = _Delta(content)\n\n\nclass _Chunk:\n def __init__(self, content):\n self.choices = [_Choice(content)]\n\n\nclass _LazyStream:\n \"\"\"A provider stream that RECORDS the order in which chunks are pulled, so a\n test can prove the runtime yields each token as it arrives rather than\n buffering the whole answer into a single blob.\"\"\"\n\n def __init__(self, texts, produced_log):\n self._texts = texts\n self._log = produced_log\n\n def __iter__(self):\n for t in self._texts:\n self._log.append(t)\n yield _Chunk(t)\n\n\nclass TestStreamIsIncrementalNotBlob:\n def test_gate9_runtime_streams_incrementally_multiple_frames(self, monkeypatch):\n _deepseek_env(monkeypatch)\n produced: list = []\n\n def create(model, **kw):\n assert kw.get(\"stream\") is True\n return _LazyStream([\"The \", \"first \", \"part \", \"then more.\"], produced)\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n gen = rt.call_openai_stream(\"write me a long answer\", provider=\"deepseek\")\n first = next(gen) # pull ONLY the first output frame\n # Lazy incremental: only the FIRST source chunk has been produced so far —\n # a buffered-blob implementation would have pulled all four before yielding.\n assert produced == [\"The \"]\n assert first == \"The \"\n rest = list(gen) # drain the remainder\n\n frames = [first] + rest\n # MULTIPLE incremental frames (a single-frame 200 is the defect, not a pass).\n assert len(frames) == 4\n assert \"\".join(frames) == \"The first part then more.\"\n # first frame strictly precedes the last in production order\n assert produced.index(\"The \") < produced.index(\"then more.\")\n # frame-count + first-frame latency recorded in observability\n h = rt.get_reasoning_health()\n assert h[\"last_stream_frames\"] == 4\n assert isinstance(h[\"last_first_frame_ms\"], int)\n\n def test_gate9_wire_emits_multiple_token_frames_not_one_blob(self, client, monkeypatch):\n # End-to-end through the real SSE endpoint + real call_openai_stream: the\n # wire must carry MULTIPLE `data: {\"type\":\"token\"}` frames, not one blob.\n _deepseek_env(monkeypatch)\n produced: list = []\n\n def create(model, **kw):\n return _LazyStream([\"Point one. \", \"Point two. \", \"Point three.\"], produced)\n\n stream_client = MagicMock()\n stream_client.chat.completions.create.side_effect = create\n\n from src.services.chat_service import ChatPreflight\n from src.services.subscription_gating import GateCheck\n gate = GateCheck(allowed=True, feature=\"monthly_ai_message_limit\", usage=0,\n limit=300, remaining=100, plan=\"free\", message=\"ok\")\n with (\n patch(\"src.api.routers.rico_chat.get_current_user\", return_value={\"email\": \"u@example.com\"}),\n patch(\"src.services.chat_service.run_chat_preflight\",\n return_value=ChatPreflight(terminal=None, gate=gate)),\n patch(\"src.services.chat_service.should_stream_ai\", return_value=True),\n patch(\"src.repositories.profile_repo.get_profile\", return_value=None),\n patch(\"src.rico_chat_api.RicoChatAPI._build_openai_context\", return_value={}),\n patch(\"src.rico_chat_api.RicoChatAPI._append_chat\"),\n patch.object(rt, \"_build_client\", return_value=stream_client),\n ):\n res = client.post(\"/api/v1/rico/chat/stream\", json={\"message\": \"give me three points\"})\n assert res.status_code == 200 # 200, but that is NOT the proof — the frames are\n token_frames = res.text.count('\"type\": \"token\"') + res.text.count('\"type\":\"token\"')\n assert token_frames >= 3, f\"expected multiple incremental token frames, got {token_frames}\"\n assert res.text.count(rt._FALLBACK_TEXT) == 0 # never the buffered fallback blob\n\n\n# ── DeepSeek explicit thinking contract ──────────────────────────────────────\n\ndef _reasoning_chunk(*, content=None, reasoning_content=None):\n delta = MagicMock()\n delta.content = content\n delta.reasoning_content = reasoning_content\n choice = MagicMock()\n choice.delta = delta\n chunk = MagicMock()\n chunk.choices = [choice]\n return chunk\n\n\ndef test_deepseek_non_stream_explicitly_disables_thinking(monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = _chat_response(\"Final answer\")\n\n with (\n patch.object(rt, \"_build_client\", return_value=client),\n patch.object(rt, \"_deepseek_model_chain\", return_value=[\"deepseek-v4-flash\"]),\n patch.object(rt, \"_advisory_chain\", side_effect=lambda models, provider=None: models),\n ):\n result = rt.call_openai_minimal(\"hello\", provider=\"deepseek\")\n\n kwargs = client.chat.completions.create.call_args.kwargs\n assert result[\"success\"] is True\n assert result[\"text\"] == \"Final answer\"\n assert kwargs[\"extra_body\"] == {\"thinking\": {\"type\": \"disabled\"}}\n assert \"stream\" not in kwargs\n\n\ndef test_deepseek_stream_explicitly_disables_thinking_and_never_yields_reasoning(monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = iter([\n _reasoning_chunk(reasoning_content=\"private reasoning\"),\n _reasoning_chunk(content=\"Final answer\"),\n ])\n\n with (\n patch.object(rt, \"_build_client\", return_value=client),\n patch.object(rt, \"_deepseek_model_chain\", return_value=[\"deepseek-v4-flash\"]),\n patch.object(rt, \"_advisory_chain\", side_effect=lambda models, provider=None: models),\n ):\n output = list(rt.call_openai_stream(\"hello\", provider=\"deepseek\"))\n\n kwargs = client.chat.completions.create.call_args.kwargs\n assert output == [\"Final answer\"]\n assert \"private reasoning\" not in \"\".join(output)\n assert kwargs[\"extra_body\"] == {\"thinking\": {\"type\": \"disabled\"}}\n assert kwargs[\"stream\"] is True\n\n\ndef test_reasoning_only_stream_stays_fail_closed_and_falls_back_once(monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n\n def create(*, model, **kwargs):\n assert kwargs[\"extra_body\"] == {\"thinking\": {\"type\": \"disabled\"}}\n if model == \"deepseek-v4-flash\":\n return iter([_reasoning_chunk(reasoning_content=\"private reasoning\")])\n return iter([_reasoning_chunk(content=\"Fallback answer\")])\n\n client.chat.completions.create.side_effect = create\n\n with (\n patch.object(rt, \"_build_client\", return_value=client),\n patch.object(\n rt,\n \"_deepseek_model_chain\",\n return_value=[\"deepseek-v4-flash\", \"deepseek-v4-pro\"],\n ),\n patch.object(rt, \"_advisory_chain\", side_effect=lambda models, provider=None: models),\n ):\n output = list(rt.call_openai_stream(\"hello\", provider=\"deepseek\"))\n\n attempts = rt.get_reasoning_health()[\"attempts\"]\n assert output == [\"Fallback answer\"]\n assert client.chat.completions.create.call_count == 2\n assert attempts[0][\"model\"] == \"deepseek-v4-flash\"\n assert attempts[0][\"category\"] == \"response_contract\"\n assert attempts[0][\"ok\"] is False\n assert attempts[1][\"model\"] == \"deepseek-v4-pro\"\n assert attempts[1][\"ok\"] is True\n\n\ndef test_openai_responses_request_contract_is_unchanged(monkeypatch):\n monkeypatch.setenv(\"OPENAI_API_KEY\", \"sk-fake-openai-key-for-tests\")\n client = MagicMock()\n response = MagicMock()\n response.output_text = \"OpenAI answer\"\n client.responses.create.return_value = response\n\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hello\", provider=\"openai\")\n\n kwargs = client.responses.create.call_args.kwargs\n assert result[\"success\"] is True\n assert result[\"text\"] == \"OpenAI answer\"\n assert \"extra_body\" not in kwargs\n assert \"thinking\" not in kwargs\n"}, "3bcac4d5d418b3711b71976733b4baf3d6876570": {"t": 1785773620.572083, "n": 32885, "h": "5f054e2af562ca74", "c": "\"\"\"\ntests/test_reasoning_provider_hardening.py\n\nDeterministic proof for the remaining reasoning-provider-observability items and\nthe nine merge-gate conditions. No live provider, no real DB, no credentials, no\nnetwork — every provider/stream/models call is mocked.\n\nCoverage map (items → tests):\n 1. fixed error-category vocabulary + categoriser → TestErrorCategoriser\n 2. /models pre-check (advisory, fail-soft, short cache) → TestModelsPrecheck\n 3. /ready endpoint (503 when no reachable valid model) → TestReadyEndpoint\n 4. no-silent-None guard on the HuggingFace cascade → TestHuggingFaceCascadeGuard\n 5. complete health fields + per-attempt elapsed → TestHealthFieldsComplete\n 6. deterministic tests for all of the above → (this whole file)\n\nMerge-gate map (each proven, never asserted in prose):\n G1 model identifier attempted → test_gate1_model_identifier_attempted\n G2 duration of each attempt → test_gate2_duration_of_each_attempt\n G3 failure classified (timeout/400/ → test_gate3_* (categoriser matrix)\n 429/5xx/connection)\n G4 fallback engaged on primary → test_gate4_fallback_engaged_when_primary_timed_out\n timeout\n G5 which model served the success → test_gate5_which_model_served\n G6 heavy prompt not \"unavailable\" → test_gate6_heavy_prompt_tries_fallback_before_unavailable\n before fallback tried\n G7 /health degraded not ok on loss → test_gate7_health_degraded_not_ok\n G8 no secret/PII in log or health → test_gate8_no_secret_in_log_or_health\n G9 stream emits MULTIPLE incremental → TestStreamIsIncrementalNotBlob\n frames, first before last\n\"\"\"\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport time\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nos.environ.setdefault(\"JWT_SECRET\", \"x\" * 32)\n\nimport src.rico_openai_runtime as rt\n\n\n# ── isolation ─────────────────────────────────────────────────────────────────\n\n@pytest.fixture(autouse=True)\ndef _reset_module_state():\n saved_outcome = dict(rt._LAST_REASONING_OUTCOME)\n saved_models = dict(rt._MODELS_STATUS)\n for k in rt._LAST_REASONING_OUTCOME:\n rt._LAST_REASONING_OUTCOME[k] = None\n rt._MODELS_STATUS.update({\n \"provider\": None, \"reachable\": None, \"available_models\": None,\n \"error_category\": None, \"checked_at\": None, \"_checked_monotonic\": None,\n })\n yield\n rt._LAST_REASONING_OUTCOME.update(saved_outcome)\n rt._MODELS_STATUS.update(saved_models)\n\n\nclass _FakeErr(Exception):\n def __init__(self, msg, status_code=None, code=None):\n super().__init__(msg)\n self.status_code = status_code\n self.code = code\n\n\ndef _deepseek_env(monkeypatch, *, hf=False):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.setenv(\"DEEPSEEK_API_KEY\", \"sk-fake-deepseek-key-for-tests\")\n if hf:\n monkeypatch.setenv(\"HF_TOKEN\", \"hf-fake-token-for-tests\")\n else:\n for k in (\"HF_TOKEN\", \"HF_API_TOKEN\", \"HF_API_KEY\", \"HUGGINGFACE_API_KEY\"):\n monkeypatch.delenv(k, raising=False)\n\n\n# ── Item 1: fixed error-category vocabulary + categoriser ─────────────────────\n\nclass TestErrorCategoriser:\n def test_vocabulary_is_the_exact_fixed_set(self):\n assert rt.ERROR_CATEGORIES == (\n \"not_configured\", \"invalid_configuration\", \"unsupported_model\",\n \"authentication\", \"insufficient_balance\", \"rate_limited\", \"timeout\",\n \"connection\", \"provider_4xx\", \"provider_5xx\", \"response_contract\", \"unknown\",\n )\n\n def test_every_result_is_in_the_vocabulary(self):\n samples = [\n dict(status_code=429), dict(status_code=401), dict(status_code=403),\n dict(status_code=402), dict(status_code=400), dict(status_code=404),\n dict(status_code=418), dict(status_code=500), dict(status_code=503),\n dict(error_type=\"ReadTimeout\"), dict(error_type=\"APIConnectionError\"),\n dict(error_type=\"EmptyStream\"), dict(key_present=False), dict(),\n ]\n for s in samples:\n assert rt.categorize_error(**s) in rt.ERROR_CATEGORIES\n\n # G3: the failure is classified into distinct, correct buckets.\n def test_gate3_timeout(self):\n assert rt.categorize_error(error_type=\"ReadTimeout\", message=\"read timed out\") == \"timeout\"\n assert rt.categorize_error(error_type=\"TimeoutError\") == \"timeout\"\n\n def test_gate3_connection(self):\n assert rt.categorize_error(error_type=\"APIConnectionError\") == \"connection\"\n assert rt.categorize_error(message=\"Connection refused\") == \"connection\"\n\n def test_gate3_400_unsupported_vs_invalid(self):\n # 400 that names a retired/unknown model → unsupported_model (deterministic-ish)\n assert rt.categorize_error(status_code=400, provider_error_code=\"model_not_found\") == \"unsupported_model\"\n assert rt.categorize_error(status_code=400, message=\"Model deepseek-chat does not exist\") == \"unsupported_model\"\n # 400 that is a plain malformed request → invalid_configuration\n assert rt.categorize_error(status_code=400, message=\"invalid parameter foo\") == \"invalid_configuration\"\n\n def test_gate3_429_rate_limited(self):\n assert rt.categorize_error(status_code=429) == \"rate_limited\"\n\n def test_gate3_5xx(self):\n assert rt.categorize_error(status_code=500) == \"provider_5xx\"\n assert rt.categorize_error(status_code=503) == \"provider_5xx\"\n\n def test_authentication_and_balance_and_notconfigured(self):\n assert rt.categorize_error(status_code=401) == \"authentication\"\n assert rt.categorize_error(status_code=403) == \"authentication\"\n assert rt.categorize_error(status_code=402) == \"insufficient_balance\"\n assert rt.categorize_error(message=\"Insufficient Balance\") == \"insufficient_balance\"\n assert rt.categorize_error(key_present=False) == \"not_configured\"\n\n def test_response_contract_and_other_4xx_and_unknown(self):\n assert rt.categorize_error(error_type=\"EmptyStream\") == \"response_contract\"\n assert rt.categorize_error(message=\"returned empty text\") == \"response_contract\"\n assert rt.categorize_error(status_code=418) == \"provider_4xx\"\n assert rt.categorize_error() == \"unknown\"\n\n def test_canonical_category_is_wired_into_failure_payload_not_raw_class(self, monkeypatch):\n _deepseek_env(monkeypatch)\n fake_client = MagicMock()\n fake_client.chat.completions.create.side_effect = _FakeErr(\n \"Model not found\", status_code=400, code=\"model_not_found\")\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n # canonical vocabulary value, not the exception class name\n assert result[\"error_category\"] == \"unsupported_model\"\n assert result[\"error_category\"] in rt.ERROR_CATEGORIES\n # health surfaces the canonical category too\n assert rt.get_reasoning_health()[\"error_category\"] == \"unsupported_model\"\n\n\n# ── Item 2: /models pre-check (advisory, fail-soft, short cache) ───────────────\n\ndef _fake_models_client(ids):\n client = MagicMock()\n listing = MagicMock()\n listing.data = [MagicMock(id=i) for i in ids]\n client.models.list.return_value = listing\n return client\n\n\nclass TestModelsPrecheck:\n def test_probe_populates_available_set(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client(\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is True\n assert rt.is_model_supported(\"some-retired-id\") is False\n assert rt.get_models_status()[\"reachable\"] is True\n assert rt.get_models_status()[\"available_count\"] == 2\n\n def test_advisory_skip_drops_absent_but_never_empties(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([\"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n # v4-flash absent → dropped; v4-pro kept\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == [\"deepseek-v4-pro\"]\n # if ALL are absent, the chain is returned untouched (never refuse to serve)\n assert rt._advisory_chain([\"x-only\", \"y-only\"]) == [\"x-only\", \"y-only\"]\n\n def test_failsoft_on_probe_error_serves_chain_untouched(self, monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.models.list.side_effect = _FakeErr(\"boom\", status_code=None)\n with patch.object(rt, \"_build_client\", return_value=client):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt._MODELS_STATUS[\"reachable\"] is False\n assert rt._MODELS_STATUS[\"available_models\"] is None\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None # unknown, not False\n assert rt._advisory_chain([\"deepseek-v4-flash\"]) == [\"deepseek-v4-flash\"]\n\n def test_empty_data_array_carries_on_untouched(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n # empty list must NOT be read as \"every model unsupported\"\n assert rt._MODELS_STATUS[\"available_models\"] is None\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == \\\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_not_configured_when_key_absent(self, monkeypatch):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.delenv(\"DEEPSEEK_API_KEY\", raising=False)\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt._MODELS_STATUS[\"reachable\"] is False\n assert rt._MODELS_STATUS[\"error_category\"] == \"not_configured\"\n\n def test_advisory_verdict_expires_after_short_window(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([\"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is False # fresh → advisory active\n # age the probe past the advisory window → verdict expires to \"unknown\"\n rt._MODELS_STATUS[\"_checked_monotonic\"] = time.monotonic() - (rt._MODELS_ADVISORY_WINDOW_SECONDS + 1)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == \\\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_chat_path_never_probes_models(self, monkeypatch):\n # A user chat turn must NEVER trigger the /models discovery round-trip.\n _deepseek_env(monkeypatch)\n client = _fake_models_client([\"deepseek-v4-flash\"])\n client.chat.completions.create.return_value = _chat_response(\"hello\")\n with patch.object(rt, \"_build_client\", return_value=client):\n rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n client.models.list.assert_not_called()\n\n\ndef _chat_response(text):\n resp = MagicMock()\n msg = MagicMock()\n msg.content = text\n choice = MagicMock()\n choice.message = msg\n resp.choices = [choice]\n return resp\n\n\n# ── Item 3: /ready endpoint ───────────────────────────────────────────────────\n\n@pytest.fixture(scope=\"module\")\ndef client():\n from fastapi.testclient import TestClient\n from src.api.app import app\n return TestClient(app, raise_server_exceptions=False)\n\n\nclass TestReadyEndpoint:\n def test_ready_200_when_configured_and_models_unknown(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n r = client.get(\"/ready\")\n assert r.status_code == 200\n assert r.json()[\"ready\"] is True\n\n def test_ready_503_when_not_configured(self, client, monkeypatch):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.delenv(\"DEEPSEEK_API_KEY\", raising=False)\n r = client.get(\"/ready\")\n assert r.status_code == 503\n body = r.json()\n assert body[\"ready\"] is False\n assert \"not_configured\" in body[\"reasons\"]\n\n def test_ready_503_when_no_chain_model_is_valid(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n # a FRESH successful probe that lists none of the resolved chain\n rt._MODELS_STATUS.update({\n \"provider\": \"deepseek\", \"reachable\": True,\n \"available_models\": frozenset({\"totally-different-model\"}),\n \"error_category\": None, \"checked_at\": rt._now_iso(),\n \"_checked_monotonic\": time.monotonic(),\n })\n r = client.get(\"/ready\")\n assert r.status_code == 503\n assert \"no_supported_model\" in r.json()[\"reasons\"]\n\n def test_ready_is_cache_only_no_upstream_probe(self, client, monkeypatch):\n # An inbound /ready must never trigger an outbound (billable) provider call.\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"refresh_model_availability\") as probe:\n client.get(\"/ready\")\n probe.assert_not_called()\n\n def test_health_stays_200_even_when_degraded(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n status_code=400, model_attempted=\"deepseek-v4-flash\",\n )\n r = client.get(\"/health\")\n assert r.status_code == 200 # never restart-loops Render's liveness probe\n assert r.json()[\"status\"] == \"degraded\"\n\n\n# ── Item 4: no-silent-None guard on the HuggingFace cascade ───────────────────\n\nclass TestHuggingFaceCascadeGuard:\n def _agent(self):\n from src.rico_openai_agent import RicoOpenAIAgent\n return RicoOpenAIAgent()\n\n def test_hf_returns_none_records_engaged_but_failed_and_logs(self, monkeypatch, caplog):\n _deepseek_env(monkeypatch, hf=True)\n agent = self._agent()\n # premium (deepseek) fails; HF backup returns None (silent failure)\n fail = {\"success\": False, \"text\": rt._FALLBACK_TEXT, \"error\": \"x\",\n \"error_code\": \"reasoning_provider_unavailable\"}\n with (\n patch(\"src.rico_openai_agent.call_openai_minimal\", return_value=fail),\n patch.object(agent, \"_call_hf_free\", return_value=None),\n caplog.at_level(logging.ERROR),\n ):\n agent.respond(\"give me one CV tip\")\n h = rt.get_reasoning_health()\n assert h[\"fallback_engaged\"] is True\n assert h[\"fallback_succeeded\"] is False\n assert any(\"reasoning_fallback_empty\" in rec.message for rec in caplog.records)\n\n def test_hf_success_records_engaged_and_succeeded(self, monkeypatch):\n _deepseek_env(monkeypatch, hf=True)\n agent = self._agent()\n fail = {\"success\": False, \"text\": rt._FALLBACK_TEXT}\n good = {\"type\": \"hf_response\", \"message\": \"Use metric numbers.\", \"provider\": \"huggingface\",\n \"model\": \"HuggingFaceH4/zephyr-7b-beta\"}\n with (\n patch(\"src.rico_openai_agent.call_openai_minimal\", return_value=fail),\n patch.object(agent, \"_call_hf_free\", return_value=good),\n ):\n out = agent.respond(\"give me one CV tip\")\n assert out[\"provider\"] == \"huggingface\"\n h = rt.get_reasoning_health()\n assert h[\"fallback_engaged\"] is True\n assert h[\"fallback_succeeded\"] is True\n\n def test_guard_does_not_run_inside_call_openai_minimal(self, monkeypatch):\n # call_openai_minimal must NOT touch the HF fallback bookkeeping.\n _deepseek_env(monkeypatch, hf=True)\n fake_client = MagicMock()\n fake_client.chat.completions.create.side_effect = _FakeErr(\"boom\", status_code=500)\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert rt._LAST_REASONING_OUTCOME[\"fallback_engaged\"] is None\n\n\n# ── Item 5: complete health fields + per-attempt elapsed ──────────────────────\n\nclass TestHealthFieldsComplete:\n def test_health_has_all_required_fields(self, monkeypatch):\n _deepseek_env(monkeypatch, hf=True)\n h = rt.get_reasoning_health()\n for key in (\"provider\", \"configured\", \"model_chain\", \"fallback_exists\",\n \"reachable\", \"last_checked_at\", \"last_success_at\", \"last_status\",\n \"error_category\", \"attempts\", \"last_elapsed_ms\", \"models_precheck\",\n \"last_stream_frames\"):\n assert key in h, f\"missing health field: {key}\"\n assert h[\"model_chain\"] == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n assert h[\"fallback_exists\"] is True\n\n def test_no_secret_in_health_output(self, monkeypatch):\n import json\n _deepseek_env(monkeypatch)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"authentication\",\n status_code=401, message=\"Bad key sk-fake-deepseek-key-for-tests in body\",\n model_attempted=\"deepseek-v4-flash\",\n )\n blob = json.dumps(rt.get_reasoning_health())\n assert \"sk-fake-deepseek-key-for-tests\" not in blob\n assert \"sk-***REDACTED***\" in blob or \"REDACTED\" in blob\n\n # G1 + G2 + G5: model attempted, per-attempt duration, model that served.\n def test_gate1_model_identifier_attempted(self, monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = _chat_response(\"ok\")\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert result[\"attempts\"][0][\"model\"] == \"deepseek-v4-flash\"\n assert rt.get_reasoning_health()[\"last_model_attempted\"] == \"deepseek-v4-flash\"\n\n def test_gate2_duration_of_each_attempt(self, monkeypatch):\n _deepseek_env(monkeypatch)\n calls = []\n\n def create(model, **kw):\n calls.append(model)\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"Model not found\", status_code=400, code=\"model_not_found\")\n return _chat_response(\"served\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n attempts = result[\"attempts\"]\n assert len(attempts) == 2\n for a in attempts:\n assert isinstance(a[\"elapsed_ms\"], int) and a[\"elapsed_ms\"] >= 0\n # failed attempt carries a canonical category, success carries None\n assert attempts[0][\"category\"] == \"unsupported_model\"\n assert attempts[0][\"ok\"] is False\n assert attempts[1][\"ok\"] is True\n\n def test_gate5_which_model_served(self, monkeypatch):\n _deepseek_env(monkeypatch)\n\n def create(model, **kw):\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"nope\", status_code=400, code=\"model_not_found\")\n return _chat_response(\"from pro\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert result[\"model\"] == \"deepseek-v4-pro\" # the model that actually served\n assert result[\"text\"] == \"from pro\"\n\n\n# ── Merge gates 4, 6, 7, 8 ────────────────────────────────────────────────────\n\nclass TestMergeGates:\n def test_gate4_fallback_engaged_when_primary_timed_out(self, monkeypatch):\n # Non-stream: a primary TIMEOUT must trigger the hop to the fallback model.\n _deepseek_env(monkeypatch)\n seen = []\n\n def create(model, **kw):\n seen.append(model)\n if model == \"deepseek-v4-flash\":\n raise TimeoutError(\"read timed out\")\n return _chat_response(\"recovered\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"heavy\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert seen == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n assert result[\"attempts\"][0][\"category\"] == \"timeout\"\n\n def test_gate6_heavy_prompt_tries_fallback_before_unavailable(self, monkeypatch):\n # A heavy prompt whose primary fails must NOT return the unavailable text\n # until the fallback has actually been attempted.\n _deepseek_env(monkeypatch)\n\n def create(model, **kw):\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"overloaded\", status_code=503)\n return _chat_response(\"A thorough three-part answer.\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"write me three cover letters\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert result[\"text\"] != rt._FALLBACK_TEXT\n assert [a[\"model\"] for a in result[\"attempts\"]] == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_gate7_health_degraded_not_ok(self, monkeypatch):\n _deepseek_env(monkeypatch) # no HF fallback\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n status_code=400, model_attempted=\"deepseek-v4-flash\",\n )\n assert rt.get_reasoning_health()[\"degraded\"] is True\n\n def test_gate7_failed_fallback_still_degraded_even_with_hf_configured(self, monkeypatch):\n # A backup that fails invisibly must still ring the alarm.\n _deepseek_env(monkeypatch, hf=True)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n model_attempted=\"deepseek-v4-flash\",\n )\n rt.record_fallback_outcome(engaged=True, succeeded=False,\n error_category=\"response_contract\")\n assert rt.get_reasoning_health()[\"degraded\"] is True\n\n def test_gate8_no_secret_in_log_or_health(self, monkeypatch, caplog):\n # Two distinct guarantees:\n # (a) an API key that leaks into an upstream error string is REDACTED in\n # both the log and the health output;\n # (b) the user's own prompt text and profile content are NEVER echoed\n # into a log or the health output (Rico never routes user content\n # into either).\n _deepseek_env(monkeypatch)\n secret = \"sk-live-DEEPSEEKKEY0123456789abcdef\"\n prompt_pii = \"Roben Edwan wants a compliance officer role and earns 45000 AED\"\n profile_pii = \"PASSPORT A1234567 : private banking client list\"\n fake_client = MagicMock()\n # Provider echoes the key in its error body (the exact leak we redact).\n fake_client.chat.completions.create.side_effect = _FakeErr(\n f\"401 Unauthorized: bad key {secret}\", status_code=401)\n with caplog.at_level(logging.DEBUG):\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n rt.call_openai_minimal(prompt_pii, profile_context=profile_pii, provider=\"deepseek\")\n\n import json\n health_blob = json.dumps(rt.get_reasoning_health())\n log_blob = \"\\n\".join(r.getMessage() for r in caplog.records)\n\n # (a) the key (and any partial key) never appears un-redacted.\n assert secret not in health_blob\n assert secret not in log_blob\n assert \"DEEPSEEKKEY\" not in health_blob\n assert \"DEEPSEEKKEY\" not in log_blob\n # (b) neither the user's prompt nor profile content is ever logged or surfaced.\n for pii in (prompt_pii, profile_pii, \"Roben Edwan\", \"PASSPORT A1234567\", \"45000 AED\"):\n assert pii not in health_blob, f\"user content leaked into health: {pii!r}\"\n assert pii not in log_blob, f\"user content leaked into log: {pii!r}\"\n\n\n# ── Gate 9: the stream is incremental, NOT a single buffered blob ─────────────\n\nclass _Delta:\n def __init__(self, content):\n self.content = content\n\n\nclass _Choice:\n def __init__(self, content):\n self.delta = _Delta(content)\n\n\nclass _Chunk:\n def __init__(self, content):\n self.choices = [_Choice(content)]\n\n\nclass _LazyStream:\n \"\"\"A provider stream that RECORDS the order in which chunks are pulled, so a\n test can prove the runtime yields each token as it arrives rather than\n buffering the whole answer into a single blob.\"\"\"\n\n def __init__(self, texts, produced_log):\n self._texts = texts\n self._log = produced_log\n\n def __iter__(self):\n for t in self._texts:\n self._log.append(t)\n yield _Chunk(t)\n\n\nclass TestStreamIsIncrementalNotBlob:\n def test_gate9_runtime_streams_incrementally_multiple_frames(self, monkeypatch):\n _deepseek_env(monkeypatch)\n produced: list = []\n\n def create(model, **kw):\n assert kw.get(\"stream\") is True\n return _LazyStream([\"The \", \"first \", \"part \", \"then more.\"], produced)\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n gen = rt.call_openai_stream(\"write me a long answer\", provider=\"deepseek\")\n first = next(gen) # pull ONLY the first output frame\n # Lazy incremental: only the FIRST source chunk has been produced so far —\n # a buffered-blob implementation would have pulled all four before yielding.\n assert produced == [\"The \"]\n assert first == \"The \"\n rest = list(gen) # drain the remainder\n\n frames = [first] + rest\n # MULTIPLE incremental frames (a single-frame 200 is the defect, not a pass).\n assert len(frames) == 4\n assert \"\".join(frames) == \"The first part then more.\"\n # first frame strictly precedes the last in production order\n assert produced.index(\"The \") < produced.index(\"then more.\")\n # frame-count + first-frame latency recorded in observability\n h = rt.get_reasoning_health()\n assert h[\"last_stream_frames\"] == 4\n assert isinstance(h[\"last_first_frame_ms\"], int)\n\n def test_gate9_wire_emits_multiple_token_frames_not_one_blob(self, client, monkeypatch):\n # End-to-end through the real SSE endpoint + real call_openai_stream: the\n # wire must carry MULTIPLE `data: {\"type\":\"token\"}` frames, not one blob.\n _deepseek_env(monkeypatch)\n produced: list = []\n\n def create(model, **kw):\n return _LazyStream([\"Point one. \", \"Point two. \", \"Point three.\"], produced)\n\n stream_client = MagicMock()\n stream_client.chat.completions.create.side_effect = create\n\n from src.services.chat_service import ChatPreflight\n from src.services.subscription_gating import GateCheck\n gate = GateCheck(allowed=True, feature=\"monthly_ai_message_limit\", usage=0,\n limit=300, remaining=100, plan=\"free\", message=\"ok\")\n with (\n patch(\"src.api.routers.rico_chat.get_current_user\", return_value={\"email\": \"u@example.com\"}),\n patch(\"src.services.chat_service.run_chat_preflight\",\n return_value=ChatPreflight(terminal=None, gate=gate)),\n patch(\"src.services.chat_service.should_stream_ai\", return_value=True),\n patch(\"src.repositories.profile_repo.get_profile\", return_value=None),\n patch(\"src.rico_chat_api.RicoChatAPI._build_openai_context\", return_value={}),\n patch(\"src.rico_chat_api.RicoChatAPI._append_chat\"),\n patch.object(rt, \"_build_client\", return_value=stream_client),\n ):\n res = client.post(\"/api/v1/rico/chat/stream\", json={\"message\": \"give me three points\"})\n assert res.status_code == 200 # 200, but that is NOT the proof — the frames are\n token_frames = res.text.count('\"type\": \"token\"') + res.text.count('\"type\":\"token\"')\n assert token_frames >= 3, f\"expected multiple incremental token frames, got {token_frames}\"\n assert res.text.count(rt._FALLBACK_TEXT) == 0 # never the buffered fallback blob\n\n\n# ── DeepSeek explicit thinking contract ──────────────────────────────────────\n\ndef _reasoning_chunk(*, content=None, reasoning_content=None):\n delta = MagicMock()\n delta.content = content\n delta.reasoning_content = reasoning_content\n choice = MagicMock()\n choice.delta = delta\n chunk = MagicMock()\n chunk.choices = [choice]\n return chunk\n\n\ndef test_deepseek_non_stream_explicitly_disables_thinking(monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = _chat_response(\"Final answer\")\n\n with (\n patch.object(rt, \"_build_client\", return_value=client),\n patch.object(rt, \"_deepseek_model_chain\", return_value=[\"deepseek-v4-flash\"]),\n patch.object(rt, \"_advisory_chain\", side_effect=lambda models, provider=None: models),\n ):\n result = rt.call_openai_minimal(\"hello\", provider=\"deepseek\")\n\n kwargs = client.chat.completions.create.call_args.kwargs\n assert result[\"success\"] is True\n assert result[\"text\"] == \"Final answer\"\n assert kwargs[\"extra_body\"] == {\"thinking\": {\"type\": \"disabled\"}}\n assert \"stream\" not in kwargs\n\n\ndef test_deepseek_stream_explicitly_disables_thinking_and_never_yields_reasoning(monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = iter([\n _reasoning_chunk(reasoning_content=\"private reasoning\"),\n _reasoning_chunk(content=\"Final answer\"),\n ])\n\n with (\n patch.object(rt, \"_build_client\", return_value=client),\n patch.object(rt, \"_deepseek_model_chain\", return_value=[\"deepseek-v4-flash\"]),\n patch.object(rt, \"_advisory_chain\", side_effect=lambda models, provider=None: models),\n ):\n output = list(rt.call_openai_stream(\"hello\", provider=\"deepseek\"))\n\n kwargs = client.chat.completions.create.call_args.kwargs\n assert output == [\"Final answer\"]\n assert \"private reasoning\" not in \"\".join(output)\n assert kwargs[\"extra_body\"] == {\"thinking\": {\"type\": \"disabled\"}}\n assert kwargs[\"stream\"] is True\n\n\ndef test_reasoning_only_stream_stays_fail_closed_and_falls_back_once(monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n\n def create(*, model, **kwargs):\n assert kwargs[\"extra_body\"] == {\"thinking\": {\"type\": \"disabled\"}}\n if model == \"deepseek-v4-flash\":\n return iter([_reasoning_chunk(reasoning_content=\"private reasoning\")])\n return iter([_reasoning_chunk(content=\"Fallback answer\")])\n\n client.chat.completions.create.side_effect = create\n\n with (\n patch.object(rt, \"_build_client\", return_value=client),\n patch.object(\n rt,\n \"_deepseek_model_chain\",\n return_value=[\"deepseek-v4-flash\", \"deepseek-v4-pro\"],\n ),\n patch.object(rt, \"_advisory_chain\", side_effect=lambda models, provider=None: models),\n ):\n output = list(rt.call_openai_stream(\"hello\", provider=\"deepseek\"))\n\n attempts = rt.get_reasoning_health()[\"attempts\"]\n assert output == [\"Fallback answer\"]\n assert client.chat.completions.create.call_count == 2\n assert attempts[0][\"model\"] == \"deepseek-v4-flash\"\n assert attempts[0][\"category\"] == \"response_contract\"\n assert attempts[0][\"ok\"] is False\n assert attempts[1][\"model\"] == \"deepseek-v4-pro\"\n assert attempts[1][\"ok\"] is True\n\n\ndef test_openai_responses_request_contract_is_unchanged(monkeypatch):\n monkeypatch.setenv(\"OPENAI_API_KEY\", \"sk-fake-openai-key-for-tests\")\n client = MagicMock()\n response = MagicMock()\n response.output_text = \"OpenAI answer\"\n client.responses.create.return_value = response\n\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hello\", provider=\"openai\")\n\n kwargs = client.responses.create.call_args.kwargs\n assert result[\"success\"] is True\n assert result[\"text\"] == \"OpenAI answer\"\n assert \"extra_body\" not in kwargs\n assert \"thinking\" not in kwargs\n"}, "bcc71e9305835a76d863dbf7299d80fad52fbc69": {"t": 1785773622.9600737, "n": 28857, "h": "94a63430b9655baf", "c": "\"\"\"\ntests/test_reasoning_provider_hardening.py\n\nDeterministic proof for the remaining reasoning-provider-observability items and\nthe nine merge-gate conditions. No live provider, no real DB, no credentials, no\nnetwork — every provider/stream/models call is mocked.\n\nCoverage map (items → tests):\n 1. fixed error-category vocabulary + categoriser → TestErrorCategoriser\n 2. /models pre-check (advisory, fail-soft, short cache) → TestModelsPrecheck\n 3. /ready endpoint (503 when no reachable valid model) → TestReadyEndpoint\n 4. no-silent-None guard on the HuggingFace cascade → TestHuggingFaceCascadeGuard\n 5. complete health fields + per-attempt elapsed → TestHealthFieldsComplete\n 6. deterministic tests for all of the above → (this whole file)\n\nMerge-gate map (each proven, never asserted in prose):\n G1 model identifier attempted → test_gate1_model_identifier_attempted\n G2 duration of each attempt → test_gate2_duration_of_each_attempt\n G3 failure classified (timeout/400/ → test_gate3_* (categoriser matrix)\n 429/5xx/connection)\n G4 fallback engaged on primary → test_gate4_fallback_engaged_when_primary_timed_out\n timeout\n G5 which model served the success → test_gate5_which_model_served\n G6 heavy prompt not \"unavailable\" → test_gate6_heavy_prompt_tries_fallback_before_unavailable\n before fallback tried\n G7 /health degraded not ok on loss → test_gate7_health_degraded_not_ok\n G8 no secret/PII in log or health → test_gate8_no_secret_in_log_or_health\n G9 stream emits MULTIPLE incremental → TestStreamIsIncrementalNotBlob\n frames, first before last\n\"\"\"\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport time\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nos.environ.setdefault(\"JWT_SECRET\", \"x\" * 32)\n\nimport src.rico_openai_runtime as rt\n\n\n# ── isolation ─────────────────────────────────────────────────────────────────\n\n@pytest.fixture(autouse=True)\ndef _reset_module_state():\n saved_outcome = dict(rt._LAST_REASONING_OUTCOME)\n saved_models = dict(rt._MODELS_STATUS)\n for k in rt._LAST_REASONING_OUTCOME:\n rt._LAST_REASONING_OUTCOME[k] = None\n rt._MODELS_STATUS.update({\n \"provider\": None, \"reachable\": None, \"available_models\": None,\n \"error_category\": None, \"checked_at\": None, \"_checked_monotonic\": None,\n })\n yield\n rt._LAST_REASONING_OUTCOME.update(saved_outcome)\n rt._MODELS_STATUS.update(saved_models)\n\n\nclass _FakeErr(Exception):\n def __init__(self, msg, status_code=None, code=None):\n super().__init__(msg)\n self.status_code = status_code\n self.code = code\n\n\ndef _deepseek_env(monkeypatch, *, hf=False):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.setenv(\"DEEPSEEK_API_KEY\", \"sk-fake-deepseek-key-for-tests\")\n if hf:\n monkeypatch.setenv(\"HF_TOKEN\", \"hf-fake-token-for-tests\")\n else:\n for k in (\"HF_TOKEN\", \"HF_API_TOKEN\", \"HF_API_KEY\", \"HUGGINGFACE_API_KEY\"):\n monkeypatch.delenv(k, raising=False)\n\n\n# ── Item 1: fixed error-category vocabulary + categoriser ─────────────────────\n\nclass TestErrorCategoriser:\n def test_vocabulary_is_the_exact_fixed_set(self):\n assert rt.ERROR_CATEGORIES == (\n \"not_configured\", \"invalid_configuration\", \"unsupported_model\",\n \"authentication\", \"insufficient_balance\", \"rate_limited\", \"timeout\",\n \"connection\", \"provider_4xx\", \"provider_5xx\", \"response_contract\", \"unknown\",\n )\n\n def test_every_result_is_in_the_vocabulary(self):\n samples = [\n dict(status_code=429), dict(status_code=401), dict(status_code=403),\n dict(status_code=402), dict(status_code=400), dict(status_code=404),\n dict(status_code=418), dict(status_code=500), dict(status_code=503),\n dict(error_type=\"ReadTimeout\"), dict(error_type=\"APIConnectionError\"),\n dict(error_type=\"EmptyStream\"), dict(key_present=False), dict(),\n ]\n for s in samples:\n assert rt.categorize_error(**s) in rt.ERROR_CATEGORIES\n\n # G3: the failure is classified into distinct, correct buckets.\n def test_gate3_timeout(self):\n assert rt.categorize_error(error_type=\"ReadTimeout\", message=\"read timed out\") == \"timeout\"\n assert rt.categorize_error(error_type=\"TimeoutError\") == \"timeout\"\n\n def test_gate3_connection(self):\n assert rt.categorize_error(error_type=\"APIConnectionError\") == \"connection\"\n assert rt.categorize_error(message=\"Connection refused\") == \"connection\"\n\n def test_gate3_400_unsupported_vs_invalid(self):\n # 400 that names a retired/unknown model → unsupported_model (deterministic-ish)\n assert rt.categorize_error(status_code=400, provider_error_code=\"model_not_found\") == \"unsupported_model\"\n assert rt.categorize_error(status_code=400, message=\"Model deepseek-chat does not exist\") == \"unsupported_model\"\n # 400 that is a plain malformed request → invalid_configuration\n assert rt.categorize_error(status_code=400, message=\"invalid parameter foo\") == \"invalid_configuration\"\n\n def test_gate3_429_rate_limited(self):\n assert rt.categorize_error(status_code=429) == \"rate_limited\"\n\n def test_gate3_5xx(self):\n assert rt.categorize_error(status_code=500) == \"provider_5xx\"\n assert rt.categorize_error(status_code=503) == \"provider_5xx\"\n\n def test_authentication_and_balance_and_notconfigured(self):\n assert rt.categorize_error(status_code=401) == \"authentication\"\n assert rt.categorize_error(status_code=403) == \"authentication\"\n assert rt.categorize_error(status_code=402) == \"insufficient_balance\"\n assert rt.categorize_error(message=\"Insufficient Balance\") == \"insufficient_balance\"\n assert rt.categorize_error(key_present=False) == \"not_configured\"\n\n def test_response_contract_and_other_4xx_and_unknown(self):\n assert rt.categorize_error(error_type=\"EmptyStream\") == \"response_contract\"\n assert rt.categorize_error(message=\"returned empty text\") == \"response_contract\"\n assert rt.categorize_error(status_code=418) == \"provider_4xx\"\n assert rt.categorize_error() == \"unknown\"\n\n def test_canonical_category_is_wired_into_failure_payload_not_raw_class(self, monkeypatch):\n _deepseek_env(monkeypatch)\n fake_client = MagicMock()\n fake_client.chat.completions.create.side_effect = _FakeErr(\n \"Model not found\", status_code=400, code=\"model_not_found\")\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n # canonical vocabulary value, not the exception class name\n assert result[\"error_category\"] == \"unsupported_model\"\n assert result[\"error_category\"] in rt.ERROR_CATEGORIES\n # health surfaces the canonical category too\n assert rt.get_reasoning_health()[\"error_category\"] == \"unsupported_model\"\n\n\n# ── Item 2: /models pre-check (advisory, fail-soft, short cache) ───────────────\n\ndef _fake_models_client(ids):\n client = MagicMock()\n listing = MagicMock()\n listing.data = [MagicMock(id=i) for i in ids]\n client.models.list.return_value = listing\n return client\n\n\nclass TestModelsPrecheck:\n def test_probe_populates_available_set(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client(\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is True\n assert rt.is_model_supported(\"some-retired-id\") is False\n assert rt.get_models_status()[\"reachable\"] is True\n assert rt.get_models_status()[\"available_count\"] == 2\n\n def test_advisory_skip_drops_absent_but_never_empties(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([\"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n # v4-flash absent → dropped; v4-pro kept\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == [\"deepseek-v4-pro\"]\n # if ALL are absent, the chain is returned untouched (never refuse to serve)\n assert rt._advisory_chain([\"x-only\", \"y-only\"]) == [\"x-only\", \"y-only\"]\n\n def test_failsoft_on_probe_error_serves_chain_untouched(self, monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.models.list.side_effect = _FakeErr(\"boom\", status_code=None)\n with patch.object(rt, \"_build_client\", return_value=client):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt._MODELS_STATUS[\"reachable\"] is False\n assert rt._MODELS_STATUS[\"available_models\"] is None\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None # unknown, not False\n assert rt._advisory_chain([\"deepseek-v4-flash\"]) == [\"deepseek-v4-flash\"]\n\n def test_empty_data_array_carries_on_untouched(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n # empty list must NOT be read as \"every model unsupported\"\n assert rt._MODELS_STATUS[\"available_models\"] is None\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == \\\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_not_configured_when_key_absent(self, monkeypatch):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.delenv(\"DEEPSEEK_API_KEY\", raising=False)\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt._MODELS_STATUS[\"reachable\"] is False\n assert rt._MODELS_STATUS[\"error_category\"] == \"not_configured\"\n\n def test_advisory_verdict_expires_after_short_window(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([\"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is False # fresh → advisory active\n # age the probe past the advisory window → verdict expires to \"unknown\"\n rt._MODELS_STATUS[\"_checked_monotonic\"] = time.monotonic() - (rt._MODELS_ADVISORY_WINDOW_SECONDS + 1)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == \\\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_chat_path_never_probes_models(self, monkeypatch):\n # A user chat turn must NEVER trigger the /models discovery round-trip.\n _deepseek_env(monkeypatch)\n client = _fake_models_client([\"deepseek-v4-flash\"])\n client.chat.completions.create.return_value = _chat_response(\"hello\")\n with patch.object(rt, \"_build_client\", return_value=client):\n rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n client.models.list.assert_not_called()\n\n\ndef _chat_response(text):\n resp = MagicMock()\n msg = MagicMock()\n msg.content = text\n choice = MagicMock()\n choice.message = msg\n resp.choices = [choice]\n return resp\n\n\n# ── Item 3: /ready endpoint ───────────────────────────────────────────────────\n\n@pytest.fixture(scope=\"module\")\ndef client():\n from fastapi.testclient import TestClient\n from src.api.app import app\n return TestClient(app, raise_server_exceptions=False)\n\n\nclass TestReadyEndpoint:\n def test_ready_200_when_configured_and_models_unknown(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n r = client.get(\"/ready\")\n assert r.status_code == 200\n assert r.json()[\"ready\"] is True\n\n def test_ready_503_when_not_configured(self, client, monkeypatch):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.delenv(\"DEEPSEEK_API_KEY\", raising=False)\n r = client.get(\"/ready\")\n assert r.status_code == 503\n body = r.json()\n assert body[\"ready\"] is False\n assert \"not_configured\" in body[\"reasons\"]\n\n def test_ready_503_when_no_chain_model_is_valid(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n # a FRESH successful probe that lists none of the resolved chain\n rt._MODELS_STATUS.update({\n \"provider\": \"deepseek\", \"reachable\": True,\n \"available_models\": frozenset({\"totally-different-model\"}),\n \"error_category\": None, \"checked_at\": rt._now_iso(),\n \"_checked_monotonic\": time.monotonic(),\n })\n r = client.get(\"/ready\")\n assert r.status_code == 503\n assert \"no_supported_model\" in r.json()[\"reasons\"]\n\n def test_ready_is_cache_only_no_upstream_probe(self, client, monkeypatch):\n # An inbound /ready must never trigger an outbound (billable) provider call.\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"refresh_model_availability\") as probe:\n client.get(\"/ready\")\n probe.assert_not_called()\n\n def test_health_stays_200_even_when_degraded(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n status_code=400, model_attempted=\"deepseek-v4-flash\",\n )\n r = client.get(\"/health\")\n assert r.status_code == 200 # never restart-loops Render's liveness probe\n assert r.json()[\"status\"] == \"degraded\"\n\n\n# ── Item 4: no-silent-None guard on the HuggingFace cascade ───────────────────\n\nclass TestHuggingFaceCascadeGuard:\n def _agent(self):\n from src.rico_openai_agent import RicoOpenAIAgent\n return RicoOpenAIAgent()\n\n def test_hf_returns_none_records_engaged_but_failed_and_logs(self, monkeypatch, caplog):\n _deepseek_env(monkeypatch, hf=True)\n agent = self._agent()\n # premium (deepseek) fails; HF backup returns None (silent failure)\n fail = {\"success\": False, \"text\": rt._FALLBACK_TEXT, \"error\": \"x\",\n \"error_code\": \"reasoning_provider_unavailable\"}\n with (\n patch(\"src.rico_openai_agent.call_openai_minimal\", return_value=fail),\n patch.object(agent, \"_call_hf_free\", return_value=None),\n caplog.at_level(logging.ERROR),\n ):\n agent.respond(\"give me one CV tip\")\n h = rt.get_reasoning_health()\n assert h[\"fallback_engaged\"] is True\n assert h[\"fallback_succeeded\"] is False\n assert any(\"reasoning_fallback_empty\" in rec.message for rec in caplog.records)\n\n def test_hf_success_records_engaged_and_succeeded(self, monkeypatch):\n _deepseek_env(monkeypatch, hf=True)\n agent = self._agent()\n fail = {\"success\": False, \"text\": rt._FALLBACK_TEXT}\n good = {\"type\": \"hf_response\", \"message\": \"Use metric numbers.\", \"provider\": \"huggingface\",\n \"model\": \"HuggingFaceH4/zephyr-7b-beta\"}\n with (\n patch(\"src.rico_openai_agent.call_openai_minimal\", return_value=fail),\n patch.object(agent, \"_call_hf_free\", return_value=good),\n ):\n out = agent.respond(\"give me one CV tip\")\n assert out[\"provider\"] == \"huggingface\"\n h = rt.get_reasoning_health()\n assert h[\"fallback_engaged\"] is True\n assert h[\"fallback_succeeded\"] is True\n\n def test_guard_does_not_run_inside_call_openai_minimal(self, monkeypatch):\n # call_openai_minimal must NOT touch the HF fallback bookkeeping.\n _deepseek_env(monkeypatch, hf=True)\n fake_client = MagicMock()\n fake_client.chat.completions.create.side_effect = _FakeErr(\"boom\", status_code=500)\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert rt._LAST_REASONING_OUTCOME[\"fallback_engaged\"] is None\n\n\n# ── Item 5: complete health fields + per-attempt elapsed ──────────────────────\n\nclass TestHealthFieldsComplete:\n def test_health_has_all_required_fields(self, monkeypatch):\n _deepseek_env(monkeypatch, hf=True)\n h = rt.get_reasoning_health()\n for key in (\"provider\", \"configured\", \"model_chain\", \"fallback_exists\",\n \"reachable\", \"last_checked_at\", \"last_success_at\", \"last_status\",\n \"error_category\", \"attempts\", \"last_elapsed_ms\", \"models_precheck\",\n \"last_stream_frames\"):\n assert key in h, f\"missing health field: {key}\"\n assert h[\"model_chain\"] == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n assert h[\"fallback_exists\"] is True\n\n def test_no_secret_in_health_output(self, monkeypatch):\n import json\n _deepseek_env(monkeypatch)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"authentication\",\n status_code=401, message=\"Bad key sk-fake-deepseek-key-for-tests in body\",\n model_attempted=\"deepseek-v4-flash\",\n )\n blob = json.dumps(rt.get_reasoning_health())\n assert \"sk-fake-deepseek-key-for-tests\" not in blob\n assert \"sk-***REDACTED***\" in blob or \"REDACTED\" in blob\n\n # G1 + G2 + G5: model attempted, per-attempt duration, model that served.\n def test_gate1_model_identifier_attempted(self, monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = _chat_response(\"ok\")\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert result[\"attempts\"][0][\"model\"] == \"deepseek-v4-flash\"\n assert rt.get_reasoning_health()[\"last_model_attempted\"] == \"deepseek-v4-flash\"\n\n def test_gate2_duration_of_each_attempt(self, monkeypatch):\n _deepseek_env(monkeypatch)\n calls = []\n\n def create(model, **kw):\n calls.append(model)\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"Model not found\", status_code=400, code=\"model_not_found\")\n return _chat_response(\"served\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n attempts = result[\"attempts\"]\n assert len(attempts) == 2\n for a in attempts:\n assert isinstance(a[\"elapsed_ms\"], int) and a[\"elapsed_ms\"] >= 0\n # failed attempt carries a canonical category, success carries None\n assert attempts[0][\"category\"] == \"unsupported_model\"\n assert attempts[0][\"ok\"] is False\n assert attempts[1][\"ok\"] is True\n\n def test_gate5_which_model_served(self, monkeypatch):\n _deepseek_env(monkeypatch)\n\n def create(model, **kw):\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"nope\", status_code=400, code=\"model_not_found\")\n return _chat_response(\"from pro\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert result[\"model\"] == \"deepseek-v4-pro\" # the model that actually served\n assert result[\"text\"] == \"from pro\"\n\n\n# ── Merge gates 4, 6, 7, 8 ────────────────────────────────────────────────────\n\nclass TestMergeGates:\n def test_gate4_fallback_engaged_when_primary_timed_out(self, monkeypatch):\n # Non-stream: a primary TIMEOUT must trigger the hop to the fallback model.\n _deepseek_env(monkeypatch)\n seen = []\n\n def create(model, **kw):\n seen.append(model)\n if model == \"deepseek-v4-flash\":\n raise TimeoutError(\"read timed out\")\n return _chat_response(\"recovered\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"heavy\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert seen == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n assert result[\"attempts\"][0][\"category\"] == \"timeout\"\n\n def test_gate6_heavy_prompt_tries_fallback_before_unavailable(self, monkeypatch):\n # A heavy prompt whose primary fails must NOT return the unavailable text\n # until the fallback has actually been attempted.\n _deepseek_env(monkeypatch)\n\n def create(model, **kw):\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"overloaded\", status_code=503)\n return _chat_response(\"A thorough three-part answer.\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"write me three cover letters\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert result[\"text\"] != rt._FALLBACK_TEXT\n assert [a[\"model\"] for a in result[\"attempts\"]] == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_gate7_health_degraded_not_ok(self, monkeypatch):\n _deepseek_env(monkeypatch) # no HF fallback\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n status_code=400, model_attempted=\"deepseek-v4-flash\",\n )\n assert rt.get_reasoning_health()[\"degraded\"] is True\n\n def test_gate7_failed_fallback_still_degraded_even_with_hf_configured(self, monkeypatch):\n # A backup that fails invisibly must still ring the alarm.\n _deepseek_env(monkeypatch, hf=True)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n model_attempted=\"deepseek-v4-flash\",\n )\n rt.record_fallback_outcome(engaged=True, succeeded=False,\n error_category=\"response_contract\")\n assert rt.get_reasoning_health()[\"degraded\"] is True\n\n def test_gate8_no_secret_in_log_or_health(self, monkeypatch, caplog):\n # Two distinct guarantees:\n # (a) an API key that leaks into an upstream error string is REDACTED in\n # both the log and the health output;\n # (b) the user's own prompt text and profile content are NEVER echoed\n # into a log or the health output (Rico never routes user content\n # into either).\n _deepseek_env(monkeypatch)\n secret = \"sk-live-DEEPSEEKKEY0123456789abcdef\"\n prompt_pii = \"Roben Edwan wants a compliance officer role and earns 45000 AED\"\n profile_pii = \"PASSPORT A1234567 : private banking client list\"\n fake_client = MagicMock()\n # Provider echoes the key in its error body (the exact leak we redact).\n fake_client.chat.completions.create.side_effect = _FakeErr(\n f\"401 Unauthorized: bad key {secret}\", status_code=401)\n with caplog.at_level(logging.DEBUG):\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n rt.call_openai_minimal(prompt_pii, profile_context=profile_pii, provider=\"deepseek\")\n\n import json\n health_blob = json.dumps(rt.get_reasoning_health())\n log_blob = \"\\n\".join(r.getMessage() for r in caplog.records)\n\n # (a) the key (and any partial key) never appears un-redacted.\n assert secret not in health_blob\n assert secret not in log_blob\n assert \"DEEPSEEKKEY\" not in health_blob\n assert \"DEEPSEEKKEY\" not in log_blob\n # (b) neither the user's prompt nor profile content is ever logged or surfaced.\n for pii in (prompt_pii, profile_pii, \"Roben Edwan\", \"PASSPORT A1234567\", \"45000 AED\"):\n assert pii not in health_blob, f\"user content leaked into health: {pii!r}\"\n assert pii not in log_blob, f\"user content leaked into log: {pii!r}\"\n\n\n# ── Gate 9: the stream is incremental, NOT a single buffered blob ─────────────\n\nclass _Delta:\n def __init__(self, content):\n self.content = content\n\n\nclass _Choice:\n def __init__(self, content):\n self.delta = _Delta(content)\n\n\nclass _Chunk:\n def __init__(self, content):\n self.choices = [_Choice(content)]\n\n\nclass _LazyStream:\n \"\"\"A provider stream that RECORDS the order in which chunks are pulled, so a\n test can prove the runtime yields each token as it arrives rather than\n buffering the whole answer into a single blob.\"\"\"\n\n def __init__(self, texts, produced_log):\n self._texts = texts\n self._log = produced_log\n\n def __iter__(self):\n for t in self._texts:\n self._log.append(t)\n yield _Chunk(t)\n\n\nclass TestStreamIsIncrementalNotBlob:\n def test_gate9_runtime_streams_incrementally_multiple_frames(self, monkeypatch):\n _deepseek_env(monkeypatch)\n produced: list = []\n\n def create(model, **kw):\n assert kw.get(\"stream\") is True\n return _LazyStream([\"The \", \"first \", \"part \", \"then more.\"], produced)\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n gen = rt.call_openai_stream(\"write me a long answer\", provider=\"deepseek\")\n first = next(gen) # pull ONLY the first output frame\n # Lazy incremental: only the FIRST source chunk has been produced so far —\n # a buffered-blob implementation would have pulled all four before yielding.\n assert produced == [\"The \"]\n assert first == \"The \"\n rest = list(gen) # drain the remainder\n\n frames = [first] + rest\n # MULTIPLE incremental frames (a single-frame 200 is the defect, not a pass).\n assert len(frames) == 4\n assert \"\".join(frames) == \"The first part then more.\"\n # first frame strictly precedes the last in production order\n assert produced.index(\"The \") < produced.index(\"then more.\")\n # frame-count + first-frame latency recorded in observability\n h = rt.get_reasoning_health()\n assert h[\"last_stream_frames\"] == 4\n assert isinstance(h[\"last_first_frame_ms\"], int)\n\n def test_gate9_wire_emits_multiple_token_frames_not_one_blob(self, client, monkeypatch):\n # End-to-end through the real SSE endpoint + real call_openai_stream: the\n # wire must carry MULTIPLE `data: {\"type\":\"token\"}` frames, not one blob.\n _deepseek_env(monkeypatch)\n produced: list = []\n\n def create(model, **kw):\n return _LazyStream([\"Point one. \", \"Point two. \", \"Point three.\"], produced)\n\n stream_client = MagicMock()\n stream_client.chat.completions.create.side_effect = create\n\n from src.services.chat_service import ChatPreflight\n from src.services.subscription_gating import GateCheck\n gate = GateCheck(allowed=True, feature=\"monthly_ai_message_limit\", usage=0,\n limit=300, remaining=100, plan=\"free\", message=\"ok\")\n with (\n patch(\"src.api.routers.rico_chat.get_current_user\", return_value={\"email\": \"u@example.com\"}),\n patch(\"src.services.chat_service.run_chat_preflight\",\n return_value=ChatPreflight(terminal=None, gate=gate)),\n patch(\"src.services.chat_service.should_stream_ai\", return_value=True),\n patch(\"src.repositories.profile_repo.get_profile\", return_value=None),\n patch(\"src.rico_chat_api.RicoChatAPI._build_openai_context\", return_value={}),\n patch(\"src.rico_chat_api.RicoChatAPI._append_chat\"),\n patch.object(rt, \"_build_client\", return_value=stream_client),\n ):\n res = client.post(\"/api/v1/rico/chat/stream\", json={\"message\": \"give me three points\"})\n assert res.status_code == 200 # 200, but that is NOT the proof — the frames are\n token_frames = res.text.count('\"type\": \"token\"') + res.text.count('\"type\":\"token\"')\n assert token_frames >= 3, f\"expected multiple incremental token frames, got {token_frames}\"\n assert res.text.count(rt._FALLBACK_TEXT) == 0 # never the buffered fallback blob\n"}, "5098f29025a089d326af8f4c84aa94e6351e9682": {"t": 1786068623.235096, "n": 32885, "h": "5f054e2af562ca74", "c": "\"\"\"\ntests/test_reasoning_provider_hardening.py\n\nDeterministic proof for the remaining reasoning-provider-observability items and\nthe nine merge-gate conditions. No live provider, no real DB, no credentials, no\nnetwork — every provider/stream/models call is mocked.\n\nCoverage map (items → tests):\n 1. fixed error-category vocabulary + categoriser → TestErrorCategoriser\n 2. /models pre-check (advisory, fail-soft, short cache) → TestModelsPrecheck\n 3. /ready endpoint (503 when no reachable valid model) → TestReadyEndpoint\n 4. no-silent-None guard on the HuggingFace cascade → TestHuggingFaceCascadeGuard\n 5. complete health fields + per-attempt elapsed → TestHealthFieldsComplete\n 6. deterministic tests for all of the above → (this whole file)\n\nMerge-gate map (each proven, never asserted in prose):\n G1 model identifier attempted → test_gate1_model_identifier_attempted\n G2 duration of each attempt → test_gate2_duration_of_each_attempt\n G3 failure classified (timeout/400/ → test_gate3_* (categoriser matrix)\n 429/5xx/connection)\n G4 fallback engaged on primary → test_gate4_fallback_engaged_when_primary_timed_out\n timeout\n G5 which model served the success → test_gate5_which_model_served\n G6 heavy prompt not \"unavailable\" → test_gate6_heavy_prompt_tries_fallback_before_unavailable\n before fallback tried\n G7 /health degraded not ok on loss → test_gate7_health_degraded_not_ok\n G8 no secret/PII in log or health → test_gate8_no_secret_in_log_or_health\n G9 stream emits MULTIPLE incremental → TestStreamIsIncrementalNotBlob\n frames, first before last\n\"\"\"\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport time\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nos.environ.setdefault(\"JWT_SECRET\", \"x\" * 32)\n\nimport src.rico_openai_runtime as rt\n\n\n# ── isolation ─────────────────────────────────────────────────────────────────\n\n@pytest.fixture(autouse=True)\ndef _reset_module_state():\n saved_outcome = dict(rt._LAST_REASONING_OUTCOME)\n saved_models = dict(rt._MODELS_STATUS)\n for k in rt._LAST_REASONING_OUTCOME:\n rt._LAST_REASONING_OUTCOME[k] = None\n rt._MODELS_STATUS.update({\n \"provider\": None, \"reachable\": None, \"available_models\": None,\n \"error_category\": None, \"checked_at\": None, \"_checked_monotonic\": None,\n })\n yield\n rt._LAST_REASONING_OUTCOME.update(saved_outcome)\n rt._MODELS_STATUS.update(saved_models)\n\n\nclass _FakeErr(Exception):\n def __init__(self, msg, status_code=None, code=None):\n super().__init__(msg)\n self.status_code = status_code\n self.code = code\n\n\ndef _deepseek_env(monkeypatch, *, hf=False):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.setenv(\"DEEPSEEK_API_KEY\", \"sk-fake-deepseek-key-for-tests\")\n if hf:\n monkeypatch.setenv(\"HF_TOKEN\", \"hf-fake-token-for-tests\")\n else:\n for k in (\"HF_TOKEN\", \"HF_API_TOKEN\", \"HF_API_KEY\", \"HUGGINGFACE_API_KEY\"):\n monkeypatch.delenv(k, raising=False)\n\n\n# ── Item 1: fixed error-category vocabulary + categoriser ─────────────────────\n\nclass TestErrorCategoriser:\n def test_vocabulary_is_the_exact_fixed_set(self):\n assert rt.ERROR_CATEGORIES == (\n \"not_configured\", \"invalid_configuration\", \"unsupported_model\",\n \"authentication\", \"insufficient_balance\", \"rate_limited\", \"timeout\",\n \"connection\", \"provider_4xx\", \"provider_5xx\", \"response_contract\", \"unknown\",\n )\n\n def test_every_result_is_in_the_vocabulary(self):\n samples = [\n dict(status_code=429), dict(status_code=401), dict(status_code=403),\n dict(status_code=402), dict(status_code=400), dict(status_code=404),\n dict(status_code=418), dict(status_code=500), dict(status_code=503),\n dict(error_type=\"ReadTimeout\"), dict(error_type=\"APIConnectionError\"),\n dict(error_type=\"EmptyStream\"), dict(key_present=False), dict(),\n ]\n for s in samples:\n assert rt.categorize_error(**s) in rt.ERROR_CATEGORIES\n\n # G3: the failure is classified into distinct, correct buckets.\n def test_gate3_timeout(self):\n assert rt.categorize_error(error_type=\"ReadTimeout\", message=\"read timed out\") == \"timeout\"\n assert rt.categorize_error(error_type=\"TimeoutError\") == \"timeout\"\n\n def test_gate3_connection(self):\n assert rt.categorize_error(error_type=\"APIConnectionError\") == \"connection\"\n assert rt.categorize_error(message=\"Connection refused\") == \"connection\"\n\n def test_gate3_400_unsupported_vs_invalid(self):\n # 400 that names a retired/unknown model → unsupported_model (deterministic-ish)\n assert rt.categorize_error(status_code=400, provider_error_code=\"model_not_found\") == \"unsupported_model\"\n assert rt.categorize_error(status_code=400, message=\"Model deepseek-chat does not exist\") == \"unsupported_model\"\n # 400 that is a plain malformed request → invalid_configuration\n assert rt.categorize_error(status_code=400, message=\"invalid parameter foo\") == \"invalid_configuration\"\n\n def test_gate3_429_rate_limited(self):\n assert rt.categorize_error(status_code=429) == \"rate_limited\"\n\n def test_gate3_5xx(self):\n assert rt.categorize_error(status_code=500) == \"provider_5xx\"\n assert rt.categorize_error(status_code=503) == \"provider_5xx\"\n\n def test_authentication_and_balance_and_notconfigured(self):\n assert rt.categorize_error(status_code=401) == \"authentication\"\n assert rt.categorize_error(status_code=403) == \"authentication\"\n assert rt.categorize_error(status_code=402) == \"insufficient_balance\"\n assert rt.categorize_error(message=\"Insufficient Balance\") == \"insufficient_balance\"\n assert rt.categorize_error(key_present=False) == \"not_configured\"\n\n def test_response_contract_and_other_4xx_and_unknown(self):\n assert rt.categorize_error(error_type=\"EmptyStream\") == \"response_contract\"\n assert rt.categorize_error(message=\"returned empty text\") == \"response_contract\"\n assert rt.categorize_error(status_code=418) == \"provider_4xx\"\n assert rt.categorize_error() == \"unknown\"\n\n def test_canonical_category_is_wired_into_failure_payload_not_raw_class(self, monkeypatch):\n _deepseek_env(monkeypatch)\n fake_client = MagicMock()\n fake_client.chat.completions.create.side_effect = _FakeErr(\n \"Model not found\", status_code=400, code=\"model_not_found\")\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n # canonical vocabulary value, not the exception class name\n assert result[\"error_category\"] == \"unsupported_model\"\n assert result[\"error_category\"] in rt.ERROR_CATEGORIES\n # health surfaces the canonical category too\n assert rt.get_reasoning_health()[\"error_category\"] == \"unsupported_model\"\n\n\n# ── Item 2: /models pre-check (advisory, fail-soft, short cache) ───────────────\n\ndef _fake_models_client(ids):\n client = MagicMock()\n listing = MagicMock()\n listing.data = [MagicMock(id=i) for i in ids]\n client.models.list.return_value = listing\n return client\n\n\nclass TestModelsPrecheck:\n def test_probe_populates_available_set(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client(\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is True\n assert rt.is_model_supported(\"some-retired-id\") is False\n assert rt.get_models_status()[\"reachable\"] is True\n assert rt.get_models_status()[\"available_count\"] == 2\n\n def test_advisory_skip_drops_absent_but_never_empties(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([\"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n # v4-flash absent → dropped; v4-pro kept\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == [\"deepseek-v4-pro\"]\n # if ALL are absent, the chain is returned untouched (never refuse to serve)\n assert rt._advisory_chain([\"x-only\", \"y-only\"]) == [\"x-only\", \"y-only\"]\n\n def test_failsoft_on_probe_error_serves_chain_untouched(self, monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.models.list.side_effect = _FakeErr(\"boom\", status_code=None)\n with patch.object(rt, \"_build_client\", return_value=client):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt._MODELS_STATUS[\"reachable\"] is False\n assert rt._MODELS_STATUS[\"available_models\"] is None\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None # unknown, not False\n assert rt._advisory_chain([\"deepseek-v4-flash\"]) == [\"deepseek-v4-flash\"]\n\n def test_empty_data_array_carries_on_untouched(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n # empty list must NOT be read as \"every model unsupported\"\n assert rt._MODELS_STATUS[\"available_models\"] is None\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == \\\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_not_configured_when_key_absent(self, monkeypatch):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.delenv(\"DEEPSEEK_API_KEY\", raising=False)\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt._MODELS_STATUS[\"reachable\"] is False\n assert rt._MODELS_STATUS[\"error_category\"] == \"not_configured\"\n\n def test_advisory_verdict_expires_after_short_window(self, monkeypatch):\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"_build_client\", return_value=_fake_models_client([\"deepseek-v4-pro\"])):\n rt.refresh_model_availability(\"deepseek\", force=True)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is False # fresh → advisory active\n # age the probe past the advisory window → verdict expires to \"unknown\"\n rt._MODELS_STATUS[\"_checked_monotonic\"] = time.monotonic() - (rt._MODELS_ADVISORY_WINDOW_SECONDS + 1)\n assert rt.is_model_supported(\"deepseek-v4-flash\") is None\n assert rt._advisory_chain([\"deepseek-v4-flash\", \"deepseek-v4-pro\"]) == \\\n [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_chat_path_never_probes_models(self, monkeypatch):\n # A user chat turn must NEVER trigger the /models discovery round-trip.\n _deepseek_env(monkeypatch)\n client = _fake_models_client([\"deepseek-v4-flash\"])\n client.chat.completions.create.return_value = _chat_response(\"hello\")\n with patch.object(rt, \"_build_client\", return_value=client):\n rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n client.models.list.assert_not_called()\n\n\ndef _chat_response(text):\n resp = MagicMock()\n msg = MagicMock()\n msg.content = text\n choice = MagicMock()\n choice.message = msg\n resp.choices = [choice]\n return resp\n\n\n# ── Item 3: /ready endpoint ───────────────────────────────────────────────────\n\n@pytest.fixture(scope=\"module\")\ndef client():\n from fastapi.testclient import TestClient\n from src.api.app import app\n return TestClient(app, raise_server_exceptions=False)\n\n\nclass TestReadyEndpoint:\n def test_ready_200_when_configured_and_models_unknown(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n r = client.get(\"/ready\")\n assert r.status_code == 200\n assert r.json()[\"ready\"] is True\n\n def test_ready_503_when_not_configured(self, client, monkeypatch):\n monkeypatch.setenv(\"RICO_AI_PROVIDER\", \"deepseek\")\n monkeypatch.delenv(\"DEEPSEEK_API_KEY\", raising=False)\n r = client.get(\"/ready\")\n assert r.status_code == 503\n body = r.json()\n assert body[\"ready\"] is False\n assert \"not_configured\" in body[\"reasons\"]\n\n def test_ready_503_when_no_chain_model_is_valid(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n # a FRESH successful probe that lists none of the resolved chain\n rt._MODELS_STATUS.update({\n \"provider\": \"deepseek\", \"reachable\": True,\n \"available_models\": frozenset({\"totally-different-model\"}),\n \"error_category\": None, \"checked_at\": rt._now_iso(),\n \"_checked_monotonic\": time.monotonic(),\n })\n r = client.get(\"/ready\")\n assert r.status_code == 503\n assert \"no_supported_model\" in r.json()[\"reasons\"]\n\n def test_ready_is_cache_only_no_upstream_probe(self, client, monkeypatch):\n # An inbound /ready must never trigger an outbound (billable) provider call.\n _deepseek_env(monkeypatch)\n with patch.object(rt, \"refresh_model_availability\") as probe:\n client.get(\"/ready\")\n probe.assert_not_called()\n\n def test_health_stays_200_even_when_degraded(self, client, monkeypatch):\n _deepseek_env(monkeypatch)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n status_code=400, model_attempted=\"deepseek-v4-flash\",\n )\n r = client.get(\"/health\")\n assert r.status_code == 200 # never restart-loops Render's liveness probe\n assert r.json()[\"status\"] == \"degraded\"\n\n\n# ── Item 4: no-silent-None guard on the HuggingFace cascade ───────────────────\n\nclass TestHuggingFaceCascadeGuard:\n def _agent(self):\n from src.rico_openai_agent import RicoOpenAIAgent\n return RicoOpenAIAgent()\n\n def test_hf_returns_none_records_engaged_but_failed_and_logs(self, monkeypatch, caplog):\n _deepseek_env(monkeypatch, hf=True)\n agent = self._agent()\n # premium (deepseek) fails; HF backup returns None (silent failure)\n fail = {\"success\": False, \"text\": rt._FALLBACK_TEXT, \"error\": \"x\",\n \"error_code\": \"reasoning_provider_unavailable\"}\n with (\n patch(\"src.rico_openai_agent.call_openai_minimal\", return_value=fail),\n patch.object(agent, \"_call_hf_free\", return_value=None),\n caplog.at_level(logging.ERROR),\n ):\n agent.respond(\"give me one CV tip\")\n h = rt.get_reasoning_health()\n assert h[\"fallback_engaged\"] is True\n assert h[\"fallback_succeeded\"] is False\n assert any(\"reasoning_fallback_empty\" in rec.message for rec in caplog.records)\n\n def test_hf_success_records_engaged_and_succeeded(self, monkeypatch):\n _deepseek_env(monkeypatch, hf=True)\n agent = self._agent()\n fail = {\"success\": False, \"text\": rt._FALLBACK_TEXT}\n good = {\"type\": \"hf_response\", \"message\": \"Use metric numbers.\", \"provider\": \"huggingface\",\n \"model\": \"HuggingFaceH4/zephyr-7b-beta\"}\n with (\n patch(\"src.rico_openai_agent.call_openai_minimal\", return_value=fail),\n patch.object(agent, \"_call_hf_free\", return_value=good),\n ):\n out = agent.respond(\"give me one CV tip\")\n assert out[\"provider\"] == \"huggingface\"\n h = rt.get_reasoning_health()\n assert h[\"fallback_engaged\"] is True\n assert h[\"fallback_succeeded\"] is True\n\n def test_guard_does_not_run_inside_call_openai_minimal(self, monkeypatch):\n # call_openai_minimal must NOT touch the HF fallback bookkeeping.\n _deepseek_env(monkeypatch, hf=True)\n fake_client = MagicMock()\n fake_client.chat.completions.create.side_effect = _FakeErr(\"boom\", status_code=500)\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert rt._LAST_REASONING_OUTCOME[\"fallback_engaged\"] is None\n\n\n# ── Item 5: complete health fields + per-attempt elapsed ──────────────────────\n\nclass TestHealthFieldsComplete:\n def test_health_has_all_required_fields(self, monkeypatch):\n _deepseek_env(monkeypatch, hf=True)\n h = rt.get_reasoning_health()\n for key in (\"provider\", \"configured\", \"model_chain\", \"fallback_exists\",\n \"reachable\", \"last_checked_at\", \"last_success_at\", \"last_status\",\n \"error_category\", \"attempts\", \"last_elapsed_ms\", \"models_precheck\",\n \"last_stream_frames\"):\n assert key in h, f\"missing health field: {key}\"\n assert h[\"model_chain\"] == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n assert h[\"fallback_exists\"] is True\n\n def test_no_secret_in_health_output(self, monkeypatch):\n import json\n _deepseek_env(monkeypatch)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"authentication\",\n status_code=401, message=\"Bad key sk-fake-deepseek-key-for-tests in body\",\n model_attempted=\"deepseek-v4-flash\",\n )\n blob = json.dumps(rt.get_reasoning_health())\n assert \"sk-fake-deepseek-key-for-tests\" not in blob\n assert \"sk-***REDACTED***\" in blob or \"REDACTED\" in blob\n\n # G1 + G2 + G5: model attempted, per-attempt duration, model that served.\n def test_gate1_model_identifier_attempted(self, monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = _chat_response(\"ok\")\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert result[\"attempts\"][0][\"model\"] == \"deepseek-v4-flash\"\n assert rt.get_reasoning_health()[\"last_model_attempted\"] == \"deepseek-v4-flash\"\n\n def test_gate2_duration_of_each_attempt(self, monkeypatch):\n _deepseek_env(monkeypatch)\n calls = []\n\n def create(model, **kw):\n calls.append(model)\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"Model not found\", status_code=400, code=\"model_not_found\")\n return _chat_response(\"served\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n attempts = result[\"attempts\"]\n assert len(attempts) == 2\n for a in attempts:\n assert isinstance(a[\"elapsed_ms\"], int) and a[\"elapsed_ms\"] >= 0\n # failed attempt carries a canonical category, success carries None\n assert attempts[0][\"category\"] == \"unsupported_model\"\n assert attempts[0][\"ok\"] is False\n assert attempts[1][\"ok\"] is True\n\n def test_gate5_which_model_served(self, monkeypatch):\n _deepseek_env(monkeypatch)\n\n def create(model, **kw):\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"nope\", status_code=400, code=\"model_not_found\")\n return _chat_response(\"from pro\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hi\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert result[\"model\"] == \"deepseek-v4-pro\" # the model that actually served\n assert result[\"text\"] == \"from pro\"\n\n\n# ── Merge gates 4, 6, 7, 8 ────────────────────────────────────────────────────\n\nclass TestMergeGates:\n def test_gate4_fallback_engaged_when_primary_timed_out(self, monkeypatch):\n # Non-stream: a primary TIMEOUT must trigger the hop to the fallback model.\n _deepseek_env(monkeypatch)\n seen = []\n\n def create(model, **kw):\n seen.append(model)\n if model == \"deepseek-v4-flash\":\n raise TimeoutError(\"read timed out\")\n return _chat_response(\"recovered\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"heavy\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert seen == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n assert result[\"attempts\"][0][\"category\"] == \"timeout\"\n\n def test_gate6_heavy_prompt_tries_fallback_before_unavailable(self, monkeypatch):\n # A heavy prompt whose primary fails must NOT return the unavailable text\n # until the fallback has actually been attempted.\n _deepseek_env(monkeypatch)\n\n def create(model, **kw):\n if model == \"deepseek-v4-flash\":\n raise _FakeErr(\"overloaded\", status_code=503)\n return _chat_response(\"A thorough three-part answer.\")\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"write me three cover letters\", provider=\"deepseek\")\n assert result[\"success\"] is True\n assert result[\"text\"] != rt._FALLBACK_TEXT\n assert [a[\"model\"] for a in result[\"attempts\"]] == [\"deepseek-v4-flash\", \"deepseek-v4-pro\"]\n\n def test_gate7_health_degraded_not_ok(self, monkeypatch):\n _deepseek_env(monkeypatch) # no HF fallback\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n status_code=400, model_attempted=\"deepseek-v4-flash\",\n )\n assert rt.get_reasoning_health()[\"degraded\"] is True\n\n def test_gate7_failed_fallback_still_degraded_even_with_hf_configured(self, monkeypatch):\n # A backup that fails invisibly must still ring the alarm.\n _deepseek_env(monkeypatch, hf=True)\n rt._record_reasoning_outcome(\n \"deepseek\", reachable=False, error_category=\"unsupported_model\",\n model_attempted=\"deepseek-v4-flash\",\n )\n rt.record_fallback_outcome(engaged=True, succeeded=False,\n error_category=\"response_contract\")\n assert rt.get_reasoning_health()[\"degraded\"] is True\n\n def test_gate8_no_secret_in_log_or_health(self, monkeypatch, caplog):\n # Two distinct guarantees:\n # (a) an API key that leaks into an upstream error string is REDACTED in\n # both the log and the health output;\n # (b) the user's own prompt text and profile content are NEVER echoed\n # into a log or the health output (Rico never routes user content\n # into either).\n _deepseek_env(monkeypatch)\n secret = \"sk-live-DEEPSEEKKEY0123456789abcdef\"\n prompt_pii = \"Roben Edwan wants a compliance officer role and earns 45000 AED\"\n profile_pii = \"PASSPORT A1234567 : private banking client list\"\n fake_client = MagicMock()\n # Provider echoes the key in its error body (the exact leak we redact).\n fake_client.chat.completions.create.side_effect = _FakeErr(\n f\"401 Unauthorized: bad key {secret}\", status_code=401)\n with caplog.at_level(logging.DEBUG):\n with patch.object(rt, \"_build_client\", return_value=fake_client):\n rt.call_openai_minimal(prompt_pii, profile_context=profile_pii, provider=\"deepseek\")\n\n import json\n health_blob = json.dumps(rt.get_reasoning_health())\n log_blob = \"\\n\".join(r.getMessage() for r in caplog.records)\n\n # (a) the key (and any partial key) never appears un-redacted.\n assert secret not in health_blob\n assert secret not in log_blob\n assert \"DEEPSEEKKEY\" not in health_blob\n assert \"DEEPSEEKKEY\" not in log_blob\n # (b) neither the user's prompt nor profile content is ever logged or surfaced.\n for pii in (prompt_pii, profile_pii, \"Roben Edwan\", \"PASSPORT A1234567\", \"45000 AED\"):\n assert pii not in health_blob, f\"user content leaked into health: {pii!r}\"\n assert pii not in log_blob, f\"user content leaked into log: {pii!r}\"\n\n\n# ── Gate 9: the stream is incremental, NOT a single buffered blob ─────────────\n\nclass _Delta:\n def __init__(self, content):\n self.content = content\n\n\nclass _Choice:\n def __init__(self, content):\n self.delta = _Delta(content)\n\n\nclass _Chunk:\n def __init__(self, content):\n self.choices = [_Choice(content)]\n\n\nclass _LazyStream:\n \"\"\"A provider stream that RECORDS the order in which chunks are pulled, so a\n test can prove the runtime yields each token as it arrives rather than\n buffering the whole answer into a single blob.\"\"\"\n\n def __init__(self, texts, produced_log):\n self._texts = texts\n self._log = produced_log\n\n def __iter__(self):\n for t in self._texts:\n self._log.append(t)\n yield _Chunk(t)\n\n\nclass TestStreamIsIncrementalNotBlob:\n def test_gate9_runtime_streams_incrementally_multiple_frames(self, monkeypatch):\n _deepseek_env(monkeypatch)\n produced: list = []\n\n def create(model, **kw):\n assert kw.get(\"stream\") is True\n return _LazyStream([\"The \", \"first \", \"part \", \"then more.\"], produced)\n\n client = MagicMock()\n client.chat.completions.create.side_effect = create\n with patch.object(rt, \"_build_client\", return_value=client):\n gen = rt.call_openai_stream(\"write me a long answer\", provider=\"deepseek\")\n first = next(gen) # pull ONLY the first output frame\n # Lazy incremental: only the FIRST source chunk has been produced so far —\n # a buffered-blob implementation would have pulled all four before yielding.\n assert produced == [\"The \"]\n assert first == \"The \"\n rest = list(gen) # drain the remainder\n\n frames = [first] + rest\n # MULTIPLE incremental frames (a single-frame 200 is the defect, not a pass).\n assert len(frames) == 4\n assert \"\".join(frames) == \"The first part then more.\"\n # first frame strictly precedes the last in production order\n assert produced.index(\"The \") < produced.index(\"then more.\")\n # frame-count + first-frame latency recorded in observability\n h = rt.get_reasoning_health()\n assert h[\"last_stream_frames\"] == 4\n assert isinstance(h[\"last_first_frame_ms\"], int)\n\n def test_gate9_wire_emits_multiple_token_frames_not_one_blob(self, client, monkeypatch):\n # End-to-end through the real SSE endpoint + real call_openai_stream: the\n # wire must carry MULTIPLE `data: {\"type\":\"token\"}` frames, not one blob.\n _deepseek_env(monkeypatch)\n produced: list = []\n\n def create(model, **kw):\n return _LazyStream([\"Point one. \", \"Point two. \", \"Point three.\"], produced)\n\n stream_client = MagicMock()\n stream_client.chat.completions.create.side_effect = create\n\n from src.services.chat_service import ChatPreflight\n from src.services.subscription_gating import GateCheck\n gate = GateCheck(allowed=True, feature=\"monthly_ai_message_limit\", usage=0,\n limit=300, remaining=100, plan=\"free\", message=\"ok\")\n with (\n patch(\"src.api.routers.rico_chat.get_current_user\", return_value={\"email\": \"u@example.com\"}),\n patch(\"src.services.chat_service.run_chat_preflight\",\n return_value=ChatPreflight(terminal=None, gate=gate)),\n patch(\"src.services.chat_service.should_stream_ai\", return_value=True),\n patch(\"src.repositories.profile_repo.get_profile\", return_value=None),\n patch(\"src.rico_chat_api.RicoChatAPI._build_openai_context\", return_value={}),\n patch(\"src.rico_chat_api.RicoChatAPI._append_chat\"),\n patch.object(rt, \"_build_client\", return_value=stream_client),\n ):\n res = client.post(\"/api/v1/rico/chat/stream\", json={\"message\": \"give me three points\"})\n assert res.status_code == 200 # 200, but that is NOT the proof — the frames are\n token_frames = res.text.count('\"type\": \"token\"') + res.text.count('\"type\":\"token\"')\n assert token_frames >= 3, f\"expected multiple incremental token frames, got {token_frames}\"\n assert res.text.count(rt._FALLBACK_TEXT) == 0 # never the buffered fallback blob\n\n\n# ── DeepSeek explicit thinking contract ──────────────────────────────────────\n\ndef _reasoning_chunk(*, content=None, reasoning_content=None):\n delta = MagicMock()\n delta.content = content\n delta.reasoning_content = reasoning_content\n choice = MagicMock()\n choice.delta = delta\n chunk = MagicMock()\n chunk.choices = [choice]\n return chunk\n\n\ndef test_deepseek_non_stream_explicitly_disables_thinking(monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = _chat_response(\"Final answer\")\n\n with (\n patch.object(rt, \"_build_client\", return_value=client),\n patch.object(rt, \"_deepseek_model_chain\", return_value=[\"deepseek-v4-flash\"]),\n patch.object(rt, \"_advisory_chain\", side_effect=lambda models, provider=None: models),\n ):\n result = rt.call_openai_minimal(\"hello\", provider=\"deepseek\")\n\n kwargs = client.chat.completions.create.call_args.kwargs\n assert result[\"success\"] is True\n assert result[\"text\"] == \"Final answer\"\n assert kwargs[\"extra_body\"] == {\"thinking\": {\"type\": \"disabled\"}}\n assert \"stream\" not in kwargs\n\n\ndef test_deepseek_stream_explicitly_disables_thinking_and_never_yields_reasoning(monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n client.chat.completions.create.return_value = iter([\n _reasoning_chunk(reasoning_content=\"private reasoning\"),\n _reasoning_chunk(content=\"Final answer\"),\n ])\n\n with (\n patch.object(rt, \"_build_client\", return_value=client),\n patch.object(rt, \"_deepseek_model_chain\", return_value=[\"deepseek-v4-flash\"]),\n patch.object(rt, \"_advisory_chain\", side_effect=lambda models, provider=None: models),\n ):\n output = list(rt.call_openai_stream(\"hello\", provider=\"deepseek\"))\n\n kwargs = client.chat.completions.create.call_args.kwargs\n assert output == [\"Final answer\"]\n assert \"private reasoning\" not in \"\".join(output)\n assert kwargs[\"extra_body\"] == {\"thinking\": {\"type\": \"disabled\"}}\n assert kwargs[\"stream\"] is True\n\n\ndef test_reasoning_only_stream_stays_fail_closed_and_falls_back_once(monkeypatch):\n _deepseek_env(monkeypatch)\n client = MagicMock()\n\n def create(*, model, **kwargs):\n assert kwargs[\"extra_body\"] == {\"thinking\": {\"type\": \"disabled\"}}\n if model == \"deepseek-v4-flash\":\n return iter([_reasoning_chunk(reasoning_content=\"private reasoning\")])\n return iter([_reasoning_chunk(content=\"Fallback answer\")])\n\n client.chat.completions.create.side_effect = create\n\n with (\n patch.object(rt, \"_build_client\", return_value=client),\n patch.object(\n rt,\n \"_deepseek_model_chain\",\n return_value=[\"deepseek-v4-flash\", \"deepseek-v4-pro\"],\n ),\n patch.object(rt, \"_advisory_chain\", side_effect=lambda models, provider=None: models),\n ):\n output = list(rt.call_openai_stream(\"hello\", provider=\"deepseek\"))\n\n attempts = rt.get_reasoning_health()[\"attempts\"]\n assert output == [\"Fallback answer\"]\n assert client.chat.completions.create.call_count == 2\n assert attempts[0][\"model\"] == \"deepseek-v4-flash\"\n assert attempts[0][\"category\"] == \"response_contract\"\n assert attempts[0][\"ok\"] is False\n assert attempts[1][\"model\"] == \"deepseek-v4-pro\"\n assert attempts[1][\"ok\"] is True\n\n\ndef test_openai_responses_request_contract_is_unchanged(monkeypatch):\n monkeypatch.setenv(\"OPENAI_API_KEY\", \"sk-fake-openai-key-for-tests\")\n client = MagicMock()\n response = MagicMock()\n response.output_text = \"OpenAI answer\"\n client.responses.create.return_value = response\n\n with patch.object(rt, \"_build_client\", return_value=client):\n result = rt.call_openai_minimal(\"hello\", provider=\"openai\")\n\n kwargs = client.responses.create.call_args.kwargs\n assert result[\"success\"] is True\n assert result[\"text\"] == \"OpenAI answer\"\n assert \"extra_body\" not in kwargs\n assert \"thinking\" not in kwargs\n"}}}} |