fix(letta): mirror official Python hook integration
This commit is contained in:
1 parent
24a9d64978
commit
53476feca7
19 files changed
+2443
-212
No files matched your search
@@ -0,0 +1,289 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import json
|
||||
import ntpath
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
from threading import Thread
|
||||
from typing import Any, Mapping, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from elydora.plugins.letta import LettaPlugin
|
||||
from elydora.utils import base64url_encode
|
||||
|
||||
|
||||
AGENT_ID = "agent-1"
|
||||
MISSING = object()
|
||||
VALID_PRIVATE_KEY = base64url_encode(bytes(range(32)))
|
||||
|
||||
|
||||
@dataclass
|
||||
class LettaFixture:
|
||||
root_dir: Path
|
||||
home_dir: Path
|
||||
project_dir: Path
|
||||
global_path: Path
|
||||
project_path: Path
|
||||
local_path: Path
|
||||
agent_dir: Path
|
||||
guard_path: Path
|
||||
audit_path: Path
|
||||
runtime_config_path: Path
|
||||
private_key_path: Path
|
||||
plugin: LettaPlugin
|
||||
config: dict[str, str]
|
||||
|
||||
def install(self) -> None:
|
||||
self.plugin.install(self.config)
|
||||
|
||||
def source(self, file_path: Optional[Path] = None) -> str:
|
||||
path = file_path or self.global_path
|
||||
with path.open("r", encoding="utf-8", newline="") as file:
|
||||
return file.read()
|
||||
|
||||
def settings(self, file_path: Optional[Path] = None) -> dict[str, Any]:
|
||||
return json.loads(self.source(file_path))
|
||||
|
||||
|
||||
class ElydoraApiHandler(BaseHTTPRequestHandler):
|
||||
agent_status: Any = "active"
|
||||
operation_status = 201
|
||||
requests: list[dict[str, Any]] = []
|
||||
|
||||
@classmethod
|
||||
def reset(cls, status: Any, operation_status: int) -> None:
|
||||
cls.agent_status = status
|
||||
cls.operation_status = operation_status
|
||||
cls.requests = []
|
||||
|
||||
def _respond(self, status: int, value: object) -> None:
|
||||
body = json.dumps(value).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
type(self).requests.append({
|
||||
"method": "GET",
|
||||
"path": self.path,
|
||||
"authorization": self.headers.get("Authorization"),
|
||||
})
|
||||
self._respond(200, {"agent": {"status": type(self).agent_status}})
|
||||
|
||||
def do_POST(self) -> None:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
raw = self.rfile.read(length)
|
||||
type(self).requests.append({
|
||||
"method": "POST",
|
||||
"path": self.path,
|
||||
"authorization": self.headers.get("Authorization"),
|
||||
"raw": raw,
|
||||
"json": json.loads(raw),
|
||||
})
|
||||
self._respond(
|
||||
type(self).operation_status,
|
||||
{"operation": {"accepted": True}},
|
||||
)
|
||||
|
||||
def log_message(self, _format: str, *_args: object) -> None:
|
||||
return
|
||||
|
||||
|
||||
def start_api_server(
|
||||
*, status: Any = "active", operation_status: int = 201
|
||||
) -> ThreadingHTTPServer:
|
||||
ElydoraApiHandler.reset(status, operation_status)
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), ElydoraApiHandler)
|
||||
Thread(target=server.serve_forever, daemon=True).start()
|
||||
return server
|
||||
|
||||
|
||||
def server_base_url(server: ThreadingHTTPServer) -> str:
|
||||
return f"http://127.0.0.1:{server.server_port}"
|
||||
|
||||
|
||||
def write_text(path: Path, source: str, mode: int = 0o600) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(source, encoding="utf-8", newline="")
|
||||
os.chmod(path, mode)
|
||||
|
||||
|
||||
def write_json(path: Path, value: object, mode: int = 0o600) -> None:
|
||||
source = value if isinstance(value, str) else json.dumps(value, indent=2) + "\n"
|
||||
write_text(path, source, mode)
|
||||
|
||||
|
||||
def prepare_fixture(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
*,
|
||||
global_settings: object = MISSING,
|
||||
project_settings: object = MISSING,
|
||||
local_settings: object = MISSING,
|
||||
base_url: str = "http://127.0.0.1:9",
|
||||
) -> LettaFixture:
|
||||
home_dir = tmp_path / "home with spaces and 'quote $HOME %HOME%"
|
||||
project_dir = tmp_path / "project with spaces"
|
||||
project_dir.mkdir(parents=True)
|
||||
monkeypatch.setenv("HOME", str(home_dir))
|
||||
monkeypatch.setenv("USERPROFILE", str(home_dir))
|
||||
monkeypatch.chdir(project_dir)
|
||||
global_path = home_dir / ".letta" / "settings.json"
|
||||
project_path = project_dir / ".letta" / "settings.json"
|
||||
local_path = project_dir / ".letta" / "settings.local.json"
|
||||
agent_dir = home_dir / ".elydora" / AGENT_ID
|
||||
guard_path = agent_dir / "guard.py"
|
||||
audit_path = agent_dir / "hook.py"
|
||||
runtime_config_path = agent_dir / "config.json"
|
||||
private_key_path = agent_dir / "private.key"
|
||||
for path, value in (
|
||||
(global_path, global_settings),
|
||||
(project_path, project_settings),
|
||||
(local_path, local_settings),
|
||||
):
|
||||
if value is not MISSING:
|
||||
write_json(path, value)
|
||||
return LettaFixture(
|
||||
root_dir=tmp_path,
|
||||
home_dir=home_dir,
|
||||
project_dir=project_dir,
|
||||
global_path=global_path,
|
||||
project_path=project_path,
|
||||
local_path=local_path,
|
||||
agent_dir=agent_dir,
|
||||
guard_path=guard_path,
|
||||
audit_path=audit_path,
|
||||
runtime_config_path=runtime_config_path,
|
||||
private_key_path=private_key_path,
|
||||
plugin=LettaPlugin(),
|
||||
config={
|
||||
"org_id": "org-1",
|
||||
"agent_id": AGENT_ID,
|
||||
"agent_name": "letta",
|
||||
"private_key": VALID_PRIVATE_KEY,
|
||||
"kid": "kid-1",
|
||||
"token": "token-1",
|
||||
"base_url": base_url,
|
||||
"guard_script_path": str(guard_path),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def managed_handler(
|
||||
settings: Mapping[str, Any], event: str
|
||||
) -> dict[str, Any]:
|
||||
for group in settings.get("hooks", {}).get(event, []):
|
||||
if set(group) != {"matcher", "hooks"} or group.get("matcher") != "*":
|
||||
continue
|
||||
for handler in group["hooks"]:
|
||||
if (
|
||||
set(handler) == {"type", "command", "timeout"}
|
||||
and handler.get("type") == "command"
|
||||
and handler.get("timeout") == 10_000
|
||||
):
|
||||
return handler
|
||||
raise AssertionError(f"managed handler for {event!r} not found")
|
||||
|
||||
|
||||
def assert_managed_triple(settings: Mapping[str, Any]) -> dict[str, Any]:
|
||||
guard = managed_handler(settings, "PreToolUse")
|
||||
audit = managed_handler(settings, "PostToolUse")
|
||||
failure = managed_handler(settings, "PostToolUseFailure")
|
||||
assert audit["command"] == failure["command"]
|
||||
return {"guard": guard, "audit": audit, "failure": failure}
|
||||
|
||||
|
||||
def legacy_group(script_path: Path, event: str) -> dict[str, Any]:
|
||||
command = (
|
||||
f'"{sys.executable}" {script_path}'
|
||||
if event == "PreToolUse"
|
||||
else str(script_path)
|
||||
)
|
||||
return {
|
||||
"matcher": "*",
|
||||
"hooks": [{"type": "command", "command": command}],
|
||||
}
|
||||
|
||||
|
||||
def run_handler(
|
||||
handler: Mapping[str, Any], payload: bytes, fixture: LettaFixture
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
environment = {
|
||||
**os.environ,
|
||||
"HOME": str(fixture.home_dir),
|
||||
"USERPROFILE": str(fixture.home_dir),
|
||||
}
|
||||
if os.name == "nt":
|
||||
powershell = ntpath.join(
|
||||
os.environ.get("SystemRoot", r"C:\Windows"),
|
||||
"System32",
|
||||
"WindowsPowerShell",
|
||||
"v1.0",
|
||||
"powershell.exe",
|
||||
)
|
||||
command = [
|
||||
powershell,
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
str(handler["command"]),
|
||||
]
|
||||
else:
|
||||
command = ["/bin/bash", "-c", str(handler["command"])]
|
||||
return subprocess.run( # nosec B603
|
||||
command,
|
||||
input=payload,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
cwd=fixture.project_dir,
|
||||
env=environment,
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
|
||||
def official_input(fixture: LettaFixture, event: str) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"event_type": event,
|
||||
"working_directory": str(fixture.project_dir),
|
||||
"session_id": "session-1",
|
||||
"tool_name": "Bash",
|
||||
"tool_input": {"command": "echo test"},
|
||||
"tool_call_id": "call-1",
|
||||
"agent_id": "letta-agent-1",
|
||||
}
|
||||
if event == "PostToolUse":
|
||||
payload["tool_result"] = {"status": "success", "output": "test"}
|
||||
if event == "PostToolUseFailure":
|
||||
payload.update({
|
||||
"error_message": "Command failed",
|
||||
"error_type": "ProcessError",
|
||||
})
|
||||
return payload
|
||||
|
||||
|
||||
def run_cli(
|
||||
fixture: LettaFixture, arguments: list[str]
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
environment = dict(os.environ)
|
||||
repository = str(Path(__file__).resolve().parents[1])
|
||||
existing = environment.get("PYTHONPATH")
|
||||
environment["PYTHONPATH"] = (
|
||||
repository + os.pathsep + existing if existing else repository
|
||||
)
|
||||
return subprocess.run( # nosec B603
|
||||
[sys.executable, "-m", "elydora.cli", *arguments],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
cwd=fixture.project_dir,
|
||||
env=environment,
|
||||
timeout=30,
|
||||
)
|
||||
@@ -18,7 +18,6 @@ from elydora.plugins import (
|
||||
copilot,
|
||||
cursor,
|
||||
kiroide,
|
||||
letta,
|
||||
opencode,
|
||||
)
|
||||
from elydora.plugins.hook_template import generate_hook_script
|
||||
@@ -375,7 +374,6 @@ def test_atomic_secret_write_preserves_existing_file_on_replace_failure(
|
||||
(copilot, copilot.CopilotPlugin, "copilot"),
|
||||
(cursor, cursor.CursorPlugin, "cursor"),
|
||||
(kiroide, kiroide.KiroIdePlugin, "kiroide"),
|
||||
(letta, letta.LettaPlugin, "letta"),
|
||||
(opencode, opencode.OpenCodePlugin, "opencode"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess # nosec B404
|
||||
|
||||
import pytest
|
||||
|
||||
from letta_support import (
|
||||
ElydoraApiHandler,
|
||||
managed_handler,
|
||||
prepare_fixture,
|
||||
server_base_url,
|
||||
start_api_server,
|
||||
)
|
||||
|
||||
|
||||
SOURCE_ROOT = os.environ.get("ELYDORA_LETTA_SOURCE")
|
||||
BUN_BINARY = os.environ.get("ELYDORA_BUN_BINARY") or shutil.which("bun")
|
||||
|
||||
|
||||
def _module_url(relative_path: str) -> str:
|
||||
if SOURCE_ROOT is None:
|
||||
raise RuntimeError("ELYDORA_LETTA_SOURCE is required")
|
||||
return (Path(SOURCE_ROOT) / relative_path).resolve().as_uri()
|
||||
|
||||
|
||||
def _result_json(stdout: str) -> dict:
|
||||
return json.loads(stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
def _run_official_probe(fixture: object, mode: str) -> subprocess.CompletedProcess[str]:
|
||||
if SOURCE_ROOT is None or BUN_BINARY is None:
|
||||
raise RuntimeError("official Letta source and Bun are required")
|
||||
global_path = getattr(fixture, "global_path")
|
||||
settings = json.loads(Path(global_path).read_text(encoding="utf-8"))
|
||||
commands = {
|
||||
"guard": managed_handler(settings, "PreToolUse")["command"],
|
||||
"audit": managed_handler(settings, "PostToolUse")["command"],
|
||||
"failure": managed_handler(settings, "PostToolUseFailure")["command"],
|
||||
}
|
||||
source = f"""
|
||||
import {{ settingsManager }} from {json.dumps(_module_url('src/settings-manager.ts'))};
|
||||
import {{ getHooksForEvent }} from {json.dumps(_module_url('src/hooks/loader.ts'))};
|
||||
import {{ executeCommandHook }} from {json.dumps(_module_url('src/hooks/executor.ts'))};
|
||||
import {{ runPostToolUseHooks }} from {json.dumps(_module_url('src/hooks/index.ts'))};
|
||||
const workspace = process.env.ELYDORA_OFFICIAL_WORKSPACE;
|
||||
const commands = JSON.parse(process.env.ELYDORA_OFFICIAL_COMMANDS);
|
||||
await settingsManager.initialize();
|
||||
await settingsManager.loadProjectSettings(workspace);
|
||||
await settingsManager.loadLocalProjectSettings(workspace);
|
||||
const pre = await getHooksForEvent('PreToolUse', 'Bash', workspace);
|
||||
const post = await getHooksForEvent('PostToolUse', 'Bash', workspace);
|
||||
const failure = await getHooksForEvent('PostToolUseFailure', 'Bash', workspace);
|
||||
const managed = (hooks, command) => hooks.find((hook) => (
|
||||
hook.type === 'command' && hook.command === command
|
||||
));
|
||||
if (process.env.ELYDORA_OFFICIAL_MODE === 'active') {{
|
||||
const guardResult = await executeCommandHook(
|
||||
managed(pre, commands.guard),
|
||||
{{
|
||||
event_type: 'PreToolUse',
|
||||
working_directory: workspace,
|
||||
tool_name: 'Bash',
|
||||
tool_input: {{ command: 'echo official' }},
|
||||
tool_call_id: 'official-pre',
|
||||
agent_id: 'letta-agent',
|
||||
}},
|
||||
workspace,
|
||||
);
|
||||
const postResult = await executeCommandHook(
|
||||
managed(post, commands.audit),
|
||||
{{
|
||||
event_type: 'PostToolUse',
|
||||
working_directory: workspace,
|
||||
tool_name: 'Bash',
|
||||
tool_input: {{ command: 'echo official' }},
|
||||
tool_result: {{ status: 'success', output: 'official' }},
|
||||
tool_call_id: 'official-post',
|
||||
}},
|
||||
workspace,
|
||||
);
|
||||
const failureResult = await executeCommandHook(
|
||||
managed(failure, commands.failure),
|
||||
{{
|
||||
event_type: 'PostToolUseFailure',
|
||||
working_directory: workspace,
|
||||
tool_name: 'Bash',
|
||||
tool_input: {{ command: 'exit 1' }},
|
||||
error_message: 'official failure',
|
||||
error_type: 'ProcessError',
|
||||
tool_call_id: 'official-failure',
|
||||
}},
|
||||
workspace,
|
||||
);
|
||||
console.log(JSON.stringify({{
|
||||
counts: [pre.length, post.length, failure.length],
|
||||
exits: [guardResult.exitCode, postResult.exitCode, failureResult.exitCode],
|
||||
}}));
|
||||
}} else {{
|
||||
const result = await runPostToolUseHooks(
|
||||
'Bash',
|
||||
{{ command: 'echo unavailable' }},
|
||||
{{ status: 'success', output: 'unavailable' }},
|
||||
'official-post-error',
|
||||
workspace,
|
||||
);
|
||||
console.log(JSON.stringify({{
|
||||
blocked: result.blocked,
|
||||
errored: result.errored,
|
||||
exits: result.results.map((entry) => entry.exitCode),
|
||||
}}));
|
||||
}}
|
||||
await settingsManager.flush();
|
||||
"""
|
||||
environment = {
|
||||
**os.environ,
|
||||
"HOME": str(getattr(fixture, "home_dir")),
|
||||
"USERPROFILE": str(getattr(fixture, "home_dir")),
|
||||
"LETTA_SKIP_KEYCHAIN_CHECK": "1",
|
||||
"ELYDORA_OFFICIAL_COMMANDS": json.dumps(commands),
|
||||
"ELYDORA_OFFICIAL_MODE": mode,
|
||||
"ELYDORA_OFFICIAL_WORKSPACE": str(getattr(fixture, "project_dir")),
|
||||
}
|
||||
return subprocess.run( # nosec B603
|
||||
[BUN_BINARY, "--silent", "--eval", source],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
cwd=SOURCE_ROOT,
|
||||
env=environment,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
SOURCE_ROOT is None or BUN_BINARY is None,
|
||||
reason="official Letta source and Bun are required",
|
||||
)
|
||||
def test_official_letta_code_loads_and_executes_all_managed_hooks(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
assert SOURCE_ROOT is not None
|
||||
manifest = json.loads(
|
||||
(Path(SOURCE_ROOT) / "package.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert manifest["version"] == "0.28.13"
|
||||
server = start_api_server()
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch, tmp_path, base_url=server_base_url(server)
|
||||
)
|
||||
server_closed = False
|
||||
try:
|
||||
fixture.install()
|
||||
active = _run_official_probe(fixture, "active")
|
||||
assert active.returncode == 0, active.stderr
|
||||
assert _result_json(active.stdout) == {
|
||||
"counts": [1, 1, 1],
|
||||
"exits": [0, 0, 0],
|
||||
}
|
||||
assert len([
|
||||
request for request in ElydoraApiHandler.requests
|
||||
if request["method"] == "POST"
|
||||
]) == 2
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
server_closed = True
|
||||
unavailable = _run_official_probe(fixture, "unavailable")
|
||||
assert unavailable.returncode == 0, unavailable.stderr
|
||||
assert _result_json(unavailable.stdout) == {
|
||||
"blocked": False,
|
||||
"errored": True,
|
||||
"exits": [1],
|
||||
}
|
||||
finally:
|
||||
if not server_closed:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
@@ -0,0 +1,395 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from elydora import cli
|
||||
from elydora.plugins import letta
|
||||
from elydora.plugins.registry import SUPPORTED_AGENTS
|
||||
from letta_support import (
|
||||
AGENT_ID,
|
||||
VALID_PRIVATE_KEY,
|
||||
assert_managed_triple,
|
||||
legacy_group,
|
||||
prepare_fixture,
|
||||
run_cli,
|
||||
write_json,
|
||||
write_text,
|
||||
)
|
||||
|
||||
|
||||
def test_letta_is_registered_in_the_sdk_and_cli() -> None:
|
||||
assert SUPPORTED_AGENTS["letta"] == {
|
||||
"name": "Letta Code",
|
||||
"hook_event": "PreToolUse/PostToolUse/PostToolUseFailure",
|
||||
"config_path": "~/.letta/settings.json",
|
||||
}
|
||||
assert cli.PLUGIN_MAP["letta"] is letta.LettaPlugin
|
||||
|
||||
|
||||
def test_install_preserves_every_source_and_is_idempotent(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
global_source = (
|
||||
"{\r\n"
|
||||
' "theme": "dark",\r\n'
|
||||
' "hooks": {\r\n'
|
||||
' "FutureEvent": [null],\r\n'
|
||||
' "PreToolUse": [{ "matcher": "Bash", "hooks": '
|
||||
'[{ "type": "command", "command": "user-hook", "quiet": true }] }]\r\n'
|
||||
" }\r\n"
|
||||
"}\r\n"
|
||||
)
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
global_settings=global_source,
|
||||
project_settings={"owner": "project"},
|
||||
local_settings={"owner": "local"},
|
||||
)
|
||||
project_before = fixture.source(fixture.project_path)
|
||||
local_before = fixture.source(fixture.local_path)
|
||||
|
||||
fixture.install()
|
||||
first_source = fixture.source()
|
||||
settings = fixture.settings()
|
||||
assert "\r\n" in first_source
|
||||
assert settings["theme"] == "dark"
|
||||
assert settings["hooks"]["FutureEvent"] == [None]
|
||||
assert settings["hooks"]["PreToolUse"][0]["hooks"][0]["command"] == "user-hook"
|
||||
assert_managed_triple(settings)
|
||||
assert fixture.source(fixture.project_path) == project_before
|
||||
assert fixture.source(fixture.local_path) == local_before
|
||||
for file_path in (
|
||||
fixture.guard_path,
|
||||
fixture.audit_path,
|
||||
fixture.runtime_config_path,
|
||||
fixture.private_key_path,
|
||||
):
|
||||
assert file_path.is_file()
|
||||
|
||||
fixture.install()
|
||||
assert fixture.source() == first_source
|
||||
output = capsys.readouterr().out
|
||||
assert "run /hooks" in output
|
||||
assert "restart active sessions" in output
|
||||
|
||||
|
||||
def test_letta_validates_known_schemas_and_preserves_future_events(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path, global_settings={
|
||||
"hooks": {
|
||||
"FutureEvent": [None],
|
||||
"PreToolUse": [{
|
||||
"matcher": "[",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "user-command",
|
||||
"timeout": 0,
|
||||
"quiet": False,
|
||||
"futureField": True,
|
||||
}],
|
||||
}],
|
||||
"Stop": [{
|
||||
"hooks": [{
|
||||
"type": "prompt",
|
||||
"prompt": "Evaluate $ARGUMENTS",
|
||||
"model": "fast",
|
||||
"timeout": 30,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
})
|
||||
fixture.install()
|
||||
settings = fixture.settings()
|
||||
assert settings["hooks"]["FutureEvent"] == [None]
|
||||
assert settings["hooks"]["PreToolUse"][0]["matcher"] == "["
|
||||
assert settings["hooks"]["PreToolUse"][0]["hooks"][0]["futureField"] is True
|
||||
assert settings["hooks"]["Stop"][0]["hooks"][0]["type"] == "prompt"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source",
|
||||
[
|
||||
"{ malformed",
|
||||
"[]",
|
||||
'{ "theme": true, }',
|
||||
'{ "theme": true // comment\n }',
|
||||
'{ "hooks": {}, "hooks": {} }',
|
||||
'{ "hooks": null }',
|
||||
'{ "hooks": [] }',
|
||||
'{ "hooks": { "disabled": "yes" } }',
|
||||
'{ "hooks": { "PreToolUse": null } }',
|
||||
'{ "hooks": { "PreToolUse": [null] } }',
|
||||
'{ "hooks": { "PreToolUse": [{ "hooks": [] }] } }',
|
||||
'{ "hooks": { "Stop": [{ "matcher": "*", "hooks": [] }] } }',
|
||||
'{ "hooks": { "PreToolUse": [{ "matcher": "*" }] } }',
|
||||
'{ "hooks": { "Stop": [{ "hooks": [null] }] } }',
|
||||
'{ "hooks": { "Stop": [{ "hooks": [{ "type": "http" }] }] } }',
|
||||
'{ "hooks": { "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command" }] }] } }',
|
||||
'{ "hooks": { "Stop": [{ "hooks": [{ "type": "prompt" }] }] } }',
|
||||
'{ "hooks": { "Stop": [{ "hooks": [{ "type": "prompt", "prompt": "x", "model": 1 }] }] } }',
|
||||
'{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "x", "quiet": 1 }] }] } }',
|
||||
'{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "x", "timeout": -1 }] }] } }',
|
||||
],
|
||||
)
|
||||
def test_install_rejects_malformed_global_settings_before_every_write(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, source: str
|
||||
) -> None:
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch, tmp_path, global_settings=source
|
||||
)
|
||||
with pytest.raises((OSError, ValueError), match="Letta Code"):
|
||||
fixture.install()
|
||||
assert fixture.source() == source
|
||||
assert not fixture.agent_dir.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["project", "local"])
|
||||
def test_install_surfaces_malformed_read_only_sources(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, kind: str
|
||||
) -> None:
|
||||
options = (
|
||||
{"project_settings": "{ malformed"}
|
||||
if kind == "project"
|
||||
else {"local_settings": "{ malformed"}
|
||||
)
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path, **options)
|
||||
label = "project-local" if kind == "local" else kind
|
||||
with pytest.raises(ValueError, match=f"Letta Code {label}"):
|
||||
fixture.install()
|
||||
assert not fixture.global_path.exists()
|
||||
assert not fixture.agent_dir.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("global_settings", "project_settings", "local_settings", "enabled"),
|
||||
[
|
||||
({"hooks": {"disabled": True}}, {}, {}, False),
|
||||
({}, {"hooks": {"disabled": True}}, {}, False),
|
||||
({}, {}, {"hooks": {"disabled": True}}, False),
|
||||
(
|
||||
{"hooks": {"disabled": False}},
|
||||
{"hooks": {"disabled": True}},
|
||||
{"hooks": {"disabled": True}},
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_hooks_disabled_uses_official_precedence(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
global_settings: object,
|
||||
project_settings: object,
|
||||
local_settings: object,
|
||||
enabled: bool,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
global_settings=global_settings,
|
||||
project_settings=project_settings,
|
||||
local_settings=local_settings,
|
||||
)
|
||||
if enabled:
|
||||
fixture.install()
|
||||
assert_managed_triple(fixture.settings())
|
||||
else:
|
||||
with pytest.raises(ValueError, match="hooks.disabled"):
|
||||
fixture.install()
|
||||
assert not fixture.agent_dir.exists()
|
||||
|
||||
|
||||
def test_install_migrates_exact_legacy_and_preserves_lookalikes(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
guard_legacy = legacy_group(fixture.guard_path, "PreToolUse")
|
||||
guard_lookalike = legacy_group(fixture.guard_path, "PreToolUse")
|
||||
guard_lookalike["hooks"][0]["quiet"] = True
|
||||
audit_legacy = legacy_group(fixture.audit_path, "PostToolUse")
|
||||
write_json(fixture.global_path, {
|
||||
"owner": "user",
|
||||
"hooks": {
|
||||
"PreToolUse": [guard_legacy, guard_lookalike],
|
||||
"PostToolUse": [audit_legacy],
|
||||
},
|
||||
})
|
||||
fixture.install()
|
||||
settings = fixture.settings()
|
||||
assert_managed_triple(settings)
|
||||
assert any(
|
||||
group["hooks"][0].get("quiet") is True
|
||||
for group in settings["hooks"]["PreToolUse"]
|
||||
)
|
||||
managed = next(
|
||||
group for group in settings["hooks"]["PreToolUse"]
|
||||
if group["hooks"][0].get("timeout") == 10_000
|
||||
)
|
||||
managed["userField"] = "preserve-group"
|
||||
managed["hooks"].append({"type": "command", "command": "user-command"})
|
||||
write_json(fixture.global_path, settings)
|
||||
|
||||
fixture.plugin.uninstall(AGENT_ID)
|
||||
remaining = fixture.settings()
|
||||
assert remaining["owner"] == "user"
|
||||
assert any(
|
||||
group["hooks"][0].get("quiet") is True
|
||||
for group in remaining["hooks"]["PreToolUse"]
|
||||
)
|
||||
assert any(
|
||||
handler.get("command") == "user-command"
|
||||
for group in remaining["hooks"]["PreToolUse"]
|
||||
for handler in group["hooks"]
|
||||
)
|
||||
assert "PostToolUse" not in remaining["hooks"]
|
||||
assert "PostToolUseFailure" not in remaining["hooks"]
|
||||
|
||||
|
||||
def test_uninstall_removes_owned_file_and_preserves_user_settings(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
user = prepare_fixture(
|
||||
monkeypatch,
|
||||
tmp_path / "user",
|
||||
global_settings={"theme": "dark", "hooks": {"Notification": []}},
|
||||
)
|
||||
user.install()
|
||||
user.plugin.uninstall(AGENT_ID)
|
||||
assert user.settings() == {
|
||||
"theme": "dark",
|
||||
"hooks": {"Notification": []},
|
||||
}
|
||||
|
||||
owned = prepare_fixture(monkeypatch, tmp_path / "owned")
|
||||
owned.install()
|
||||
owned.plugin.uninstall(AGENT_ID)
|
||||
assert not owned.global_path.exists()
|
||||
|
||||
|
||||
def test_status_requires_effective_contract_and_strict_runtime_identity(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path, project_settings={})
|
||||
fixture.install()
|
||||
assert fixture.plugin.status()["installed"] is True
|
||||
write_json(fixture.project_path, {"hooks": {"disabled": True}})
|
||||
assert fixture.plugin.status()["installed"] is False
|
||||
write_json(fixture.project_path, {})
|
||||
settings = fixture.settings()
|
||||
settings["hooks"]["PostToolUseFailure"].append(
|
||||
settings["hooks"]["PostToolUseFailure"][-1]
|
||||
)
|
||||
write_json(fixture.global_path, settings)
|
||||
assert fixture.plugin.status()["installed"] is False
|
||||
|
||||
fixture.install()
|
||||
write_text(fixture.private_key_path, "invalid")
|
||||
with pytest.raises(ValueError, match="private key"):
|
||||
fixture.plugin.status()
|
||||
fixture.install()
|
||||
write_text(fixture.audit_path, "tampered")
|
||||
assert fixture.plugin.status()["installed"] is False
|
||||
fixture.install()
|
||||
runtime = json.loads(fixture.runtime_config_path.read_text())
|
||||
runtime["agent_name"] = "other"
|
||||
write_json(fixture.runtime_config_path, runtime)
|
||||
with pytest.raises(ValueError, match="runtime identity"):
|
||||
fixture.plugin.status()
|
||||
|
||||
|
||||
def test_preflight_rejects_unverifiable_runtime_and_invalid_agent_id(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
identity = prepare_fixture(monkeypatch, tmp_path / "identity")
|
||||
write_text(identity.audit_path, "unverified")
|
||||
with pytest.raises(ValueError, match="without config.json"):
|
||||
identity.install()
|
||||
assert not identity.global_path.exists()
|
||||
|
||||
invalid = prepare_fixture(monkeypatch, tmp_path / "invalid")
|
||||
invalid.config["agent_id"] = "../escape"
|
||||
with pytest.raises(ValueError, match="Invalid agent ID"):
|
||||
invalid.install()
|
||||
assert not invalid.global_path.exists()
|
||||
|
||||
|
||||
def test_letta_cli_install_status_and_uninstall_end_to_end(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch, tmp_path, global_settings={"theme": "dark"}
|
||||
)
|
||||
key_file = tmp_path / "install-private.key"
|
||||
token_file = tmp_path / "install-token.txt"
|
||||
write_text(key_file, VALID_PRIVATE_KEY + "\n")
|
||||
write_text(token_file, "token-1\n")
|
||||
install = run_cli(fixture, [
|
||||
"install",
|
||||
"--agent", "letta",
|
||||
"--org_id", "org-1",
|
||||
"--agent_id", AGENT_ID,
|
||||
"--kid", "kid-1",
|
||||
"--private_key_file", str(key_file),
|
||||
"--token_file", str(token_file),
|
||||
"--base_url", "http://127.0.0.1:9",
|
||||
])
|
||||
assert install.returncode == 0, install.stderr
|
||||
assert_managed_triple(fixture.settings())
|
||||
status = run_cli(fixture, ["status"])
|
||||
assert status.returncode == 0, status.stderr
|
||||
assert "Letta Code" in status.stdout and "[installed]" in status.stdout
|
||||
uninstall = run_cli(fixture, [
|
||||
"uninstall", "--agent", "letta", "--agent_id", AGENT_ID
|
||||
])
|
||||
assert uninstall.returncode == 0, uninstall.stderr
|
||||
assert fixture.settings() == {"theme": "dark"}
|
||||
assert not fixture.agent_dir.exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "nt", reason="Windows resolves HOME and USERPROFILE separately"
|
||||
)
|
||||
def test_windows_cli_separates_letta_home_from_runtime_home(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch, tmp_path, global_settings={"theme": "dark"}
|
||||
)
|
||||
runtime_home = tmp_path / "windows profile"
|
||||
runtime_agent_dir = runtime_home / ".elydora" / AGENT_ID
|
||||
monkeypatch.setenv("USERPROFILE", str(runtime_home))
|
||||
key_file = tmp_path / "install-private.key"
|
||||
write_text(key_file, VALID_PRIVATE_KEY + "\n")
|
||||
|
||||
install = run_cli(fixture, [
|
||||
"install",
|
||||
"--agent", "letta",
|
||||
"--org_id", "org-1",
|
||||
"--agent_id", AGENT_ID,
|
||||
"--kid", "kid-1",
|
||||
"--private_key_file", str(key_file),
|
||||
"--base_url", "http://127.0.0.1:9",
|
||||
])
|
||||
assert install.returncode == 0, install.stderr
|
||||
assert_managed_triple(fixture.settings())
|
||||
assert (runtime_agent_dir / "guard.py").is_file()
|
||||
assert (runtime_agent_dir / "hook.py").is_file()
|
||||
|
||||
status = run_cli(fixture, ["status"])
|
||||
assert status.returncode == 0, status.stderr
|
||||
assert "Letta Code" in status.stdout and "[installed]" in status.stdout
|
||||
|
||||
uninstall = run_cli(fixture, [
|
||||
"uninstall", "--agent", "letta", "--agent_id", AGENT_ID
|
||||
])
|
||||
assert uninstall.returncode == 0, uninstall.stderr
|
||||
assert fixture.settings() == {"theme": "dark"}
|
||||
assert not runtime_agent_dir.exists()
|
||||
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from letta_support import (
|
||||
ElydoraApiHandler,
|
||||
assert_managed_triple,
|
||||
official_input,
|
||||
prepare_fixture,
|
||||
run_handler,
|
||||
server_base_url,
|
||||
start_api_server,
|
||||
)
|
||||
|
||||
|
||||
def _payload(value: object) -> bytes:
|
||||
return json.dumps(value).encode()
|
||||
|
||||
|
||||
def test_guard_accepts_active_agents_with_native_exit_semantics(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
server = start_api_server(status="active")
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch, tmp_path, base_url=server_base_url(server)
|
||||
)
|
||||
try:
|
||||
fixture.install()
|
||||
guard = assert_managed_triple(fixture.settings())["guard"]
|
||||
result = run_handler(
|
||||
guard,
|
||||
_payload(official_input(fixture, "PreToolUse")),
|
||||
fixture,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout == b""
|
||||
assert len([
|
||||
request for request in ElydoraApiHandler.requests
|
||||
if request["method"] == "GET"
|
||||
]) == 1
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", ["frozen", "revoked"])
|
||||
def test_guard_propagates_blocking_states_through_exit_code_two(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, status: str
|
||||
) -> None:
|
||||
server = start_api_server(status=status)
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch, tmp_path, base_url=server_base_url(server)
|
||||
)
|
||||
try:
|
||||
fixture.install()
|
||||
guard = assert_managed_triple(fixture.settings())["guard"]
|
||||
result = run_handler(
|
||||
guard,
|
||||
_payload(official_input(fixture, "PreToolUse")),
|
||||
fixture,
|
||||
)
|
||||
assert result.returncode == 2, result.stderr
|
||||
assert result.stdout == b""
|
||||
assert status.encode() in result.stderr.lower()
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
def test_audit_forwards_complete_success_and_failure_payloads(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
server = start_api_server()
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch, tmp_path, base_url=server_base_url(server)
|
||||
)
|
||||
try:
|
||||
fixture.install()
|
||||
handlers = assert_managed_triple(fixture.settings())
|
||||
for handler_name, event in (
|
||||
("audit", "PostToolUse"),
|
||||
("failure", "PostToolUseFailure"),
|
||||
):
|
||||
payload = {
|
||||
**official_input(fixture, event),
|
||||
"future_provider_field": {"preserved": True},
|
||||
}
|
||||
result = run_handler(
|
||||
handlers[handler_name], _payload(payload), fixture
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout == b""
|
||||
posts = [
|
||||
request for request in ElydoraApiHandler.requests
|
||||
if request["method"] == "POST"
|
||||
]
|
||||
assert len(posts) == 2
|
||||
assert posts[0]["json"]["payload"] == {
|
||||
**official_input(fixture, "PostToolUse"),
|
||||
"future_provider_field": {"preserved": True},
|
||||
}
|
||||
assert posts[1]["json"]["payload"] == {
|
||||
**official_input(fixture, "PostToolUseFailure"),
|
||||
"future_provider_field": {"preserved": True},
|
||||
}
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
def test_audit_failure_is_observable_and_fails_closed(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
fixture.install()
|
||||
audit = assert_managed_triple(fixture.settings())["audit"]
|
||||
result = run_handler(
|
||||
audit,
|
||||
_payload(official_input(fixture, "PostToolUse")),
|
||||
fixture,
|
||||
)
|
||||
assert result.returncode == 1
|
||||
assert b"Elydora audit" in result.stderr
|
||||
error_log = fixture.agent_dir / "error.log"
|
||||
source = error_log.read_text(encoding="utf-8")
|
||||
assert "[elydora-hook]" in source
|
||||
assert "URLError" in source
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode bits apply on Unix")
|
||||
def test_runtime_artifacts_use_private_modes(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
fixture.install()
|
||||
for file_path, expected in (
|
||||
(fixture.runtime_config_path, 0o600),
|
||||
(fixture.private_key_path, 0o600),
|
||||
(fixture.guard_path, 0o700),
|
||||
(fixture.audit_path, 0o700),
|
||||
(fixture.global_path, 0o600),
|
||||
):
|
||||
assert file_path.stat().st_mode & 0o777 == expected
|
||||
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from elydora.plugins import _transaction
|
||||
from elydora.plugins.letta_config import render_letta_document
|
||||
from elydora.plugins.letta_contract import build_letta_group
|
||||
from elydora.plugins.letta_installation import (
|
||||
commit_letta_installation,
|
||||
preflight_letta_installation,
|
||||
prepare_letta_installation,
|
||||
)
|
||||
from elydora.plugins.letta_sources import read_letta_sources
|
||||
from letta_support import prepare_fixture, write_json
|
||||
|
||||
|
||||
def _groups(guard_path: str, audit_path: str) -> dict:
|
||||
return {
|
||||
"PreToolUse": build_letta_group(guard_path),
|
||||
"PostToolUse": build_letta_group(audit_path),
|
||||
"PostToolUseFailure": build_letta_group(audit_path),
|
||||
}
|
||||
|
||||
|
||||
def test_transaction_aborts_when_read_only_source_changes(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch, tmp_path, project_settings={"owner": "before"}
|
||||
)
|
||||
sources = read_letta_sources()
|
||||
paths = preflight_letta_installation(fixture.config, sources)
|
||||
rendered = render_letta_document(
|
||||
sources.global_settings,
|
||||
None,
|
||||
_groups(paths.guard_path, paths.audit_path),
|
||||
)
|
||||
changes = prepare_letta_installation(fixture.config, paths, rendered)
|
||||
write_json(fixture.project_path, {"owner": "after"})
|
||||
with pytest.raises(OSError, match="project settings changed"):
|
||||
commit_letta_installation(changes, sources)
|
||||
assert json.loads(fixture.project_path.read_text()) == {"owner": "after"}
|
||||
assert not fixture.global_path.exists()
|
||||
assert not fixture.agent_dir.exists()
|
||||
|
||||
|
||||
def test_transaction_rolls_back_every_runtime_and_settings_change(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch, tmp_path, global_settings={"owner": "user"}
|
||||
)
|
||||
original = fixture.source()
|
||||
real_replace = _transaction.os.replace
|
||||
failed = False
|
||||
|
||||
def fail_settings_commit(source: Any, destination: Any) -> None:
|
||||
nonlocal failed
|
||||
if not failed and Path(destination) == fixture.global_path:
|
||||
failed = True
|
||||
raise OSError("simulated Letta settings commit failure")
|
||||
real_replace(source, destination)
|
||||
|
||||
monkeypatch.setattr(_transaction.os, "replace", fail_settings_commit)
|
||||
with pytest.raises(OSError, match="Install Letta Code hooks"):
|
||||
fixture.install()
|
||||
assert failed
|
||||
assert fixture.source() == original
|
||||
for file_path in (
|
||||
fixture.guard_path,
|
||||
fixture.audit_path,
|
||||
fixture.runtime_config_path,
|
||||
fixture.private_key_path,
|
||||
):
|
||||
assert not file_path.exists()
|
||||
assert [
|
||||
path
|
||||
for path in fixture.home_dir.rglob("*")
|
||||
if path.suffix in {".tmp", ".rollback"}
|
||||
] == []
|
||||
|
||||
|
||||
def test_install_rejects_linked_configuration_directory(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
redirected = tmp_path / "redirected-letta"
|
||||
redirected.mkdir()
|
||||
fixture.home_dir.mkdir(parents=True)
|
||||
try:
|
||||
os.symlink(redirected, fixture.global_path.parent, target_is_directory=True)
|
||||
except (NotImplementedError, OSError):
|
||||
pytest.skip("directory links require platform privileges")
|
||||
with pytest.raises(OSError, match="global configuration directory"):
|
||||
fixture.install()
|
||||
assert not fixture.agent_dir.exists()
|
||||
|
||||
|
||||
def test_install_rejects_directory_at_settings_path(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
fixture.global_path.mkdir(parents=True)
|
||||
with pytest.raises(OSError, match="global settings path"):
|
||||
fixture.install()
|
||||
assert not fixture.agent_dir.exists()
|
||||
@@ -1,15 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from elydora.plugins.letta import _is_elydora_hook as is_letta_hook
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from elydora.plugins.letta_command import build_letta_command
|
||||
from elydora.plugins.letta_contract import managed_letta_removals
|
||||
|
||||
|
||||
def test_hook_detection_handles_only_string_commands() -> None:
|
||||
assert is_letta_hook({"command": 42}, "") is False
|
||||
assert is_letta_hook({"hooks": [{"command": None}]}, "") is False
|
||||
assert is_letta_hook(
|
||||
{"hooks": [{"command": "run .elydora/agent-1/hook.py"}]}, ""
|
||||
) is True
|
||||
assert is_letta_hook(
|
||||
{"hooks": [{"command": "run .elydora/agent-1/hook.py"}]},
|
||||
"agent-2",
|
||||
) is False
|
||||
def test_letta_hook_detection_requires_exact_owned_commands(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
home = tmp_path / "home"
|
||||
guard = home / ".elydora" / "agent-1" / "guard.py"
|
||||
monkeypatch.setenv("HOME", str(home))
|
||||
monkeypatch.setenv("USERPROFILE", str(home))
|
||||
hooks = {
|
||||
"PreToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": build_letta_command(str(guard)),
|
||||
"timeout": 10_000,
|
||||
}],
|
||||
}],
|
||||
}
|
||||
assert len(managed_letta_removals(hooks)) == 1
|
||||
|
||||
hooks["PreToolUse"][0]["hooks"][0]["quiet"] = True
|
||||
assert managed_letta_removals(hooks) == []
|
||||
|
||||
hooks["PreToolUse"][0]["hooks"][0] = {
|
||||
"type": "command",
|
||||
"command": f'"{sys.executable}" {guard}',
|
||||
}
|
||||
assert len(managed_letta_removals(hooks, "agent-1")) == 1
|
||||
assert managed_letta_removals(hooks, "agent-2") == []
|
||||
Reference in new issue
Block a user