Recently Written · git

ai-config-render

Render one canonical AI-agent config (rules, skills, MCP servers) into Claude Code, Codex, Copilot CLI, Gemini and more

git clone https://github.com/equwal/ai-config-render

Log | Files | Refs


tests/test_render.py (6058 bytes)

1 from __future__ import annotations
2 
3 import json
4 import shutil
5 import tomllib
6 from pathlib import Path
7 
8 import pytest
9 from hypothesis import example, given
10 from hypothesis import strategies as st
11 
12 from ai_config_render import BUILTIN_TARGETS, main, render_instructions, render_json, render_toml
13 
14 EXAMPLE = Path(__file__).parent.parent / "examples" / "basic"
15 
16 
17 @pytest.fixture
18 def env(tmp_path: Path) -> tuple[Path, Path, Path]:
19     """A source copy, a fake home, and a machine file that points both into tmp_path."""
20     home, source = tmp_path / "home", tmp_path / "src"
21     shutil.copytree(EXAMPLE, source)
22     cfg = (source / "aicr.toml").read_text(encoding="utf-8")
23     extra = "".join(f"[targets.{t}]\n" for t in BUILTIN_TARGETS if f"[targets.{t}]" not in cfg)
24     (source / "aicr.toml").write_text(cfg + extra, encoding="utf-8")
25     machine = tmp_path / "machine.toml"
26     machine.write_text(
27         f'name = "test"\nstate = {json.dumps(str(tmp_path / "state.json"))}\n[vars]\nhome = {json.dumps(home.as_posix())}\n',
28         encoding="utf-8",
29     )
30     return source, home, machine
31 
32 
33 def run(cmd: str, source: Path, machine: Path) -> int:
34     return main([cmd, "--source", str(source), "--machine", str(machine)])
35 
36 
37 def test_renders_every_builtin_format(env: tuple[Path, Path, Path]) -> None:
38     source, home, machine = env
39     assert run("render", source, machine) == 0
40     for spec in BUILTIN_TARGETS.values():
41         if "instructions" in spec:
42             text = Path(spec["instructions"].replace("${home}", str(home))).read_text(encoding="utf-8")
43             assert "<!-- aicr:style -->\n## Style" in text and "<!-- /aicr:testing -->" in text
44         p = Path(spec["mcp"]["path"].replace("${home}", str(home)))
45         if spec["mcp"]["format"] == "toml":
46             srv = tomllib.loads(p.read_text(encoding="utf-8"))["mcp_servers"]["filesystem"]
47         else:
48             srv = json.loads(p.read_text(encoding="utf-8"))["mcpServers"]["filesystem"]
49         assert srv["command"] == "npx" and srv["args"][-1] == f"{home.as_posix()}/projects"
50         for d in spec.get("skills", []):
51             assert (Path(d.replace("${home}", str(home))) / "commit-message" / "SKILL.md").is_file()
52     copilot = json.loads((home / ".copilot" / "mcp-config.json").read_text(encoding="utf-8"))
53     assert copilot["mcpServers"]["filesystem"]["tools"] == ["*"]
54 
55 
56 def test_idempotent_and_check(env: tuple[Path, Path, Path]) -> None:
57     source, _, machine = env
58     assert run("check", source, machine) == 1
59     run("render", source, machine)
60     assert run("check", source, machine) == 0
61     assert run("render", source, machine) == 0
62     assert run("check", source, machine) == 0
63 
64 
65 def test_preserves_user_content_and_drops_removed_entries(env: tuple[Path, Path, Path]) -> None:
66     source, home, machine = env
67     claude_md, codex = home / ".claude" / "CLAUDE.md", home / ".codex" / "config.toml"
68     claude_md.parent.mkdir(parents=True)
69     claude_md.write_text("# Mine\nkeep me\n", encoding="utf-8")
70     codex.parent.mkdir(parents=True)
71     codex.write_text('model = "x"\n\n[mcp_servers.mine]\ncommand = "a"\n', encoding="utf-8")
72     run("render", source, machine)
73     cfg = (source / "aicr.toml").read_text(encoding="utf-8")
74     (source / "aicr.toml").write_text(
75         cfg.replace("[rules.testing]\n", "").replace("[mcp.filesystem]", "[mcp.gone]"), encoding="utf-8"
76     )
77     run("render", source, machine)
78     text = claude_md.read_text(encoding="utf-8")
79     assert text.startswith("# Mine\nkeep me\n") and "aicr:style" in text and "aicr:testing" not in text
80     data = tomllib.loads(codex.read_text(encoding="utf-8"))
81     assert data["model"] == "x" and set(data["mcp_servers"]) == {"mine", "gone"}
82     assert list(claude_md.parent.glob("CLAUDE.md.aicr-bak"))
83 
84 
85 def test_list_targets(capsys: pytest.CaptureFixture[str], env: tuple[Path, Path, Path]) -> None:
86     source, _, machine = env
87     assert run("list-targets", source, machine) == 0
88     assert "* codex" in capsys.readouterr().out
89 
90 
91 text_st = st.text(st.characters(exclude_categories=["Cs"]), max_size=200).filter(lambda s: "<!--" not in s)
92 name_st = st.from_regex(r"[a-z][a-z0-9_-]{0,8}", fullmatch=True)
93 
94 
95 @given(text_st, st.dictionaries(name_st, text_st.filter(lambda s: s.strip() != ""), max_size=4), text_st)
96 def test_instruction_merge_round_trip(user: str, blocks: dict[str, str], new_body: str) -> None:
97     once = render_instructions(user, blocks, set())
98     assert render_instructions(once, blocks, set()) == once  # idempotent
99     for name, body in blocks.items():
100         assert f"<!-- aicr:{name} -->\n{body.rstrip()}\n<!-- /aicr:{name} -->\n" in once
101     assert render_instructions(once, {}, set(blocks)).rstrip("\n") == user.rstrip("\n")  # removing restores user text
102 
103 
104 json_val = st.recursive(
105     st.none() | st.booleans() | st.integers() | st.text(max_size=10), lambda c: st.lists(c, max_size=3), max_leaves=5
106 )
107 
108 
109 @given(st.dictionaries(name_st, json_val, max_size=3), st.dictionaries(name_st, st.just({"command": "c"}), max_size=3))
110 def test_json_merge_keeps_user_keys(user: dict[str, object], servers: dict[str, dict[str, object]]) -> None:
111     out = json.loads(render_json(json.dumps({"other": user}), servers, set(servers), "mcpServers"))
112     assert out["other"] == user and out["mcpServers"] == servers
113     assert json.loads(render_json(json.dumps(out), {}, set(servers), "mcpServers")) == {"other": user, "mcpServers": {}}
114 
115 
116 @given(
117     st.dictionaries(
118         name_st.filter(lambda n: n != "user"),
119         st.dictionaries(name_st, st.text(max_size=10) | st.integers(), max_size=3),
120         max_size=3,
121     )
122 )
123 @example({"a": {"a": chr(0x7F)}})  # regression: a raw DEL made invalid TOML
124 def test_toml_round_trip(servers: dict[str, dict[str, object]]) -> None:
125     out = render_toml('top = 1\n\n[mcp_servers.user]\ncommand = "u"\n', servers, set(servers))
126     data = tomllib.loads(out)
127     assert data["top"] == 1 and data["mcp_servers"].pop("user") == {"command": "u"}
128     assert data["mcp_servers"] == servers
129     assert render_toml(out, servers, set(servers)) == out