Files
better-claude-config/tests/test_json_repair.py
T
AJ 6205c85b61
CI / Lint (ruff) (push) Successful in 10s
CI / Tests (py3.10) (push) Successful in 23s
CI / Tests (py3.12) (push) Successful in 10s
feat: lenient JSON repair for pasted MCP snippets
Pasted config snippets no longer have to be valid JSON. repair_json_text()
auto-fixes markdown fences, surrounding prose, // /* */ # comments,
trailing and missing commas, smart quotes, single quotes, unquoted keys,
Python/JS literals, and unclosed braces. parse_pasted_json_verbose()
reports every repair applied; the paste dialog now parses as you type
and previews exactly what will be added and what was fixed.

Also includes ruff lint fixes and formatting across bcc.py/bcc_core.py.
2026-07-01 23:43:39 -04:00

189 lines
6.5 KiB
Python

"""Tests for the lenient JSON repair pipeline (repair_json_text +
parse_pasted_json_verbose). Every case here is a shape people actually paste
from MCP docs, blog posts, and chat windows."""
import pytest
import bcc_core as c
GOOD = {"command": "npx", "args": ["-y", "@x/brave"]}
def assert_brave(servers):
assert "brave" in servers
assert servers["brave"]["command"] == "npx"
# --------------------------------------------------------------------------- #
# Valid JSON passes through untouched
# --------------------------------------------------------------------------- #
def test_strict_json_no_notes():
servers, notes = c.parse_pasted_json_verbose('{"brave": {"command": "npx", "args": []}}')
assert_brave(servers)
assert notes == []
# --------------------------------------------------------------------------- #
# Markdown / prose wrappers
# --------------------------------------------------------------------------- #
def test_markdown_fence():
text = 'Add this to your config:\n```json\n{"brave": {"command": "npx"}}\n```\nThen restart.'
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert any("fence" in n for n in notes)
def test_fence_without_language_tag():
text = '```\n{"brave": {"command": "npx"}}\n```'
servers, _ = c.parse_pasted_json_verbose(text)
assert_brave(servers)
def test_prose_around_bare_json():
text = 'Paste the following: {"brave": {"command": "npx"}} and restart Claude.'
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert any("before" in n or "after" in n for n in notes)
# --------------------------------------------------------------------------- #
# Comments
# --------------------------------------------------------------------------- #
def test_line_comments():
text = '{\n // the search server\n "brave": {"command": "npx"}\n}'
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert any("//" in n for n in notes)
def test_block_comments():
text = '{ /* config */ "brave": {"command": "npx"} }'
servers, _ = c.parse_pasted_json_verbose(text)
assert_brave(servers)
def test_url_with_double_slash_is_not_a_comment():
text = '{"remote": {"url": "https://mcp.example.com/sse"}}'
servers, notes = c.parse_pasted_json_verbose(text)
assert servers["remote"]["url"] == "https://mcp.example.com/sse"
assert notes == []
# --------------------------------------------------------------------------- #
# Commas
# --------------------------------------------------------------------------- #
def test_trailing_comma_object():
text = '{"brave": {"command": "npx", "args": ["-y"],},}'
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert any("trailing" in n for n in notes)
def test_missing_comma_between_servers():
text = '{"brave": {"command": "npx"}\n"git": {"command": "uvx"}}'
servers, notes = c.parse_pasted_json_verbose(text)
assert "brave" in servers and "git" in servers
assert any("missing comma" in n for n in notes)
def test_missing_comma_between_string_fields():
text = '{"brave": {"command": "npx"\n"args": ["-y"]}}'
servers, _ = c.parse_pasted_json_verbose(text)
assert servers["brave"]["args"] == ["-y"]
# --------------------------------------------------------------------------- #
# Quotes
# --------------------------------------------------------------------------- #
def test_smart_quotes():
text = "{“brave”: {“command”: “npx”}}"
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert any("smart quotes" in n for n in notes)
def test_single_quotes():
text = "{'brave': {'command': 'npx', 'args': ['-y']}}"
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert any("single-quoted" in n for n in notes)
def test_single_quotes_with_inner_double():
text = """{'brave': {'command': 'npx', 'env': {'NOTE': 'say "hi"'}}}"""
servers, _ = c.parse_pasted_json_verbose(text)
assert servers["brave"]["env"]["NOTE"] == 'say "hi"'
# --------------------------------------------------------------------------- #
# JS-style laxness
# --------------------------------------------------------------------------- #
def test_unquoted_keys():
text = '{brave: {command: "npx", args: ["-y"]}}'
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert any("bare object keys" in n for n in notes)
def test_python_literals():
text = '{"brave": {"command": "npx", "enabled": True, "extra": None}}'
servers, notes = c.parse_pasted_json_verbose(text)
assert servers["brave"]["enabled"] is True
assert servers["brave"]["extra"] is None
assert any("literals" in n for n in notes)
# --------------------------------------------------------------------------- #
# Structure damage
# --------------------------------------------------------------------------- #
def test_braceless_fragment():
text = '"brave": {"command": "npx", "args": ["-y"]}'
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert any("wrapped" in n for n in notes)
def test_missing_closing_braces():
text = '{"mcpServers": {"brave": {"command": "npx", "args": ["-y"]'
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert any("unclosed" in n for n in notes)
def test_kitchen_sink():
# fence + comment + single quotes + unquoted key + trailing comma + prose
text = (
"Here's the config you need:\n"
"```json\n"
"{\n"
" // brave search\n"
" brave: {\n"
" 'command': 'npx',\n"
" 'args': ['-y', '@x/brave',],\n"
" },\n"
"}\n"
"```\n"
)
servers, notes = c.parse_pasted_json_verbose(text)
assert_brave(servers)
assert servers["brave"]["args"] == ["-y", "@x/brave"]
assert len(notes) >= 4
# --------------------------------------------------------------------------- #
# Still fails cleanly on hopeless input
# --------------------------------------------------------------------------- #
def test_hopeless_input_raises_value_error():
with pytest.raises(ValueError):
c.parse_pasted_json_verbose("this is just prose with no json at all")
def test_junk_object_still_rejected():
with pytest.raises(ValueError):
c.parse_pasted_json_verbose('{"junk": 5}')
def test_empty_raises():
with pytest.raises(ValueError):
c.parse_pasted_json_verbose(" ")