opencode-config/tests/test_permissions.py
Sergey 4e926a43a7
fix(readme): russian heading, simplified support, access_url, env username (#118)
* fix(readme): russian heading, simplified support, access_url, env username

* fix(ci): wrap long assert to satisfy ruff E501 in test_permissions.py

* docs(project-map): add PR#118 tags (create-readme, repo-readme, .env.example)

* fix(ci): format test_permissions.py for ruff format check

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-29 04:56:35 +03:00

190 lines
7.6 KiB
Python

"""Tests for global deny rules and role-based tool access in ``.opencode/opencode.json``.
Covers issue #39 acceptance criteria:
- Global ``permission.bash`` deny rules for direct ``git commit``/``gh pr create``/
``gh pr merge``/``gh issue create`` (findLast — last wins, so they override earlier
allows).
- ``agent.<name>.tools`` role-based access map per subagent.
- ``check-permissions.py`` exits 0 on the real repo configs.
Strategy:
- ``test_*_present`` load the real ``.opencode/opencode.json`` and assert structure.
- ``test_check_permissions_passes`` runs the validator as a subprocess (black-box).
"""
import json
import subprocess
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
OPENCODE_JSON = REPO_ROOT / ".opencode" / "opencode.json"
CHECK_PERM_SCRIPT = REPO_ROOT / ".opencode" / "scripts" / "check-permissions.py"
ENV_EXAMPLE = REPO_ROOT / ".env.example"
def _load_config() -> dict:
with open(OPENCODE_JSON) as f:
return json.load(f)
# ── CONTEXT7_API_KEY typo fix (issue #43) ────────────────────────────────────
def test_context7_api_key_present_in_env_example():
"""CONTEXT7_API_KEY (correct) is present in .env.example."""
content = ENV_EXAMPLE.read_text()
assert "CONTEXT7_API_KEY=" in content, "CONTEXT7_API_KEY missing from .env.example"
def test_context7_api_key_absent_in_env_example():
"""CONTEX7_API_KEY (typo) is absent from .env.example."""
content = ENV_EXAMPLE.read_text()
assert "CONTEX7_API_KEY=" not in content, "typo CONTEX7_API_KEY still in .env.example"
def test_context7_api_key_present_in_opencode_json():
"""CONTEXT7_API_KEY (correct) is referenced in opencode.json (context7 MCP)."""
content = OPENCODE_JSON.read_text()
assert "{env:CONTEXT7_API_KEY}" in content, "CONTEXT7_API_KEY missing from opencode.json"
def test_context7_api_key_absent_in_opencode_json():
"""CONTEX7_API_KEY (typo) is absent from opencode.json."""
content = OPENCODE_JSON.read_text()
assert "{env:CONTEX7_API_KEY}" not in content, "typo CONTEX7_API_KEY still in opencode.json"
# ── global deny rules ───────────────────────────────────────────────────────
# ── OPENCODE_SERVER_USERNAME in .env.example (issue #117) ───────────────────
def test_opencode_server_username_present_in_env_example():
"""OPENCODE_SERVER_USERNAME is present in .env.example."""
content = ENV_EXAMPLE.read_text()
assert "OPENCODE_SERVER_USERNAME" in content, (
"OPENCODE_SERVER_USERNAME missing from .env.example"
)
# ── global deny rules ───────────────────────────────────────────────────────
def test_global_deny_rules_present():
"""The 4 deny rules exist in permission.bash and are set to 'deny'."""
bash = _load_config()["permission"]["bash"]
expected = {
"git commit *": "deny",
"gh pr create *": "deny",
"gh pr merge *": "deny",
"gh issue create *": "deny",
}
for pattern, action in expected.items():
assert pattern in bash, f"missing deny rule: {pattern}"
assert bash[pattern] == action, f"{pattern}: expected {action}, got {bash[pattern]}"
def test_git_push_remains_allowed():
"""git push must NOT be denied (issue constraint)."""
bash = _load_config()["permission"]["bash"]
assert bash.get("git push *") == "allow", "git push must remain allowed"
def test_deny_rules_override_earlier_allows():
"""findLast semantics: deny rules come AFTER earlier allows, so deny wins.
For ``git commit *`` the allow and deny use the same pattern — JSON dedupes
keys, so ``json.load`` keeps the last value (deny). For the other 3 the deny
pattern is narrower (e.g. ``gh pr create *`` vs ``gh pr create*``) and appears
later in insertion order. This test verifies ordering for the differing
patterns and final value for the duplicate-key case.
"""
bash = _load_config()["permission"]["bash"]
keys = list(bash.keys())
# git commit *: same pattern for allow+deny, json.load keeps last (deny).
assert bash["git commit *"] == "deny"
# The other 3: deny pattern must appear after the broader allow pattern.
ordering_checks = [
("gh pr create*", "gh pr create *"),
("gh pr merge*", "gh pr merge *"),
("gh issue*", "gh issue create *"),
]
for allow_pattern, deny_pattern in ordering_checks:
if allow_pattern in keys and deny_pattern in keys:
assert keys.index(deny_pattern) > keys.index(allow_pattern), (
f"deny '{deny_pattern}' must come after allow '{allow_pattern}' "
f"(findLast: last wins)"
)
# ── agent.general.tools ────────────────────────────────────────────────────
def test_general_tools():
"""general: commit/create_pr/create_issue=true, merge_pr=false."""
tools = _load_config()["agent"]["general"]["tools"]
assert tools["commit"] is True
assert tools["create_pr"] is True
assert tools["create_issue"] is True
assert tools["merge_pr"] is False
assert _load_config()["agent"]["general"]["steps"] == 150
# ── agent.reviewer.tools ────────────────────────────────────────────────────
def test_reviewer_tools_readonly():
"""reviewer is read-only for repo ops; can create issues for discovered bugs."""
tools = _load_config()["agent"]["reviewer"]["tools"]
assert tools["commit"] is False
assert tools["create_pr"] is False
assert tools["create_issue"] is True
assert tools["merge_pr"] is False
# ── agent.docs-reviewer.tools ───────────────────────────────────────────────
def test_docs_reviewer_tools():
"""docs-reviewer: commit=true (project map/handoff), create_issue=true, rest false."""
tools = _load_config()["agent"]["docs-reviewer"]["tools"]
assert tools["commit"] is True
assert tools["create_pr"] is False
assert tools["create_issue"] is True
assert tools["merge_pr"] is False
# ── agent.memory-syncer.tools ───────────────────────────────────────────────
def test_memory_syncer_tools_readonly():
"""memory-syncer is read-only for repo ops; can create issues."""
tools = _load_config()["agent"]["memory-syncer"]["tools"]
assert tools["commit"] is False
assert tools["create_pr"] is False
assert tools["create_issue"] is True
assert tools["merge_pr"] is False
# ── check-permissions.py passes ─────────────────────────────────────────────
def test_check_permissions_passes():
"""The validator exits 0 with OK message on current configs."""
result = subprocess.run(
["python3", str(CHECK_PERM_SCRIPT)],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
assert "OK: No dangerous permission rules found." in result.stdout
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])