7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
241 lines
8.8 KiB
Python
241 lines
8.8 KiB
Python
"""
|
|
Tests for realistic LLM output patterns in JSON parsing that are not covered
|
|
by the existing 75 tests across 3 files.
|
|
|
|
Source: src/local_deep_research/utilities/json_utils.py
|
|
"""
|
|
|
|
from local_deep_research.utilities.json_utils import (
|
|
extract_json,
|
|
get_llm_response_text,
|
|
_extract_by_brackets,
|
|
_strip_code_fences,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Think tags wrapping code fences (real DeepSeek pattern)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestThinkTagsWithCodeFences:
|
|
"""Real LLM patterns where think tags wrap JSON code fences."""
|
|
|
|
def test_think_tags_wrapping_json_code_fence(self):
|
|
"""<think>...</think> followed by ```json ... ``` — real DeepSeek pattern."""
|
|
text = '<think>Let me analyze this...</think>\n```json\n{"key": "value"}\n```'
|
|
result = extract_json(text)
|
|
assert result == {"key": "value"}
|
|
|
|
def test_think_tags_wrapping_list_code_fence(self):
|
|
"""Think tags before a JSON list in code fence."""
|
|
text = (
|
|
'<think>Planning response...</think>\n```json\n["a", "b", "c"]\n```'
|
|
)
|
|
result = extract_json(text)
|
|
assert result == ["a", "b", "c"]
|
|
|
|
def test_nested_think_content_with_json(self):
|
|
"""Think tags with JSON-like content inside, real JSON outside."""
|
|
text = '<think>Maybe {"wrong": true}?</think>\n{"correct": true}'
|
|
result = extract_json(text)
|
|
assert result == {"correct": True}
|
|
|
|
def test_unclosed_think_tag_before_json(self):
|
|
"""<think> without closing </think> before JSON."""
|
|
text = '<think>some reasoning\n{"key": "value"}'
|
|
result = extract_json(text)
|
|
assert result == {"key": "value"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Code fences with non-json language tags
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestNonJsonCodeFences:
|
|
"""JSON inside code fences tagged with non-json language."""
|
|
|
|
def test_typescript_tagged_code_fence(self):
|
|
"""```typescript wrapping JSON."""
|
|
text = '```typescript\n{"type": "config"}\n```'
|
|
# _strip_code_fences looks for ```json or plain ``` (3+ backticks)
|
|
# For non-json tags, it falls to the plain ``` path
|
|
result = extract_json(text)
|
|
# The stripping gets "typescript\n{...}" which isn't valid JSON
|
|
# but bracket extraction should still find the JSON
|
|
assert result == {"type": "config"}
|
|
|
|
def test_plain_code_fence(self):
|
|
"""Plain ``` without language tag."""
|
|
text = '```\n{"plain": true}\n```'
|
|
result = extract_json(text)
|
|
assert result == {"plain": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# JSON with preamble/trailing text (common LLM pattern)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestPreambleAndTrailing:
|
|
"""JSON surrounded by conversational text."""
|
|
|
|
def test_apologize_preamble(self):
|
|
"""'I apologize...' preamble before JSON."""
|
|
text = 'I apologize for the confusion. Here is the corrected JSON:\n{"fixed": true}'
|
|
result = extract_json(text)
|
|
assert result == {"fixed": True}
|
|
|
|
def test_trailing_explanation(self):
|
|
"""JSON followed by explanation text."""
|
|
text = '{"result": 42}\n\nThis represents the answer to the question.'
|
|
result = extract_json(text)
|
|
assert result == {"result": 42}
|
|
|
|
def test_preamble_and_trailing(self):
|
|
"""Both preamble and trailing text around JSON."""
|
|
text = 'Sure! Here it is:\n{"data": [1, 2, 3]}\nHope this helps!'
|
|
result = extract_json(text)
|
|
assert result == {"data": [1, 2, 3]}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_llm_response_text edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetLlmResponseText:
|
|
"""Edge cases for extracting text from LLM response objects."""
|
|
|
|
def test_none_returns_empty(self):
|
|
"""None input returns empty string."""
|
|
assert get_llm_response_text(None) == ""
|
|
|
|
def test_content_attribute_used(self):
|
|
"""Object with .content attribute uses it."""
|
|
obj = type("Resp", (), {"content": "hello"})()
|
|
assert get_llm_response_text(obj) == "hello"
|
|
|
|
def test_text_attribute_used(self):
|
|
"""Object with .text but no .content uses .text."""
|
|
obj = type("Resp", (), {"text": "world", "content": None})()
|
|
assert get_llm_response_text(obj) == "world"
|
|
|
|
def test_both_none_falls_to_str(self):
|
|
""".content and .text both None -> falls to str()."""
|
|
obj = type("Resp", (), {"content": None, "text": None})()
|
|
result = get_llm_response_text(obj)
|
|
# str() of the object
|
|
assert isinstance(result, str)
|
|
|
|
def test_empty_string_content_used(self):
|
|
"""Empty string .content is used as-is (not None)."""
|
|
obj = type("Resp", (), {"content": ""})()
|
|
assert get_llm_response_text(obj) == ""
|
|
|
|
def test_think_tags_removed(self):
|
|
"""Think tags in content are removed."""
|
|
obj = type("Resp", (), {"content": "<think>reasoning</think>answer"})()
|
|
assert get_llm_response_text(obj) == "answer"
|
|
|
|
def test_non_string_content_converted(self):
|
|
"""Non-string .content is converted to string."""
|
|
obj = type("Resp", (), {"content": 42})()
|
|
result = get_llm_response_text(obj)
|
|
assert result == "42"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _extract_by_brackets edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtractByBrackets:
|
|
"""Edge cases for bracket extraction."""
|
|
|
|
def test_adjacent_braces(self):
|
|
"""Adjacent {} objects — extracts outermost."""
|
|
result = _extract_by_brackets('{"a": 1}{"b": 2}', "{", "}")
|
|
# find("{")=0, rfind("}")=17 -> extracts whole string
|
|
assert '"a"' in result
|
|
assert '"b"' in result
|
|
|
|
def test_only_open_bracket(self):
|
|
"""Only open bracket, no close -> returns None."""
|
|
result = _extract_by_brackets("{no close", "{", "}")
|
|
assert result is None
|
|
|
|
def test_empty_string(self):
|
|
"""Empty string -> returns None."""
|
|
result = _extract_by_brackets("", "{", "}")
|
|
assert result is None
|
|
|
|
def test_close_before_open(self):
|
|
"""Close bracket before open -> returns None."""
|
|
result = _extract_by_brackets("} then {", "{", "}")
|
|
# find("{")=7, rfind("}")=0, 0 < 7 -> None
|
|
assert result is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _strip_code_fences edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestStripCodeFences:
|
|
"""Edge cases for code fence stripping."""
|
|
|
|
def test_no_code_fences(self):
|
|
"""Text without code fences returned as-is."""
|
|
text = '{"key": "value"}'
|
|
assert _strip_code_fences(text) == text
|
|
|
|
def test_json_code_fence(self):
|
|
"""```json ... ``` stripped correctly."""
|
|
text = '```json\n{"key": "value"}\n```'
|
|
result = _strip_code_fences(text)
|
|
assert result == '{"key": "value"}'
|
|
|
|
def test_multiple_code_fences_first_extracted(self):
|
|
"""Multiple code fences — first one extracted."""
|
|
text = '```json\n{"first": 1}\n```\n\n```json\n{"second": 2}\n```'
|
|
result = _strip_code_fences(text)
|
|
assert '{"first": 1}' in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# extract_json with expected_type
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtractJsonExpectedType:
|
|
"""Test expected_type parameter behavior."""
|
|
|
|
def test_expect_list_gets_list(self):
|
|
"""expected_type=list prefers list extraction."""
|
|
text = '{"dict": true}\n[1, 2, 3]'
|
|
result = extract_json(text, expected_type=list)
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_expect_dict_gets_dict(self):
|
|
"""expected_type=dict prefers dict extraction."""
|
|
text = '[1, 2]\n{"dict": true}'
|
|
result = extract_json(text, expected_type=dict)
|
|
assert result == {"dict": True}
|
|
|
|
def test_none_type_accepts_either(self):
|
|
"""expected_type=None accepts first valid JSON."""
|
|
text = '{"key": "value"}'
|
|
result = extract_json(text, expected_type=None)
|
|
assert result == {"key": "value"}
|
|
|
|
def test_empty_input_returns_none(self):
|
|
"""Empty string returns None."""
|
|
assert extract_json("") is None
|
|
assert extract_json(" ") is None
|
|
|
|
def test_no_valid_json_returns_none(self):
|
|
"""Text without valid JSON returns None."""
|
|
assert extract_json("just some text without json") is None
|