104907e311
Merge PR 87 Co-authored-by: Jason Walker <913443@dadeschools.net> Co-committed-by: Jason Walker <913443@dadeschools.net>
196 lines
8.4 KiB
Python
196 lines
8.4 KiB
Python
"""Negative tests for LLM-Agent-SHA attribution (#86, Phase 0).
|
|
|
|
``LLM-Agent-SHA`` (docs/llm-agent-sha.md) is attribution metadata ONLY. These
|
|
tests prove it can never bypass the review/merge safety gates:
|
|
|
|
1. Same Gitea user + different LLM-Agent-SHA still fails self-review/approval.
|
|
2. Same Gitea user + different LLM-Agent-SHA still fails self-merge.
|
|
3. The eligibility logic does not consult SHA metadata at all — results are
|
|
identical with no SHA, one SHA, or a different SHA in the environment, and
|
|
no gate accepts an agent-SHA input.
|
|
|
|
Phase 0 adds no SHA support to any MCP tool; the environment variables set
|
|
here (``LLM_AGENT_SHA`` / ``LLM_AGENT_ROLE``) simulate a future launcher and
|
|
must be ignored by every gate.
|
|
"""
|
|
import inspect
|
|
import os
|
|
import re
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
|
|
|
|
from mcp_server import ( # noqa: E402
|
|
gitea_check_pr_eligibility,
|
|
gitea_merge_pr,
|
|
gitea_review_pr,
|
|
gitea_submit_pr_review,
|
|
)
|
|
|
|
FAKE_AUTH = "Basic dGVzdDp0ZXN0"
|
|
SHA_PATTERN = re.compile(r"^llm-[0-9a-f]{12}$")
|
|
# Two distinct, well-formed agent SHAs "belonging" to the same Gitea user.
|
|
SHA_IMPLEMENTER = "llm-8f3a9c2d6b41"
|
|
SHA_WOULD_BE_REVIEWER = "llm-41d0e7aa9f2c"
|
|
|
|
|
|
def _pr(author, state="open", sha="abc123", mergeable=True):
|
|
return {
|
|
"user": {"login": author},
|
|
"state": state,
|
|
"head": {"sha": sha},
|
|
"mergeable": mergeable,
|
|
}
|
|
|
|
|
|
class TestShaFormatConvention(unittest.TestCase):
|
|
"""The documented format: llm-<12 lowercase hex>, nothing identifying."""
|
|
|
|
def test_documented_examples_are_valid(self):
|
|
for value in (SHA_IMPLEMENTER, SHA_WOULD_BE_REVIEWER, "llm-b7c93d441a08"):
|
|
self.assertRegex(value, SHA_PATTERN)
|
|
|
|
def test_identifying_or_malformed_values_are_rejected(self):
|
|
for bad in (
|
|
"llm-8F3A9C2D6B41", # uppercase
|
|
"llm-8f3a9c2d6b4", # too short
|
|
"llm-8f3a9c2d6b411", # too long
|
|
"llm-opus4", # model name, not hex
|
|
"claude-8f3a9c2d6b41", # provider prefix
|
|
"jcwalker3", # username
|
|
"llm-user@example.com", # email
|
|
"8f3a9c2d6b41", # missing prefix
|
|
"",
|
|
):
|
|
self.assertNotRegex(bad, SHA_PATTERN)
|
|
|
|
|
|
class TestShaCannotBypassSelfReview(unittest.TestCase):
|
|
"""Scenario: user A (SHA 1) authored the PR; user A (SHA 2) tries to act."""
|
|
|
|
def _env(self, agent_sha, role):
|
|
# Reviewer-capable profile + a simulated launcher-injected agent SHA.
|
|
return {
|
|
"GITEA_PROFILE_NAME": "gitea-reviewer",
|
|
"GITEA_ALLOWED_OPERATIONS": "read,review,approve,merge",
|
|
"LLM_AGENT_SHA": agent_sha,
|
|
"LLM_AGENT_ROLE": role,
|
|
}
|
|
|
|
@patch("mcp_server.api_request")
|
|
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
|
def test_same_user_different_sha_cannot_approve(self, _auth, mock_api):
|
|
mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
|
|
env = self._env(SHA_WOULD_BE_REVIEWER, "reviewer")
|
|
with patch.dict(os.environ, env, clear=True):
|
|
r = gitea_check_pr_eligibility(pr_number=9, action="approve", remote="prgs")
|
|
self.assertFalse(r["eligible"])
|
|
self.assertIn("authenticated user is PR author", r["reasons"])
|
|
|
|
@patch("mcp_server.api_request")
|
|
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
|
def test_same_user_different_sha_cannot_merge(self, _auth, mock_api):
|
|
mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
|
|
env = self._env(SHA_WOULD_BE_REVIEWER, "merger")
|
|
with patch.dict(os.environ, env, clear=True):
|
|
r = gitea_check_pr_eligibility(pr_number=9, action="merge", remote="prgs")
|
|
self.assertFalse(r["eligible"])
|
|
self.assertIn("authenticated user is PR author", r["reasons"])
|
|
|
|
@patch("mcp_server.api_request")
|
|
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
|
def test_gated_merge_tool_refuses_self_merge_despite_sha(self, _auth, mock_api):
|
|
# Even the fully-confirmed gated merge path must refuse: correct
|
|
# confirmation string, mergeable PR, merge-capable profile — but the
|
|
# authenticated user is the PR author, whatever the agent SHA says.
|
|
mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
|
|
env = self._env(SHA_WOULD_BE_REVIEWER, "merger")
|
|
with patch.dict(os.environ, env, clear=True):
|
|
r = gitea_merge_pr(
|
|
pr_number=9, confirmation="MERGE PR 9", remote="prgs")
|
|
self.assertFalse(r.get("performed"))
|
|
for call in mock_api.call_args_list:
|
|
method, url = call.args[0], call.args[1]
|
|
self.assertFalse(
|
|
method == "POST" and url.endswith("/merge"),
|
|
f"self-merge mutation reached the API: {method} {url}",
|
|
)
|
|
|
|
@patch("mcp_server.api_request")
|
|
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
|
def test_review_tool_refuses_self_approval_despite_sha(self, _auth, mock_api):
|
|
mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
|
|
env = self._env(SHA_WOULD_BE_REVIEWER, "reviewer")
|
|
with patch.dict(os.environ, env, clear=True):
|
|
r = gitea_review_pr(
|
|
pr_number=9, event="APPROVE", body="self approve", merge=False,
|
|
remote="prgs")
|
|
self.assertFalse(r["success"])
|
|
self.assertIn("authenticated user is PR author", r["message"])
|
|
for call in mock_api.call_args_list:
|
|
method, url = call.args[0], call.args[1]
|
|
self.assertFalse(
|
|
method == "POST" and url.endswith("/reviews"),
|
|
f"self-review mutation reached the API: {method} {url}",
|
|
)
|
|
|
|
|
|
class TestEligibilityNeverConsultsSha(unittest.TestCase):
|
|
"""The gates have no SHA input: not via env, not via parameters."""
|
|
|
|
def _run_eligibility(self, extra_env):
|
|
env = {
|
|
"GITEA_PROFILE_NAME": "gitea-reviewer",
|
|
"GITEA_ALLOWED_OPERATIONS": "read,review,approve",
|
|
}
|
|
env.update(extra_env)
|
|
with patch("mcp_server.get_auth_header", return_value=FAKE_AUTH), \
|
|
patch("mcp_server.api_request") as mock_api:
|
|
mock_api.side_effect = [{"login": "reviewer-bot"}, _pr("author-bot")]
|
|
with patch.dict(os.environ, env, clear=True):
|
|
return gitea_check_pr_eligibility(
|
|
pr_number=5, action="approve", remote="prgs")
|
|
|
|
def test_result_identical_with_without_and_across_shas(self):
|
|
baseline = self._run_eligibility({})
|
|
with_one = self._run_eligibility(
|
|
{"LLM_AGENT_SHA": SHA_IMPLEMENTER, "LLM_AGENT_ROLE": "implementer"})
|
|
with_other = self._run_eligibility(
|
|
{"LLM_AGENT_SHA": SHA_WOULD_BE_REVIEWER, "LLM_AGENT_ROLE": "reviewer"})
|
|
self.assertEqual(baseline, with_one)
|
|
self.assertEqual(baseline, with_other)
|
|
self.assertTrue(baseline["eligible"]) # sanity: a real decision ran
|
|
|
|
def test_eligibility_result_carries_no_agent_sha(self):
|
|
r = self._run_eligibility(
|
|
{"LLM_AGENT_SHA": SHA_IMPLEMENTER, "LLM_AGENT_ROLE": "implementer"})
|
|
blob = repr(r)
|
|
self.assertNotIn(SHA_IMPLEMENTER, blob)
|
|
self.assertNotIn("LLM_AGENT", blob)
|
|
|
|
def test_gate_functions_accept_no_agent_sha_parameter(self):
|
|
for fn in (gitea_check_pr_eligibility, gitea_merge_pr,
|
|
gitea_review_pr, gitea_submit_pr_review):
|
|
for param in inspect.signature(fn).parameters:
|
|
lowered = param.lower()
|
|
self.assertNotIn("agent", lowered,
|
|
f"{fn.__name__} accepts agent param {param!r}")
|
|
self.assertNotIn("llm", lowered,
|
|
f"{fn.__name__} accepts llm param {param!r}")
|
|
|
|
def test_gate_sources_never_read_agent_sha(self):
|
|
# Phase 0 guarantee: no gate reads LLM_AGENT_* metadata at all.
|
|
for fn in (gitea_check_pr_eligibility, gitea_merge_pr,
|
|
gitea_review_pr, gitea_submit_pr_review):
|
|
src = inspect.getsource(fn)
|
|
self.assertNotIn("LLM_AGENT", src,
|
|
f"{fn.__name__} reads LLM_AGENT metadata")
|
|
self.assertNotIn("agent_sha", src,
|
|
f"{fn.__name__} consults an agent SHA")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|