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
228 lines
7.2 KiB
Python
228 lines
7.2 KiB
Python
"""High-value tests for utilities/json_utils.py pure logic."""
|
|
|
|
import json
|
|
import unittest
|
|
from unittest.mock import MagicMock
|
|
|
|
from local_deep_research.utilities.json_utils import (
|
|
extract_json,
|
|
get_llm_response_text,
|
|
_clean_llm_json_artifacts,
|
|
_extract_by_brackets,
|
|
_remove_think_tags,
|
|
_strip_code_fences,
|
|
)
|
|
|
|
|
|
class TestGetLlmResponseText(unittest.TestCase):
|
|
def test_none_returns_empty_string(self):
|
|
assert get_llm_response_text(None) == ""
|
|
|
|
def test_content_attribute(self):
|
|
msg = MagicMock()
|
|
msg.content = "hello world"
|
|
assert get_llm_response_text(msg) == "hello world"
|
|
|
|
def test_text_attribute_when_no_content(self):
|
|
msg = MagicMock(spec=[])
|
|
msg.text = "from text"
|
|
assert get_llm_response_text(msg) == "from text"
|
|
|
|
def test_str_fallback(self):
|
|
assert get_llm_response_text(42) == "42"
|
|
|
|
def test_think_tags_removed(self):
|
|
msg = MagicMock()
|
|
msg.content = "before <think>inner</think> after"
|
|
assert get_llm_response_text(msg) == "before after"
|
|
|
|
def test_content_none_falls_to_text(self):
|
|
msg = MagicMock()
|
|
msg.content = None
|
|
msg.text = "fallback"
|
|
assert get_llm_response_text(msg) == "fallback"
|
|
|
|
def test_non_string_content_converted(self):
|
|
msg = MagicMock()
|
|
msg.content = 123
|
|
result = get_llm_response_text(msg)
|
|
assert result == "123"
|
|
|
|
|
|
class TestRemoveThinkTags(unittest.TestCase):
|
|
def test_paired_tags(self):
|
|
assert _remove_think_tags("a <think>b</think> c") == "a c"
|
|
|
|
def test_orphaned_closing(self):
|
|
assert _remove_think_tags("text </think> more") == "text more"
|
|
|
|
def test_orphaned_opening(self):
|
|
assert _remove_think_tags("text <think> more") == "text more"
|
|
|
|
def test_multiline_think(self):
|
|
text = "start <think>\nline1\nline2\n</think> end"
|
|
assert _remove_think_tags(text) == "start end"
|
|
|
|
def test_no_tags(self):
|
|
assert _remove_think_tags("plain text") == "plain text"
|
|
|
|
def test_empty_string(self):
|
|
assert _remove_think_tags("") == ""
|
|
|
|
|
|
class TestStripCodeFences(unittest.TestCase):
|
|
def test_json_fence(self):
|
|
text = '```json\n{"key": "val"}\n```'
|
|
assert _strip_code_fences(text) == '{"key": "val"}'
|
|
|
|
def test_plain_fence(self):
|
|
text = '```\n{"key": "val"}\n```'
|
|
assert _strip_code_fences(text) == '{"key": "val"}'
|
|
|
|
def test_no_fence(self):
|
|
text = '{"key": "val"}'
|
|
assert _strip_code_fences(text) == text
|
|
|
|
def test_json_fence_with_surrounding_prose(self):
|
|
text = 'Here is the result:\n```json\n{"a": 1}\n```\nDone.'
|
|
assert _strip_code_fences(text) == '{"a": 1}'
|
|
|
|
def test_unclosed_fence_returns_original(self):
|
|
text = "```only one fence"
|
|
assert _strip_code_fences(text) == text
|
|
|
|
|
|
class TestExtractByBrackets(unittest.TestCase):
|
|
def test_curly_brackets(self):
|
|
text = 'prefix {"a": 1} suffix'
|
|
assert _extract_by_brackets(text, "{", "}") == '{"a": 1}'
|
|
|
|
def test_square_brackets(self):
|
|
text = "before [1, 2, 3] after"
|
|
assert _extract_by_brackets(text, "[", "]") == "[1, 2, 3]"
|
|
|
|
def test_nested_brackets(self):
|
|
text = '{"a": {"b": 1}}'
|
|
assert _extract_by_brackets(text, "{", "}") == '{"a": {"b": 1}}'
|
|
|
|
def test_no_brackets_returns_none(self):
|
|
assert _extract_by_brackets("no brackets", "{", "}") is None
|
|
|
|
def test_mismatched_order_returns_none(self):
|
|
assert _extract_by_brackets("} before {", "{", "}") is None
|
|
|
|
def test_only_open_returns_none(self):
|
|
assert _extract_by_brackets("{ no close", "{", "}") is None
|
|
|
|
|
|
class TestCleanLlmJsonArtifacts(unittest.TestCase):
|
|
def test_trailing_comma_before_brace(self):
|
|
text = '{"a": 1,}'
|
|
assert _clean_llm_json_artifacts(text) == '{"a": 1}'
|
|
|
|
def test_trailing_comma_before_bracket(self):
|
|
text = "[1, 2, 3,]"
|
|
assert _clean_llm_json_artifacts(text) == "[1, 2, 3]"
|
|
|
|
def test_line_comments(self):
|
|
text = '{"a": 1 // comment\n}'
|
|
result = _clean_llm_json_artifacts(text)
|
|
assert "//" not in result
|
|
|
|
def test_ellipsis_entries(self):
|
|
text = '["a", ..., "b"]'
|
|
result = _clean_llm_json_artifacts(text)
|
|
assert "..." not in result
|
|
parsed = json.loads(result)
|
|
assert parsed == ["a", "b"]
|
|
|
|
def test_quoted_ellipsis(self):
|
|
text = '["a", "...", "b"]'
|
|
result = _clean_llm_json_artifacts(text)
|
|
assert "..." not in result
|
|
parsed = json.loads(result)
|
|
assert parsed == ["a", "b"]
|
|
|
|
def test_no_artifacts_unchanged(self):
|
|
text = '{"a": 1}'
|
|
assert _clean_llm_json_artifacts(text) == text
|
|
|
|
|
|
class TestExtractJsonDirect(unittest.TestCase):
|
|
def test_none_returns_none(self):
|
|
assert extract_json(None) is None
|
|
|
|
def test_empty_string_returns_none(self):
|
|
assert extract_json("") is None
|
|
|
|
def test_whitespace_only_returns_none(self):
|
|
assert extract_json(" ") is None
|
|
|
|
def test_valid_dict(self):
|
|
assert extract_json('{"key": "val"}') == {"key": "val"}
|
|
|
|
def test_valid_list(self):
|
|
assert extract_json("[1, 2, 3]") == [1, 2, 3]
|
|
|
|
def test_invalid_json_returns_none(self):
|
|
assert extract_json("not json at all") is None
|
|
|
|
|
|
class TestExtractJsonExpectedType(unittest.TestCase):
|
|
def test_expect_dict_gets_dict(self):
|
|
result = extract_json('{"a": 1}', expected_type=dict)
|
|
assert result == {"a": 1}
|
|
|
|
def test_expect_list_gets_list(self):
|
|
result = extract_json("[1, 2]", expected_type=list)
|
|
assert result == [1, 2]
|
|
|
|
def test_expect_dict_skips_list(self):
|
|
# Text has both a list and a dict
|
|
text = 'Here is [1] and {"a": 1}'
|
|
result = extract_json(text, expected_type=dict)
|
|
assert isinstance(result, dict)
|
|
|
|
def test_expect_list_skips_dict(self):
|
|
text = 'Here is {"a": 1} and [1, 2]'
|
|
result = extract_json(text, expected_type=list)
|
|
assert isinstance(result, list)
|
|
|
|
def test_type_mismatch_falls_through(self):
|
|
# Only a list in text, but dict expected — returns None
|
|
result = extract_json("[1, 2]", expected_type=dict)
|
|
assert result is None
|
|
|
|
|
|
class TestExtractJsonComplex(unittest.TestCase):
|
|
def test_json_in_code_fence(self):
|
|
text = '```json\n{"a": 1}\n```'
|
|
assert extract_json(text) == {"a": 1}
|
|
|
|
def test_json_with_surrounding_prose(self):
|
|
text = 'The answer is: {"result": true} as shown.'
|
|
assert extract_json(text) == {"result": True}
|
|
|
|
def test_json_with_think_tags(self):
|
|
text = '<think>reasoning</think>{"answer": 42}'
|
|
assert extract_json(text) == {"answer": 42}
|
|
|
|
def test_json_with_trailing_comma(self):
|
|
text = '{"a": 1, "b": 2,}'
|
|
result = extract_json(text)
|
|
assert result == {"a": 1, "b": 2}
|
|
|
|
def test_json_with_comments(self):
|
|
text = '{"a": 1 // this is a comment\n}'
|
|
result = extract_json(text)
|
|
assert result == {"a": 1}
|
|
|
|
def test_nested_json(self):
|
|
text = '{"outer": {"inner": [1, 2, 3]}}'
|
|
result = extract_json(text)
|
|
assert result == {"outer": {"inner": [1, 2, 3]}}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|