test: migrate test_core.py script to a proper pytest suite under tests/
This commit is contained in:
@@ -1,92 +0,0 @@
|
|||||||
import json, tempfile, shutil
|
|
||||||
from pathlib import Path
|
|
||||||
import bcc_core as c
|
|
||||||
|
|
||||||
tmp = Path(tempfile.mkdtemp())
|
|
||||||
ok = []
|
|
||||||
|
|
||||||
def check(name, cond):
|
|
||||||
ok.append(cond)
|
|
||||||
print(("PASS" if cond else "FAIL"), "-", name)
|
|
||||||
|
|
||||||
# ---- 1. discovery (monkeypatch the base dir) ----
|
|
||||||
base = tmp / "AppSupport"
|
|
||||||
for d in ("Claude", "Claude-Work", "NotClaude"):
|
|
||||||
(base / d).mkdir(parents=True)
|
|
||||||
(base / "Claude" / c.CONFIG_FILENAME).write_text("{}")
|
|
||||||
# Claude-Work has no config yet; NotClaude shouldn't match
|
|
||||||
c.app_support_base = lambda: base
|
|
||||||
profs = c.discover_profiles()
|
|
||||||
labels = sorted(p.label for p in profs)
|
|
||||||
check("discovers Claude + Claude-Work, not NotClaude",
|
|
||||||
"Claude" in labels and "Claude-Work" in labels and "NotClaude" not in labels)
|
|
||||||
check("flags missing config file", any(p.label=="Claude-Work" and not p.config_exists for p in profs))
|
|
||||||
|
|
||||||
# ---- 2. write preserves OTHER keys + order, only touches mcpServers ----
|
|
||||||
cfgpath = tmp / "real.json"
|
|
||||||
original = {
|
|
||||||
"globalShortcut": "Cmd+Shift+Space",
|
|
||||||
"mcpServers": {"old": {"command": "node", "args": ["x.js"]}},
|
|
||||||
"someOtherTool": {"keep": True},
|
|
||||||
}
|
|
||||||
cfgpath.write_text(json.dumps(original, indent=2))
|
|
||||||
cfg = c.load_config(cfgpath)
|
|
||||||
servers = c.extract_servers(cfg)
|
|
||||||
# add a new one, disable the old one
|
|
||||||
servers.append(c.ServerEntry("brave", {"command": "npx", "args": ["-y", "@x/brave"]}, True))
|
|
||||||
servers[0].enabled = False
|
|
||||||
c.apply_servers(cfg, servers)
|
|
||||||
c.write_config(cfgpath, cfg)
|
|
||||||
written = json.loads(cfgpath.read_text())
|
|
||||||
keys = list(written.keys())
|
|
||||||
check("preserved globalShortcut", written.get("globalShortcut") == "Cmd+Shift+Space")
|
|
||||||
check("preserved someOtherTool", written.get("someOtherTool") == {"keep": True})
|
|
||||||
check("disabled server parked in _disabledMcpServers", "old" in written.get("_disabledMcpServers", {}))
|
|
||||||
check("enabled server in mcpServers", "brave" in written.get("mcpServers", {}))
|
|
||||||
check("old not in active mcpServers", "old" not in written.get("mcpServers", {}))
|
|
||||||
check("key order kept (globalShortcut before mcpServers)", keys.index("globalShortcut") < keys.index("mcpServers"))
|
|
||||||
check("backup created", (cfgpath.parent / c.BACKUP_DIRNAME).is_dir() and any((cfgpath.parent / c.BACKUP_DIRNAME).iterdir()))
|
|
||||||
|
|
||||||
# ---- 3. paste parser, all shapes ----
|
|
||||||
full = '{"mcpServers": {"a": {"command": "node", "args": ["a.js"]}}}'
|
|
||||||
check("parse full config shape", list(c.parse_pasted_json(full)) == ["a"])
|
|
||||||
inner = '{"b": {"command": "uvx", "args": ["mcp-server-git"]}}'
|
|
||||||
check("parse inner block shape", list(c.parse_pasted_json(inner)) == ["b"])
|
|
||||||
bare = '{"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]}'
|
|
||||||
parsed = c.parse_pasted_json(bare)
|
|
||||||
check("parse bare server + suggests name 'filesystem'", "filesystem" in parsed)
|
|
||||||
remote = '{"url": "https://mcp.asana.com/sse"}'
|
|
||||||
check("parse remote, suggests host name", "mcp" in c.parse_pasted_json(remote))
|
|
||||||
try:
|
|
||||||
c.parse_pasted_json('{"junk": 5}')
|
|
||||||
check("rejects junk", False)
|
|
||||||
except ValueError:
|
|
||||||
check("rejects junk", True)
|
|
||||||
|
|
||||||
# ---- 4. validation ----
|
|
||||||
bad = [c.ServerEntry("", {"command": "x"}, True),
|
|
||||||
c.ServerEntry("dup", {"command": "x"}, True),
|
|
||||||
c.ServerEntry("dup", {"command": "x"}, True),
|
|
||||||
c.ServerEntry("r", {"url": ""}, True),
|
|
||||||
c.ServerEntry("nocmd", {"command": ""}, True)]
|
|
||||||
probs = c.validate_servers(bad)
|
|
||||||
check("validation catches empty name", any("empty name" in p for p in probs))
|
|
||||||
check("validation catches duplicate", any("Duplicate" in p for p in probs))
|
|
||||||
check("validation catches missing url", any("no URL" in p for p in probs))
|
|
||||||
check("validation catches missing command", any("no command" in p for p in probs))
|
|
||||||
check("valid set passes clean", c.validate_servers([c.ServerEntry("good", {"command":"node"}, True)]) == [])
|
|
||||||
|
|
||||||
# ---- 5. dependency check (python3 exists; bogusxyz does not) ----
|
|
||||||
r_ok = c.check_dependency({"command": "python3"})
|
|
||||||
check("dep check finds python3", r_ok["status"] == "ok")
|
|
||||||
r_missing = c.check_dependency({"command": "definitely-not-real-binary-xyz"})
|
|
||||||
check("dep check flags missing", r_missing["status"] == "missing")
|
|
||||||
r_remote = c.check_dependency({"url": "https://example.com/mcp"})
|
|
||||||
check("dep check marks remote", r_remote["status"] == "remote")
|
|
||||||
# windows-style wrapper: cmd /c npx ... -> should look past cmd to npx
|
|
||||||
r_wrap = c.check_dependency({"command": "cmd", "args": ["/c", "definitely-not-real-xyz", "foo"]})
|
|
||||||
check("dep check sees through shell wrapper", "definitely-not-real-xyz" in r_wrap["label"])
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(f"{sum(ok)}/{len(ok)} passed")
|
|
||||||
shutil.rmtree(tmp)
|
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
"""Pytest port of the original test_core.py script (same 23 behaviours, now
|
||||||
|
proper test functions with tmp_path/monkeypatch fixtures)."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import bcc_core as c
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 1. Discovery
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
@pytest.fixture
|
||||||
|
def fake_base(tmp_path, monkeypatch):
|
||||||
|
base = tmp_path / "AppSupport"
|
||||||
|
for d in ("Claude", "Claude-Work", "NotClaude"):
|
||||||
|
(base / d).mkdir(parents=True)
|
||||||
|
(base / "Claude" / c.CONFIG_FILENAME).write_text("{}")
|
||||||
|
monkeypatch.setattr(c, "app_support_base", lambda: base)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def test_discovers_claude_dirs_not_others(fake_base):
|
||||||
|
labels = sorted(p.label for p in c.discover_profiles())
|
||||||
|
assert "Claude" in labels
|
||||||
|
assert "Claude-Work" in labels
|
||||||
|
assert "NotClaude" not in labels
|
||||||
|
|
||||||
|
|
||||||
|
def test_flags_missing_config_file(fake_base):
|
||||||
|
profs = c.discover_profiles()
|
||||||
|
assert any(p.label == "Claude-Work" and not p.config_exists for p in profs)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 2. Write pipeline: preserves other keys + order, only touches mcpServers
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
@pytest.fixture
|
||||||
|
def written_config(tmp_path):
|
||||||
|
cfgpath = tmp_path / "real.json"
|
||||||
|
original = {
|
||||||
|
"globalShortcut": "Cmd+Shift+Space",
|
||||||
|
"mcpServers": {"old": {"command": "node", "args": ["x.js"]}},
|
||||||
|
"someOtherTool": {"keep": True},
|
||||||
|
}
|
||||||
|
cfgpath.write_text(json.dumps(original, indent=2))
|
||||||
|
cfg = c.load_config(cfgpath)
|
||||||
|
servers = c.extract_servers(cfg)
|
||||||
|
servers.append(c.ServerEntry("brave", {"command": "npx", "args": ["-y", "@x/brave"]}, True))
|
||||||
|
servers[0].enabled = False
|
||||||
|
c.apply_servers(cfg, servers)
|
||||||
|
c.write_config(cfgpath, cfg)
|
||||||
|
return cfgpath, json.loads(cfgpath.read_text())
|
||||||
|
|
||||||
|
|
||||||
|
def test_preserves_unrelated_keys(written_config):
|
||||||
|
_, written = written_config
|
||||||
|
assert written.get("globalShortcut") == "Cmd+Shift+Space"
|
||||||
|
assert written.get("someOtherTool") == {"keep": True}
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_server_parked(written_config):
|
||||||
|
_, written = written_config
|
||||||
|
assert "old" in written.get(c.DISABLED_KEY, {})
|
||||||
|
assert "old" not in written.get("mcpServers", {})
|
||||||
|
|
||||||
|
|
||||||
|
def test_enabled_server_active(written_config):
|
||||||
|
_, written = written_config
|
||||||
|
assert "brave" in written.get("mcpServers", {})
|
||||||
|
|
||||||
|
|
||||||
|
def test_key_order_kept(written_config):
|
||||||
|
_, written = written_config
|
||||||
|
keys = list(written.keys())
|
||||||
|
assert keys.index("globalShortcut") < keys.index("mcpServers")
|
||||||
|
|
||||||
|
|
||||||
|
def test_backup_created(written_config):
|
||||||
|
cfgpath, _ = written_config
|
||||||
|
bdir = cfgpath.parent / c.BACKUP_DIRNAME
|
||||||
|
assert bdir.is_dir()
|
||||||
|
assert any(bdir.iterdir())
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 3. Paste parser — all accepted shapes
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def test_parse_full_config_shape():
|
||||||
|
full = '{"mcpServers": {"a": {"command": "node", "args": ["a.js"]}}}'
|
||||||
|
assert list(c.parse_pasted_json(full)) == ["a"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_inner_block_shape():
|
||||||
|
inner = '{"b": {"command": "uvx", "args": ["mcp-server-git"]}}'
|
||||||
|
assert list(c.parse_pasted_json(inner)) == ["b"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_bare_server_suggests_name():
|
||||||
|
bare = '{"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]}'
|
||||||
|
assert "filesystem" in c.parse_pasted_json(bare)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_remote_suggests_host_name():
|
||||||
|
remote = '{"url": "https://mcp.asana.com/sse"}'
|
||||||
|
assert "mcp" in c.parse_pasted_json(remote)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rejects_junk():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
c.parse_pasted_json('{"junk": 5}')
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 4. Validation
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
@pytest.fixture
|
||||||
|
def problems():
|
||||||
|
bad = [
|
||||||
|
c.ServerEntry("", {"command": "x"}, True),
|
||||||
|
c.ServerEntry("dup", {"command": "x"}, True),
|
||||||
|
c.ServerEntry("dup", {"command": "x"}, True),
|
||||||
|
c.ServerEntry("r", {"url": ""}, True),
|
||||||
|
c.ServerEntry("nocmd", {"command": ""}, True),
|
||||||
|
]
|
||||||
|
return c.validate_servers(bad)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_catches_empty_name(problems):
|
||||||
|
assert any("empty name" in p for p in problems)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_catches_duplicate(problems):
|
||||||
|
assert any("Duplicate" in p for p in problems)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_catches_missing_url(problems):
|
||||||
|
assert any("no URL" in p for p in problems)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_catches_missing_command(problems):
|
||||||
|
assert any("no command" in p for p in problems)
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_set_passes_clean():
|
||||||
|
assert c.validate_servers([c.ServerEntry("good", {"command": "node"}, True)]) == []
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 5. Dependency / PATH checking
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def test_dep_check_finds_python3():
|
||||||
|
assert c.check_dependency({"command": "python3"})["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dep_check_flags_missing():
|
||||||
|
assert c.check_dependency({"command": "definitely-not-real-binary-xyz"})["status"] == "missing"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dep_check_marks_remote():
|
||||||
|
assert c.check_dependency({"url": "https://example.com/mcp"})["status"] == "remote"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dep_check_sees_through_shell_wrapper():
|
||||||
|
r = c.check_dependency({"command": "cmd", "args": ["/c", "definitely-not-real-xyz", "foo"]})
|
||||||
|
assert "definitely-not-real-xyz" in r["label"]
|
||||||
Reference in New Issue
Block a user