e11886bde9
- Label now explains the model with an example: a flag and its value go
on separate lines. Placeholder shows the common uv pattern.
- split_suspicious_args() flags lines that contain whitespace plus a
dash-prefixed token ('--directory /path') — legit single args with
spaces ('My Documents') are never touched. Quotes are respected.
- The editor shows a warning under the args box with a 'Fix: split onto
separate lines' button; the fix goes through the undo stack.
254 lines
8.8 KiB
Python
254 lines
8.8 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 json
|
|
|
|
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
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Suspicious-args detection (several argv tokens typed on one line)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_split_flag_and_value_on_one_line():
|
|
fixed, notes = c.split_suspicious_args(["--directory /path/to/server", "run", "main.py"])
|
|
assert fixed == ["--directory", "/path/to/server", "run", "main.py"]
|
|
assert len(notes) == 1
|
|
|
|
|
|
def test_split_full_option_string():
|
|
fixed, notes = c.split_suspicious_args(["-y @pkg/server --port 8080"])
|
|
assert fixed == ["-y", "@pkg/server", "--port", "8080"]
|
|
assert notes
|
|
|
|
|
|
def test_split_respects_quotes():
|
|
fixed, _ = c.split_suspicious_args(['--name "My Server"'])
|
|
assert fixed == ["--name", "My Server"]
|
|
|
|
|
|
def test_split_leaves_legit_spaces_alone():
|
|
args = ["/Users/me/My Documents", "hello world", "run"]
|
|
fixed, notes = c.split_suspicious_args(args)
|
|
assert fixed == args
|
|
assert notes == []
|
|
|
|
|
|
def test_split_leaves_clean_args_alone():
|
|
args = ["--directory", "/path/to/server", "run", "main.py"]
|
|
fixed, notes = c.split_suspicious_args(args)
|
|
assert fixed == args
|
|
assert notes == []
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Config-file repair (load-time)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_repair_config_file_fixes_and_reports(tmp_path):
|
|
p = tmp_path / "claude_desktop_config.json"
|
|
p.write_text(
|
|
'{\n "globalShortcut": "Cmd+Space",\n'
|
|
" // my servers\n"
|
|
' "mcpServers": {\n'
|
|
' "brave": {"command": "npx", "args": ["-y"],},\n'
|
|
" },\n}"
|
|
)
|
|
cfg, notes, preview = c.repair_config_file(p)
|
|
assert cfg["mcpServers"]["brave"]["command"] == "npx"
|
|
assert cfg["globalShortcut"] == "Cmd+Space" # unrelated keys survive
|
|
assert any("comment" in n for n in notes)
|
|
assert any("trailing" in n for n in notes)
|
|
assert json.loads(preview) == cfg
|
|
# nothing was written to disk
|
|
assert "//" in p.read_text()
|
|
|
|
|
|
def test_repair_config_file_hopeless_raises(tmp_path):
|
|
p = tmp_path / "claude_desktop_config.json"
|
|
p.write_text("total nonsense, no braces at all")
|
|
with pytest.raises(ValueError):
|
|
c.repair_config_file(p)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 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(" ")
|