217 lines
6.6 KiB
Python
217 lines
6.6 KiB
Python
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
|