fix(copilot): mirror transactional Python hooks

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 12:34:09 -04:00
1 parent af8d227b45
commit 9b67aad113
13 files changed
+2120 -583

No files matched your search

+216
View File
@@ -0,0 +1,216 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Any
import pytest
from elydora.plugins import copilot
from elydora.plugins.base import InstallConfig
AGENT_ID = "agent-1"
VALID_PRIVATE_KEY = base64.urlsafe_b64encode(bytes([11]) * 32).rstrip(
b"="
).decode("ascii")
MISSING = object()
@dataclass(frozen=True)
class CopilotFixture:
plugin: copilot.CopilotPlugin
config: InstallConfig
home_dir: Path
copilot_home: Path
project_dir: Path
agent_dir: Path
config_path: Path
legacy_path: Path
guard_path: Path
hook_path: Path
runtime_config_path: Path
private_key_path: Path
user_settings_path: Path
legacy_user_config_path: Path
claude_settings_path: Path
claude_local_settings_path: Path
repository_settings_path: Path
local_settings_path: Path
def write_json_or_text(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
source = (
value
if isinstance(value, str)
else json.dumps(value, indent=2, ensure_ascii=False) + "\n"
)
path.write_text(source, encoding="utf-8")
def prepare_fixture(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
*,
user_config: object = MISSING,
legacy_config: object = MISSING,
user_settings: object = MISSING,
legacy_user_config: object = MISSING,
claude_settings: object = MISSING,
claude_local_settings: object = MISSING,
repository_settings: object = MISSING,
local_settings: object = MISSING,
) -> CopilotFixture:
home_dir = tmp_path / "home with spaces and 'quote %COPILOT%"
project_dir = tmp_path / "project with spaces"
copilot_home = home_dir / "custom Copilot 'home"
agent_dir = home_dir / ".elydora" / AGENT_ID
config_path = copilot_home / "hooks" / "elydora-audit.json"
legacy_path = project_dir / ".github" / "hooks" / "hooks.json"
user_settings_path = copilot_home / "settings.json"
legacy_user_config_path = copilot_home / "config.json"
claude_settings_path = project_dir / ".claude" / "settings.json"
claude_local_settings_path = (
project_dir / ".claude" / "settings.local.json"
)
repository_settings_path = (
project_dir / ".github" / "copilot" / "settings.json"
)
local_settings_path = (
project_dir / ".github" / "copilot" / "settings.local.json"
)
project_dir.mkdir(parents=True)
optional = (
(config_path, user_config),
(legacy_path, legacy_config),
(user_settings_path, user_settings),
(legacy_user_config_path, legacy_user_config),
(claude_settings_path, claude_settings),
(claude_local_settings_path, claude_local_settings),
(repository_settings_path, repository_settings),
(local_settings_path, local_settings),
)
for path, value in optional:
if value is not MISSING:
write_json_or_text(path, value)
monkeypatch.chdir(project_dir)
monkeypatch.setenv("HOME", str(home_dir))
monkeypatch.setenv("USERPROFILE", str(home_dir))
monkeypatch.setenv("COPILOT_HOME", str(copilot_home))
config: InstallConfig = {
"agent_id": AGENT_ID,
"agent_name": "copilot",
"org_id": "org-1",
"private_key": VALID_PRIVATE_KEY,
"kid": "kid-1",
"token": "token-1",
"base_url": "http://127.0.0.1:9",
"guard_script_path": str(agent_dir / "guard.py"),
}
return CopilotFixture(
plugin=copilot.CopilotPlugin(),
config=config,
home_dir=home_dir,
copilot_home=copilot_home,
project_dir=project_dir,
agent_dir=agent_dir,
config_path=config_path,
legacy_path=legacy_path,
guard_path=agent_dir / "guard.py",
hook_path=agent_dir / "hook.py",
runtime_config_path=agent_dir / "config.json",
private_key_path=agent_dir / "private.key",
user_settings_path=user_settings_path,
legacy_user_config_path=legacy_user_config_path,
claude_settings_path=claude_settings_path,
claude_local_settings_path=claude_local_settings_path,
repository_settings_path=repository_settings_path,
local_settings_path=local_settings_path,
)
def managed_handler(
settings: dict[str, Any], event: str, script_name: str
) -> dict[str, Any]:
for handler in settings.get("hooks", {}).get(event, []):
if script_name in str(handler.get("bash")):
return handler
raise AssertionError(f"managed {event} handler not found")
def assert_native_handler(handler: dict[str, Any]) -> None:
assert set(handler) == {"type", "bash", "powershell", "timeoutSec"}
assert handler["type"] == "command"
assert handler["timeoutSec"] == 10
assert handler["bash"].startswith("'")
assert Path(sys.executable).name in handler["bash"]
assert handler["powershell"].startswith("& '")
assert handler["powershell"].endswith("; exit $LASTEXITCODE")
def legacy_managed_config(
fixture: CopilotFixture,
extra_hooks: dict[str, Any] | None = None,
) -> dict[str, Any]:
return {
"version": 1,
"hooks": {
"preToolUse": [{
"type": "command",
"bash": f'"{sys.executable}" {fixture.guard_path}',
"powershell": f'"{sys.executable}" {fixture.guard_path}',
"timeoutSec": 5,
}],
"postToolUse": [{
"type": "command",
"bash": str(fixture.hook_path),
"powershell": str(fixture.hook_path),
"timeoutSec": 5,
}],
**(extra_hooks or {}),
},
}
def run_hook(
handler: dict[str, Any],
fixture: CopilotFixture,
payload: str,
) -> subprocess.CompletedProcess[str]:
if os.name == "nt":
command = [
"powershell.exe",
"-NoProfile",
"-NonInteractive",
"-Command",
handler["powershell"],
]
else:
command = ["/bin/sh", "-c", handler["bash"]]
return subprocess.run(
command,
capture_output=True,
check=False,
cwd=fixture.project_dir,
env={
**os.environ,
"HOME": str(fixture.home_dir),
"USERPROFILE": str(fixture.home_dir),
},
input=payload,
text=True,
)
def assert_runtime_absent(fixture: CopilotFixture) -> None:
assert fixture.guard_path.exists() is False
assert fixture.hook_path.exists() is False
assert fixture.runtime_config_path.exists() is False
assert fixture.private_key_path.exists() is False