import json from unittest import mock import pytest from projektmatch import llm def fake_post(payloads): """Return a mock for requests.post yielding chat completions.""" responses = [] for p in payloads: r = mock.Mock() r.status_code = 200 r.json.return_value = {"choices": [{"message": {"content": p}}]} responses.append(r) return mock.Mock(side_effect=responses) def test_chat_json_strips_think_and_parses(): content = "lange Kette{\"a\": 1}" with mock.patch("projektmatch.llm.requests.post", fake_post([content])) as p: out = llm.chat_json("http://v/v1", "m", [{"role": "user", "content": "x"}], {"type": "object"}) assert out == {"a": 1} body = p.call_args.kwargs["json"] assert body["max_tokens"] == 8192 assert body["chat_template_kwargs"] == {"enable_thinking": False} assert body["response_format"]["json_schema"]["schema"] == {"type": "object"} def test_extract_schema_caps_requirements_list(): reqs = llm.EXTRACT_SCHEMA["properties"]["requirements"] assert reqs["maxItems"] == 40 def test_chat_json_strips_markdown_fences(): content = "```json\n{\"a\": 2}\n```" with mock.patch("projektmatch.llm.requests.post", fake_post([content])): out = llm.chat_json("http://v/v1", "m", [], {"type": "object"}) assert out == {"a": 2} def test_chat_json_retries_on_empty_content(): good = json.dumps({"a": 1}) with mock.patch("projektmatch.llm.time.sleep", lambda s: None), \ mock.patch("projektmatch.llm.requests.post", fake_post(["", good])): assert llm.chat_json("http://v/v1", "m", [], {"type": "object"}) == {"a": 1} def test_chat_json_raises_after_exhausted_retries(): with mock.patch("projektmatch.llm.time.sleep", lambda s: None), \ mock.patch("projektmatch.llm.requests.post", fake_post(["", "", ""])): with pytest.raises(llm.LlmError, match="kein JSON"): llm.chat_json("http://v/v1", "m", [], {"type": "object"}) def test_match_cv_retries_on_missing_nr_then_raises(): reqs = [{"nr": 1, "text": "Python"}, {"nr": 2, "text": "K8s"}] partial = json.dumps({"ratings": [ {"nr": 1, "rating": "yes", "reason": "ok"}]}) with mock.patch("projektmatch.llm.requests.post", fake_post([partial, partial])): with pytest.raises(llm.LlmError, match="unvollständig"): llm.match_cv("http://v/v1", "m", "CV", reqs) def test_match_cv_ok(): reqs = [{"nr": 1, "text": "Python"}] full = json.dumps({"ratings": [ {"nr": 1, "rating": "unknown", "reason": "Teilevidenz"}]}) with mock.patch("projektmatch.llm.requests.post", fake_post([full])): out = llm.match_cv("http://v/v1", "m", "CV", reqs) assert out[0]["rating"] == "unknown"