Kansho/backend/tests/test_profile_review_errors.py
Lars 9d79e3e92f
Some checks failed
Deploy Development / deploy (push) Successful in 51s
Test Suite / pytest-backend (push) Failing after 2m32s
Test Suite / smoke-dev (push) Successful in 0s
Test Suite / frontend-build (push) Successful in 15s
Run backend tests with pytest after Dev deploy.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 14:47:04 +02:00

180 lines
7.4 KiB
Python

"""Profile-review failures expose reason and whether the provider was billed.
Run from backend/: python tests/test_profile_review_errors.py
No live provider calls.
"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch
import httpx
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1"
from fastapi.testclient import TestClient
from engine import EngineError
from main import app
from privacy_gateway import public_cost_report
from providers import ProviderConfig, complete_chat, provider_error_diagnostics
def expect(ok: bool, message: str) -> None:
if not ok:
raise SystemExit(f"FAIL: {message}")
print(f"OK {message}")
def header(token: str) -> dict:
return {"X-Auth-Token": token}
def _openrouter() -> ProviderConfig:
return ProviderConfig(
role="generate",
name="openrouter",
mode="http",
url="https://openrouter.ai/api/v1/chat/completions",
model="openai/gpt-4o",
key="sk-test",
local=False,
zdr=True,
no_train=True,
)
def main() -> None:
none = public_cost_report({})
expect(none["billed"] == "no", "empty diagnostics are unbilled")
expect("keine Kosten entstanden" in none["note"], "empty diagnostics say no provider call")
detect_only = public_cost_report(
{"detect_calls": 2, "detect_cost": 0.0012, "generate_called": False, "generate_requested": False}
)
expect(detect_only["billed"] == "yes", "detect cost counts as billed")
expect("Detect" in detect_only["note"] and "Generate nicht" in detect_only["note"], "detect-only note names both stages")
sent = public_cost_report(
{"detect_calls": 1, "detect_cost": 0.0004, "generate_requested": True, "generate_called": False}
)
expect(sent["billed"] == "yes", "known detect cost stays billed if generate is unknown")
expect("Generate-Aufruf gesendet" in sent["note"], "unknown generate request is explicit")
unknown = public_cost_report({"generate_requested": True, "generate_called": False})
expect(unknown["billed"] == "unknown", "generate request without any usage is unknown")
known = public_cost_report(
{"detect_calls": 1, "detect_cost": 0.0004, "generate_called": True, "cost": 0.021}
)
expect(known["billed"] == "yes", "generate usage is billed")
expect(abs((known["total_cost"] or 0) - 0.0214) < 1e-9, "total adds detect and generate")
parsed = provider_error_diagnostics(
402,
'{"error":{"message":"Insufficient credits","code":402},"usage":{"cost":0.0}}',
)
expect(parsed["http_status"] == 402, "provider diagnostics keep HTTP status")
expect(parsed["provider_message"] == "Insufficient credits", "provider message is copied")
expect(parsed.get("cost") == 0.0, "error usage cost is kept")
class Reject:
status_code = 402
text = '{"error":{"message":"Insufficient credits","code":402}}'
def json(self):
return {"error": {"message": "Insufficient credits", "code": 402}}
try:
with patch("providers.httpx.post", lambda *args, **kwargs: Reject()):
complete_chat(_openrouter(), [{"role": "user", "content": "x"}], timeout=5)
raise SystemExit("FAIL: rejected complete_chat should raise")
except Exception as exc:
expect(getattr(exc, "code", "") == "provider_rejected", "credit reject is provider_rejected")
expect("HTTP 402" in str(exc), "reject message includes HTTP status")
expect("Insufficient credits" in str(exc), "reject message includes provider reason")
expect(exc.diagnostics.get("generate_requested") is True, "reject marks generate as requested")
def boom(*args, **kwargs):
raise httpx.TimeoutException("timed out")
try:
with patch("providers.httpx.post", boom):
complete_chat(_openrouter(), [{"role": "user", "content": "x"}], timeout=12)
raise SystemExit("FAIL: timeout should raise")
except Exception as exc:
expect(getattr(exc, "code", "") == "provider_timeout", "timeout is distinct from unreachable")
expect("12 Sekunden" in str(exc), "timeout names the limit")
with TestClient(app) as client:
setup = client.post(
"/api/auth/setup",
json={"email": "lars@example.test", "name": "Lars", "password": "test-pass"},
)
expect(setup.status_code == 200, f"setup {setup.text}")
token = setup.json()["token"]
headers = header(token)
imported = client.post(
"/api/journal/writing-profile/import",
headers=headers,
json={"body": "Heute Kirschen am Markt, später der Hafen, und abends nur zwei knappe Zeilen."},
)
expect(imported.status_code == 200, f"import {imported.text}")
rejected = EngineError(
"provider_rejected",
"generate-Provider hat die Anfrage abgelehnt (HTTP 402): Insufficient credits",
502,
diagnostics={
"http_status": 402,
"provider_message": "Insufficient credits",
"generate_requested": True,
"generate_called": False,
"detect_calls": 1,
"detect_cost": 0.0015,
},
)
with patch("engine.execute_prompt", side_effect=rejected):
response = client.post("/api/journal/writing-profile/initial-build/api", headers=headers)
expect(response.status_code == 502, f"rejected review status {response.text}")
detail = response.json()["detail"]
expect(detail["code"] == "provider_rejected", "API keeps provider code")
expect("Insufficient credits" in detail["message"], "API message keeps provider reason")
expect(detail["cost_report"]["billed"] == "yes", "detect cost is reported as billed")
expect("Detect" in detail["cost_report"]["note"], "cost note mentions detect")
expect(detail["cost_report"]["generate_requested"] is True, "generate was requested")
expect(detail["cost_report"]["generate_called"] is False, "generate did not complete")
invalid = {
"content": "kein json",
"diagnostics": {
"generate_called": True,
"generate_requested": True,
"cost": 0.033,
"detect_calls": 1,
"detect_cost": 0.002,
},
"trace": {"generate_called": True, "purpose": "profile_review"},
}
with patch("engine.execute_prompt", return_value=invalid):
response = client.post("/api/journal/writing-profile/review/api", headers=headers)
expect(response.status_code == 400, f"invalid result status {response.text}")
detail = response.json()["detail"]
expect(detail["code"] == "invalid_review_result", "parse failure keeps store code")
expect("JSON" in detail["message"] or "json" in detail["message"].lower(), "parse failure explains JSON")
expect(detail["cost_report"]["billed"] == "yes", "successful generate before parse failure is billed")
expect(detail["cost_report"]["generate_called"] is True, "parse failure still records generate")
expect(abs((detail["cost_report"]["cost"] or 0) - 0.033) < 1e-9, "generate cost survives parse failure")
print("ALL TESTS PASSED")
if __name__ == "__main__":
main()