Rebuilt from the standalone 54e65eb tree (durable transactions, Kiro IDE adapter, hook retry with a bounded chain lock, version 2.0.2) with the Open-Source API trims re-applied: no token rotation, token issuance, or agent integration updates, and the mirror's registration contract and OpenCode plugin tests kept. Claude-Session: https://claude.ai/code/session_01QoUjdc7HGKTsgj5GhG8HvF
1488 lines
50 KiB
Python
1488 lines
50 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import copy
|
|
from dataclasses import dataclass
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import stat
|
|
import sys
|
|
import threading
|
|
import time
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from elydora import cli
|
|
from elydora.plugins import kiroide
|
|
from elydora.plugins import (
|
|
_transaction,
|
|
kiroide_command,
|
|
kiroide_contract,
|
|
kiroide_installation,
|
|
kiroide_io,
|
|
)
|
|
from elydora.plugins.base import InstallConfig
|
|
from elydora.plugins.registry import SUPPORTED_AGENTS
|
|
|
|
|
|
AGENT_ID = "agent-1"
|
|
PRIVATE_KEY = base64.urlsafe_b64encode(bytes([11]) * 32).rstrip(b"=").decode()
|
|
ROTATED_PRIVATE_KEY = (
|
|
base64.urlsafe_b64encode(bytes([12]) * 32).rstrip(b"=").decode()
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class KiroIdeFixture:
|
|
home: Path
|
|
workspace: Path
|
|
config_path: Path
|
|
legacy_path: Path
|
|
agent_directory: Path
|
|
guard_path: Path
|
|
audit_path: Path
|
|
plugin: kiroide.KiroIdePlugin
|
|
config: InstallConfig
|
|
|
|
|
|
def _write_json(path: Path, value: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
|
|
|
|
|
|
def _install_config(home: Path) -> InstallConfig:
|
|
return {
|
|
"org_id": "org-1",
|
|
"agent_id": AGENT_ID,
|
|
"agent_name": "kiroide",
|
|
"private_key": PRIVATE_KEY,
|
|
"kid": "kid-1",
|
|
"token": "token-1",
|
|
"base_url": "https://api.elydora.test",
|
|
"guard_script_path": str(home / ".elydora" / AGENT_ID / "guard.py"),
|
|
}
|
|
|
|
|
|
def _prepare_fixture(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
*,
|
|
current: Any = None,
|
|
legacy: Any = None,
|
|
base_url: str = "https://api.elydora.test",
|
|
) -> KiroIdeFixture:
|
|
home = tmp_path / "home with spaces and 'quote %KIRO%"
|
|
workspace = tmp_path / "workspace with spaces"
|
|
home.mkdir(parents=True)
|
|
workspace.mkdir(parents=True)
|
|
config_path = workspace / ".kiro" / "hooks" / "elydora-audit.json"
|
|
legacy_path = home / ".kiro" / "hooks" / "elydora-audit.kiro.hook"
|
|
if current is not None:
|
|
if isinstance(current, str):
|
|
config_path.parent.mkdir(parents=True)
|
|
config_path.write_text(current, encoding="utf-8")
|
|
else:
|
|
_write_json(config_path, current)
|
|
if legacy is not None:
|
|
if isinstance(legacy, str):
|
|
legacy_path.parent.mkdir(parents=True)
|
|
legacy_path.write_text(legacy, encoding="utf-8")
|
|
else:
|
|
_write_json(legacy_path, legacy)
|
|
monkeypatch.setenv("HOME", str(home))
|
|
monkeypatch.setenv("USERPROFILE", str(home))
|
|
monkeypatch.chdir(workspace)
|
|
agent_directory = home / ".elydora" / AGENT_ID
|
|
config = _install_config(home)
|
|
config["base_url"] = base_url
|
|
return KiroIdeFixture(
|
|
home,
|
|
workspace,
|
|
config_path,
|
|
legacy_path,
|
|
agent_directory,
|
|
agent_directory / "guard.py",
|
|
agent_directory / "hook.py",
|
|
kiroide.KiroIdePlugin(),
|
|
config,
|
|
)
|
|
|
|
|
|
def _find_hook(document: dict[str, Any], name: str) -> dict[str, Any]:
|
|
return next(hook for hook in document["hooks"] if hook.get("name") == name)
|
|
|
|
|
|
def _managed_document(guard_command: str, audit_command: str) -> dict[str, Any]:
|
|
return {
|
|
"version": "v1",
|
|
"hooks": [
|
|
{
|
|
"name": "elydora-guard",
|
|
"description": "Block tool use when the Elydora agent is frozen",
|
|
"trigger": "PreToolUse",
|
|
"matcher": ".*",
|
|
"action": {"type": "command", "command": guard_command},
|
|
"timeout": 10,
|
|
"enabled": True,
|
|
},
|
|
{
|
|
"name": "elydora-audit",
|
|
"description": "Record tool use in the Elydora audit trail",
|
|
"trigger": "PostToolUse",
|
|
"matcher": ".*",
|
|
"action": {"type": "command", "command": audit_command},
|
|
"timeout": 10,
|
|
"enabled": True,
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _legacy_document(fixture: KiroIdeFixture, agent_id: str = AGENT_ID) -> dict:
|
|
agent_directory = fixture.home / ".elydora" / agent_id
|
|
return {
|
|
"name": "Elydora Audit",
|
|
"description": (
|
|
"Sends tool-use events to the Elydora tamper-evident audit platform"
|
|
),
|
|
"version": "1.0.0",
|
|
"hooks": {
|
|
"pre_tool_use": {
|
|
"command": f'"{sys.executable}" {agent_directory / "guard.py"}',
|
|
"timeout_ms": 5000,
|
|
},
|
|
"post_tool_use": {
|
|
"command": str(agent_directory / "hook.py"),
|
|
"timeout_ms": 5000,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _legacy_runtime_config(
|
|
fixture: KiroIdeFixture, agent_id: str = AGENT_ID
|
|
) -> dict[str, str]:
|
|
return {
|
|
"org_id": "org-1",
|
|
"agent_id": agent_id,
|
|
"kid": "kid-1",
|
|
"base_url": "https://api.elydora.test",
|
|
"token": "",
|
|
"agent_name": "kiroide",
|
|
}
|
|
|
|
|
|
def _write_legacy_runtime(fixture: KiroIdeFixture, agent_id: str) -> Path:
|
|
agent_directory = fixture.home / ".elydora" / agent_id
|
|
_write_json(
|
|
agent_directory / "config.json",
|
|
_legacy_runtime_config(fixture, agent_id),
|
|
)
|
|
(agent_directory / "private.key").write_text(PRIVATE_KEY, encoding="utf-8")
|
|
(agent_directory / "guard.py").write_text("guard\n", encoding="utf-8")
|
|
(agent_directory / "hook.py").write_text("audit\n", encoding="utf-8")
|
|
return agent_directory
|
|
|
|
|
|
def _write_large_error_log(agent_directory: Path, marker: bytes) -> Path:
|
|
error_log = agent_directory / "error.log"
|
|
with error_log.open("wb") as file:
|
|
for _index in range(48):
|
|
file.write(marker * 4096)
|
|
if os.name != "nt":
|
|
error_log.chmod(0o600)
|
|
assert error_log.stat().st_size > 2 * 1024 * 1024
|
|
return error_log
|
|
|
|
|
|
def _run_command(
|
|
command: str, fixture: KiroIdeFixture, payload: dict[str, Any]
|
|
) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
command,
|
|
shell=True,
|
|
cwd=fixture.workspace,
|
|
env={
|
|
**os.environ,
|
|
"HOME": str(fixture.home),
|
|
"USERPROFILE": str(fixture.home),
|
|
},
|
|
input=json.dumps(payload),
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
|
|
|
|
class _AuditHandler(BaseHTTPRequestHandler):
|
|
operations: list[dict[str, Any]] = []
|
|
|
|
def do_POST(self) -> None:
|
|
length = int(self.headers.get("Content-Length", "0"))
|
|
raw = self.rfile.read(length).decode("utf-8")
|
|
self.operations.append(json.loads(raw))
|
|
self.send_response(201)
|
|
self.end_headers()
|
|
|
|
def log_message(self, _format: str, *_args: object) -> None:
|
|
return
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _AuditServer:
|
|
server: ThreadingHTTPServer
|
|
thread: threading.Thread
|
|
|
|
@property
|
|
def base_url(self) -> str:
|
|
host, port = self.server.server_address
|
|
return f"http://{host}:{port}"
|
|
|
|
def close(self) -> None:
|
|
self.server.shutdown()
|
|
self.server.server_close()
|
|
self.thread.join()
|
|
|
|
|
|
def _start_audit_server() -> _AuditServer:
|
|
_AuditHandler.operations = []
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), _AuditHandler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
return _AuditServer(server, thread)
|
|
|
|
|
|
def _prepare_installation(
|
|
fixture: KiroIdeFixture,
|
|
config: InstallConfig | None = None,
|
|
) -> tuple[
|
|
kiroide_io.KiroIdeSources,
|
|
kiroide_installation.KiroIdeRuntimePaths,
|
|
kiroide_installation.PreparedKiroIdeInstallation,
|
|
kiroide_contract.RenderedKiroIdeDocument,
|
|
]:
|
|
selected = fixture.config if config is None else config
|
|
sources = kiroide_io.read_kiroide_sources()
|
|
kiroide_contract.require_available_kiroide_hooks(sources.document.hooks)
|
|
paths = kiroide_installation.preflight_kiroide_installation(
|
|
selected, sources
|
|
)
|
|
hooks = kiroide_contract.without_managed_kiroide_hooks(
|
|
sources.document.hooks
|
|
)
|
|
rendered = kiroide_contract.render_kiroide_document(
|
|
sources.document,
|
|
[
|
|
*hooks,
|
|
kiroide_contract.build_kiroide_hook(
|
|
kiroide_contract.GUARD_NAME, paths.guard_path
|
|
),
|
|
kiroide_contract.build_kiroide_hook(
|
|
kiroide_contract.AUDIT_NAME, paths.audit_path
|
|
),
|
|
],
|
|
)
|
|
prepared = kiroide_installation.prepare_kiroide_installation(
|
|
selected, paths, sources, rendered
|
|
)
|
|
return sources, paths, prepared, rendered
|
|
|
|
|
|
def test_kiroide_uses_current_workspace_v1_hook_contract(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
home = tmp_path / "home with spaces"
|
|
workspace = tmp_path / "workspace with spaces"
|
|
home.mkdir()
|
|
workspace.mkdir()
|
|
config_path = workspace / ".kiro" / "hooks" / "elydora-audit.json"
|
|
existing_hook = {
|
|
"name": "workspace-context",
|
|
"trigger": "SessionStart",
|
|
"action": {"type": "agent", "prompt": "Read AGENTS.md"},
|
|
"enabled": True,
|
|
}
|
|
_write_json(
|
|
config_path,
|
|
{"version": "v1", "owner": "workspace", "hooks": [existing_hook]},
|
|
)
|
|
monkeypatch.setenv("HOME", str(home))
|
|
monkeypatch.setenv("USERPROFILE", str(home))
|
|
monkeypatch.chdir(workspace)
|
|
|
|
plugin = kiroide.KiroIdePlugin()
|
|
plugin.install(_install_config(home))
|
|
first_source = config_path.read_text(encoding="utf-8")
|
|
plugin.install(_install_config(home))
|
|
|
|
assert SUPPORTED_AGENTS["kiroide"] == {
|
|
"name": "Kiro IDE",
|
|
"hook_event": "PreToolUse/PostToolUse",
|
|
"config_path": ".kiro/hooks/elydora-audit.json",
|
|
}
|
|
assert config_path.read_text(encoding="utf-8") == first_source
|
|
document = json.loads(first_source)
|
|
assert document["version"] == "v1"
|
|
assert document["owner"] == "workspace"
|
|
assert document["hooks"][0] == existing_hook
|
|
assert document["hooks"][1]["trigger"] == "PreToolUse"
|
|
assert document["hooks"][2]["trigger"] == "PostToolUse"
|
|
assert all(hook["matcher"] == ".*" for hook in document["hooks"][1:])
|
|
assert all(hook["timeout"] == 10 for hook in document["hooks"][1:])
|
|
assert all(hook["enabled"] is True for hook in document["hooks"][1:])
|
|
assert not (home / ".kiro" / "hooks" / "elydora-audit.json").exists()
|
|
private_key_path = home / ".elydora" / AGENT_ID / "private.key"
|
|
audit_path = home / ".elydora" / AGENT_ID / "hook.py"
|
|
assert private_key_path.read_text(encoding="utf-8") == PRIVATE_KEY
|
|
assert PRIVATE_KEY not in audit_path.read_text(encoding="utf-8")
|
|
if os.name != "nt":
|
|
assert private_key_path.stat().st_mode & 0o777 == 0o600
|
|
assert audit_path.stat().st_mode & 0o777 == 0o700
|
|
|
|
|
|
def test_kiroide_commands_block_frozen_agents_and_forward_native_events(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
api = _start_audit_server()
|
|
try:
|
|
fixture = _prepare_fixture(
|
|
monkeypatch, tmp_path, base_url=api.base_url
|
|
)
|
|
fixture.plugin.install(fixture.config)
|
|
document = json.loads(fixture.config_path.read_text(encoding="utf-8"))
|
|
payload = {
|
|
"hook_event_name": "preToolUse",
|
|
"cwd": str(fixture.workspace),
|
|
"session_id": "session-1",
|
|
"tool_name": "execute_bash",
|
|
"tool_input": {"command": "pytest -q"},
|
|
"future_field": {"retained": True},
|
|
}
|
|
status_cache_path = fixture.agent_directory / "status-cache.json"
|
|
_write_json(
|
|
status_cache_path,
|
|
{"status": "frozen", "cached_at": time.time()},
|
|
)
|
|
if os.name != "nt":
|
|
status_cache_path.chmod(0o600)
|
|
|
|
blocked = _run_command(
|
|
_find_hook(document, "elydora-guard")["action"]["command"],
|
|
fixture,
|
|
payload,
|
|
)
|
|
assert blocked.returncode == 2
|
|
assert "Tool execution blocked" in blocked.stderr
|
|
|
|
payload["hook_event_name"] = "postToolUse"
|
|
payload["tool_response"] = {"success": True, "result": "12 passed"}
|
|
audited = _run_command(
|
|
_find_hook(document, "elydora-audit")["action"]["command"],
|
|
fixture,
|
|
payload,
|
|
)
|
|
assert audited.returncode == 0, audited.stderr
|
|
assert _AuditHandler.operations[-1]["payload"] == payload
|
|
assert _AuditHandler.operations[-1]["action"] == {
|
|
"tool": "execute_bash"
|
|
}
|
|
assert _AuditHandler.operations[-1]["subject"] == {
|
|
"session_id": "session-1"
|
|
}
|
|
finally:
|
|
api.close()
|
|
|
|
|
|
def test_kiroide_status_requires_exact_enabled_hooks_and_runtime_sources(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
assert fixture.plugin.status() == {
|
|
"installed": True,
|
|
"agent": "kiroide",
|
|
"details": f"Config: {fixture.config_path}",
|
|
}
|
|
|
|
document = json.loads(fixture.config_path.read_text(encoding="utf-8"))
|
|
_find_hook(document, "elydora-guard")["enabled"] = False
|
|
_write_json(fixture.config_path, document)
|
|
assert fixture.plugin.status() == {
|
|
"installed": False,
|
|
"agent": "kiroide",
|
|
"details": (
|
|
"Managed hooks are incomplete or invalid: "
|
|
f"{fixture.config_path}"
|
|
),
|
|
}
|
|
|
|
fixture.plugin.install(fixture.config)
|
|
fixture.guard_path.write_text("tampered\n", encoding="utf-8")
|
|
status = fixture.plugin.status()
|
|
assert status["installed"] is False
|
|
assert "runtime files are missing or invalid" in status["details"]
|
|
|
|
|
|
def test_kiroide_status_surfaces_malformed_runtime_metadata(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
(fixture.agent_directory / "config.json").write_text(
|
|
"{ malformed", encoding="utf-8"
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="parse Elydora runtime config"):
|
|
fixture.plugin.status()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("source", "message"),
|
|
[
|
|
("{ malformed", "parse Kiro IDE hooks"),
|
|
(
|
|
'{"version":"v1","version":"v1","hooks":[]}',
|
|
'duplicate field "version"',
|
|
),
|
|
({"version": "v0", "hooks": []}, 'version must be "v1"'),
|
|
({"version": "v1", "hooks": None}, 'field "hooks" must be an array'),
|
|
(
|
|
{
|
|
"version": "v1",
|
|
"hooks": [
|
|
{
|
|
"name": "bad",
|
|
"trigger": "FutureEvent",
|
|
"action": {"type": "command", "command": "echo bad"},
|
|
}
|
|
],
|
|
},
|
|
"unsupported trigger",
|
|
),
|
|
],
|
|
)
|
|
def test_kiroide_rejects_invalid_workspace_hooks_before_runtime_writes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
source: Any,
|
|
message: str,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path, current=source)
|
|
original = fixture.config_path.read_text(encoding="utf-8")
|
|
|
|
with pytest.raises(ValueError, match=message):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert fixture.config_path.read_text(encoding="utf-8") == original
|
|
assert not fixture.agent_directory.exists()
|
|
|
|
|
|
def test_kiroide_cli_preflight_rejects_workspace_before_runtime_creation(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path, current="{ malformed")
|
|
private_key_file = tmp_path / "private.key"
|
|
token_file = tmp_path / "token"
|
|
private_key_file.write_text(PRIVATE_KEY, encoding="utf-8")
|
|
token_file.write_text("token-1", encoding="utf-8")
|
|
if os.name != "nt":
|
|
private_key_file.chmod(0o600)
|
|
token_file.chmod(0o600)
|
|
args = cli.build_parser().parse_args(
|
|
[
|
|
"install",
|
|
"--agent",
|
|
"kiroide",
|
|
"--org_id",
|
|
"org-1",
|
|
"--agent_id",
|
|
AGENT_ID,
|
|
"--private_key_file",
|
|
str(private_key_file),
|
|
"--token_file",
|
|
str(token_file),
|
|
"--kid",
|
|
"kid-1",
|
|
]
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="parse Kiro IDE hooks"):
|
|
cli.cmd_install(args)
|
|
|
|
assert fixture.config_path.read_text(encoding="utf-8") == "{ malformed"
|
|
assert not fixture.agent_directory.exists()
|
|
|
|
|
|
def test_kiroide_rejects_noncanonical_private_keys_before_writes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.config["private_key"] = PRIVATE_KEY + "="
|
|
|
|
with pytest.raises(ValueError, match="canonical 32-byte base64url"):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert not fixture.config_path.exists()
|
|
assert not fixture.agent_directory.exists()
|
|
|
|
|
|
def test_kiroide_rejects_managed_name_collisions(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
current = {
|
|
"version": "v1",
|
|
"hooks": [
|
|
{
|
|
"name": "elydora-guard",
|
|
"trigger": "PreToolUse",
|
|
"action": {"type": "command", "command": "user-command"},
|
|
}
|
|
],
|
|
}
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path, current=current)
|
|
|
|
with pytest.raises(ValueError, match="conflicts with the Elydora contract"):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert json.loads(fixture.config_path.read_text(encoding="utf-8")) == current
|
|
assert not fixture.agent_directory.exists()
|
|
|
|
|
|
def test_kiroide_rejects_linked_workspace_configuration_directory(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
external = tmp_path / "external-kiro"
|
|
external.mkdir()
|
|
try:
|
|
(fixture.workspace / ".kiro").symlink_to(external, target_is_directory=True)
|
|
except OSError as error:
|
|
pytest.skip(f"Directory symbolic links are unavailable: {error}")
|
|
|
|
with pytest.raises(OSError, match="not a physical directory"):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert not (external / "hooks" / "elydora-audit.json").exists()
|
|
assert not fixture.agent_directory.exists()
|
|
|
|
|
|
def test_kiroide_migrates_only_the_matching_legacy_python_hook(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture))
|
|
|
|
fixture.plugin.install(fixture.config)
|
|
assert not fixture.legacy_path.exists()
|
|
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture, "agent-2"))
|
|
fixture.plugin.install(fixture.config)
|
|
assert fixture.legacy_path.exists()
|
|
|
|
|
|
def test_kiroide_preserves_unrelated_legacy_hook_and_rejects_malformed_legacy(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
unrelated = {
|
|
"name": "User Hook",
|
|
"version": "1.0.0",
|
|
"hooks": {"post_tool_use": {"command": "user-command"}},
|
|
}
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path, legacy=unrelated)
|
|
fixture.plugin.install(fixture.config)
|
|
assert json.loads(fixture.legacy_path.read_text(encoding="utf-8")) == unrelated
|
|
|
|
fixture.plugin.uninstall(AGENT_ID)
|
|
fixture.legacy_path.write_text("{ malformed", encoding="utf-8")
|
|
with pytest.raises(ValueError, match="parse legacy Kiro IDE hook"):
|
|
fixture.plugin.install(fixture.config)
|
|
assert fixture.legacy_path.read_text(encoding="utf-8") == "{ malformed"
|
|
|
|
|
|
def test_kiroide_uninstall_removes_exact_ownership_and_preserves_user_hooks(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
user_hook = {
|
|
"name": "workspace-context",
|
|
"trigger": "SessionStart",
|
|
"action": {"type": "agent", "prompt": "Read AGENTS.md"},
|
|
}
|
|
fixture = _prepare_fixture(
|
|
monkeypatch,
|
|
tmp_path,
|
|
current={"version": "v1", "owner": "workspace", "hooks": [user_hook]},
|
|
)
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
fixture.plugin.uninstall("agent-2")
|
|
assert len(json.loads(fixture.config_path.read_text())["hooks"]) == 3
|
|
assert fixture.agent_directory.exists()
|
|
assert (fixture.agent_directory / "private.key").read_text(
|
|
encoding="utf-8"
|
|
) == PRIVATE_KEY
|
|
fixture.plugin.uninstall(AGENT_ID)
|
|
assert json.loads(fixture.config_path.read_text(encoding="utf-8")) == {
|
|
"version": "v1",
|
|
"owner": "workspace",
|
|
"hooks": [user_hook],
|
|
}
|
|
|
|
owned = _prepare_fixture(monkeypatch, tmp_path / "owned")
|
|
owned.plugin.install(owned.config)
|
|
owned.plugin.uninstall(AGENT_ID)
|
|
assert not owned.config_path.exists()
|
|
|
|
|
|
def test_kiroide_uninstall_without_agent_id_removes_owned_runtime(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
fixture.plugin.uninstall()
|
|
|
|
assert not fixture.config_path.exists()
|
|
assert not fixture.agent_directory.exists()
|
|
assert not (fixture.agent_directory / "private.key").exists()
|
|
|
|
|
|
def test_kiroide_uninstall_without_agent_id_removes_current_and_legacy_runtimes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
legacy_agent_id = "agent-2"
|
|
legacy_directory = _write_legacy_runtime(fixture, legacy_agent_id)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture, legacy_agent_id))
|
|
|
|
fixture.plugin.uninstall()
|
|
|
|
assert not fixture.config_path.exists()
|
|
assert not fixture.legacy_path.exists()
|
|
assert not fixture.agent_directory.exists()
|
|
assert not legacy_directory.exists()
|
|
assert not (legacy_directory / "private.key").exists()
|
|
|
|
|
|
def test_kiroide_no_id_uninstall_streams_all_oversized_error_logs(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
current_error = _write_large_error_log(
|
|
fixture.agent_directory,
|
|
b"current runtime error\n",
|
|
)
|
|
legacy_agent_id = "agent-2"
|
|
legacy_directory = _write_legacy_runtime(fixture, legacy_agent_id)
|
|
legacy_error = _write_large_error_log(
|
|
legacy_directory,
|
|
b"legacy runtime error\n",
|
|
)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture, legacy_agent_id))
|
|
|
|
fixture.plugin.uninstall()
|
|
|
|
assert not current_error.exists()
|
|
assert not legacy_error.exists()
|
|
assert not fixture.agent_directory.exists()
|
|
assert not legacy_directory.exists()
|
|
|
|
|
|
def test_kiroide_uninstall_without_agent_id_deduplicates_current_and_legacy_owner(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture))
|
|
|
|
fixture.plugin.uninstall()
|
|
|
|
assert not fixture.config_path.exists()
|
|
assert not fixture.legacy_path.exists()
|
|
assert not fixture.agent_directory.exists()
|
|
|
|
|
|
def test_kiroide_uninstall_without_agent_id_rejects_unknown_runtime_before_writes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
legacy_agent_id = "agent-2"
|
|
legacy_directory = _write_legacy_runtime(fixture, legacy_agent_id)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture, legacy_agent_id))
|
|
unknown = legacy_directory / "user-data.txt"
|
|
unknown.write_text("preserve\n", encoding="utf-8")
|
|
original_provider = fixture.config_path.read_bytes()
|
|
original_legacy = fixture.legacy_path.read_bytes()
|
|
original_key = (fixture.agent_directory / "private.key").read_bytes()
|
|
original_legacy_key = (legacy_directory / "private.key").read_bytes()
|
|
|
|
with pytest.raises(OSError, match="contains unmanaged entries"):
|
|
fixture.plugin.uninstall()
|
|
|
|
assert fixture.config_path.read_bytes() == original_provider
|
|
assert fixture.legacy_path.read_bytes() == original_legacy
|
|
assert (fixture.agent_directory / "private.key").read_bytes() == original_key
|
|
assert (legacy_directory / "private.key").read_bytes() == original_legacy_key
|
|
assert unknown.read_text(encoding="utf-8") == "preserve\n"
|
|
|
|
|
|
def test_kiroide_uninstall_without_agent_id_rolls_back_provider_and_runtime(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
runtime_paths = [
|
|
fixture.guard_path,
|
|
fixture.audit_path,
|
|
fixture.agent_directory / "private.key",
|
|
fixture.agent_directory / "config.json",
|
|
]
|
|
original_provider = fixture.config_path.read_bytes()
|
|
original_runtime = {path: path.read_bytes() for path in runtime_paths}
|
|
original_capture = _transaction._capture_physical
|
|
|
|
def fail_private_key_removal(
|
|
source: str,
|
|
destination: str,
|
|
directory: _transaction.PinnedDirectory,
|
|
) -> None:
|
|
if Path(source) == fixture.agent_directory / "private.key" and (
|
|
destination.endswith(".rollback")
|
|
):
|
|
raise OSError("injected private key removal failure")
|
|
original_capture(source, destination, directory)
|
|
|
|
monkeypatch.setattr(_transaction, "_capture_physical", fail_private_key_removal)
|
|
|
|
with pytest.raises(OSError, match="injected private key removal failure"):
|
|
fixture.plugin.uninstall()
|
|
|
|
assert fixture.config_path.read_bytes() == original_provider
|
|
assert {path: path.read_bytes() for path in runtime_paths} == original_runtime
|
|
|
|
|
|
def test_kiroide_transaction_rolls_back_when_legacy_cleanup_fails(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
original = {"version": "v1", "owner": "workspace", "hooks": []}
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path, current=original)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture))
|
|
original_capture = _transaction._capture_physical
|
|
|
|
def fail_legacy_removal(
|
|
source: str,
|
|
destination: str,
|
|
directory: _transaction.PinnedDirectory,
|
|
) -> None:
|
|
if Path(source) == fixture.legacy_path and destination.endswith(".rollback"):
|
|
raise OSError("injected legacy cleanup failure")
|
|
original_capture(source, destination, directory)
|
|
|
|
monkeypatch.setattr(_transaction, "_capture_physical", fail_legacy_removal)
|
|
|
|
with pytest.raises(OSError, match="injected legacy cleanup failure"):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert json.loads(fixture.config_path.read_text(encoding="utf-8")) == original
|
|
assert fixture.legacy_path.exists()
|
|
for path in (
|
|
fixture.guard_path,
|
|
fixture.audit_path,
|
|
fixture.agent_directory / "config.json",
|
|
fixture.agent_directory / "private.key",
|
|
):
|
|
assert not path.exists()
|
|
leftovers = list(fixture.config_path.parent.glob(".*.tmp"))
|
|
leftovers += list(fixture.config_path.parent.glob(".*.rollback"))
|
|
assert leftovers == []
|
|
|
|
|
|
@pytest.mark.parametrize("swapped", ["workspace", "kiro", "hooks"])
|
|
def test_kiroide_transaction_rejects_provider_directory_identity_swaps(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
swapped: str,
|
|
) -> None:
|
|
fixture = _prepare_fixture(
|
|
monkeypatch,
|
|
tmp_path,
|
|
current={"version": "v1", "hooks": []},
|
|
)
|
|
sources, paths, prepared, _rendered = _prepare_installation(fixture)
|
|
monkeypatch.chdir(tmp_path)
|
|
original_paths = {
|
|
"workspace": fixture.workspace,
|
|
"kiro": fixture.workspace / ".kiro",
|
|
"hooks": fixture.workspace / ".kiro" / "hooks",
|
|
}
|
|
original = original_paths[swapped]
|
|
backup = original.with_name(original.name + "-original")
|
|
replacement = tmp_path / f"redirected-{swapped}"
|
|
if swapped == "workspace":
|
|
(replacement / ".kiro" / "hooks").mkdir(parents=True)
|
|
redirected_config = replacement / ".kiro" / "hooks" / "elydora-audit.json"
|
|
elif swapped == "kiro":
|
|
(replacement / "hooks").mkdir(parents=True)
|
|
redirected_config = replacement / "hooks" / "elydora-audit.json"
|
|
else:
|
|
replacement.mkdir()
|
|
redirected_config = replacement / "elydora-audit.json"
|
|
original.rename(backup)
|
|
try:
|
|
original.symlink_to(replacement, target_is_directory=True)
|
|
except OSError as error:
|
|
backup.rename(original)
|
|
pytest.skip(f"Directory symbolic links are unavailable: {error}")
|
|
|
|
with pytest.raises(OSError, match="not a physical directory|changed during"):
|
|
kiroide_installation.commit_kiroide_installation(
|
|
prepared, sources, paths
|
|
)
|
|
|
|
assert not redirected_config.exists()
|
|
assert not fixture.agent_directory.exists()
|
|
|
|
|
|
def test_kiroide_key_rotation_pins_unchanged_provider_sources(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
original_key = (fixture.agent_directory / "private.key").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
rotated = InstallConfig(**fixture.config)
|
|
rotated["private_key"] = ROTATED_PRIVATE_KEY
|
|
sources, paths, prepared, rendered = _prepare_installation(fixture, rotated)
|
|
assert rendered.changed is False
|
|
document = json.loads(fixture.config_path.read_text(encoding="utf-8"))
|
|
document["concurrent_change"] = True
|
|
_write_json(fixture.config_path, document)
|
|
|
|
with pytest.raises(OSError, match="Kiro IDE hooks changed during"):
|
|
kiroide_installation.commit_kiroide_installation(
|
|
prepared, sources, paths
|
|
)
|
|
|
|
assert (fixture.agent_directory / "private.key").read_text(
|
|
encoding="utf-8"
|
|
) == original_key
|
|
assert json.loads(fixture.config_path.read_text(encoding="utf-8"))[
|
|
"concurrent_change"
|
|
] is True
|
|
|
|
|
|
def test_kiroide_noop_install_rejects_concurrent_provider_changes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
sources, paths, prepared, rendered = _prepare_installation(fixture)
|
|
assert prepared.changes == []
|
|
assert rendered.changed is False
|
|
document = json.loads(fixture.config_path.read_text(encoding="utf-8"))
|
|
_find_hook(document, "elydora-guard")["enabled"] = False
|
|
_write_json(fixture.config_path, document)
|
|
|
|
with pytest.raises(OSError, match="Kiro IDE hooks changed during"):
|
|
kiroide_installation.commit_kiroide_installation(
|
|
prepared, sources, paths
|
|
)
|
|
|
|
assert _find_hook(
|
|
json.loads(fixture.config_path.read_text(encoding="utf-8")),
|
|
"elydora-guard",
|
|
)["enabled"] is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"relative_path",
|
|
["private.key", "config.json", "guard.py", "hook.py"],
|
|
)
|
|
def test_kiroide_noop_install_pins_every_runtime_source(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
relative_path: str,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
sources, paths, prepared, _rendered = _prepare_installation(fixture)
|
|
assert prepared.changes == []
|
|
assert len(prepared.runtime_preconditions) == 4
|
|
target = fixture.agent_directory / relative_path
|
|
concurrent = target.read_text(encoding="utf-8") + "\nconcurrent-change"
|
|
target.write_text(concurrent, encoding="utf-8")
|
|
|
|
with pytest.raises(OSError, match="changed during Install Kiro IDE hooks"):
|
|
kiroide_installation.commit_kiroide_installation(
|
|
prepared, sources, paths
|
|
)
|
|
|
|
assert target.read_text(encoding="utf-8") == concurrent
|
|
|
|
|
|
def test_kiroide_runtime_config_binds_agent_to_absolute_workspace(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
runtime_config = json.loads(
|
|
(fixture.agent_directory / "config.json").read_text(encoding="utf-8")
|
|
)
|
|
assert runtime_config["workspace_root"] == str(fixture.workspace.resolve())
|
|
assert Path(runtime_config["workspace_root"]).is_absolute()
|
|
|
|
|
|
def test_kiroide_rejects_same_agent_install_from_another_workspace(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
original_hook = fixture.config_path.read_bytes()
|
|
runtime_config_path = fixture.agent_directory / "config.json"
|
|
original_runtime = runtime_config_path.read_bytes()
|
|
second_workspace = tmp_path / "second workspace"
|
|
second_workspace.mkdir()
|
|
monkeypatch.chdir(second_workspace)
|
|
|
|
with pytest.raises(ValueError, match="bound to another workspace"):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert fixture.config_path.read_bytes() == original_hook
|
|
assert runtime_config_path.read_bytes() == original_runtime
|
|
assert not (second_workspace / ".kiro").exists()
|
|
|
|
|
|
def test_kiroide_cli_uninstall_requires_bound_workspace(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
original_hook = fixture.config_path.read_bytes()
|
|
second_workspace = tmp_path / "second workspace"
|
|
second_workspace.mkdir()
|
|
args = cli.build_parser().parse_args(
|
|
[
|
|
"uninstall",
|
|
"--agent",
|
|
"kiroide",
|
|
"--agent_id",
|
|
AGENT_ID,
|
|
]
|
|
)
|
|
monkeypatch.chdir(second_workspace)
|
|
|
|
with pytest.raises(ValueError, match="bound to another workspace"):
|
|
cli.cmd_uninstall(args)
|
|
|
|
assert fixture.config_path.read_bytes() == original_hook
|
|
assert fixture.agent_directory.exists()
|
|
assert not (second_workspace / ".kiro").exists()
|
|
|
|
monkeypatch.chdir(fixture.workspace)
|
|
cli.cmd_uninstall(args)
|
|
assert not fixture.config_path.exists()
|
|
assert not fixture.agent_directory.exists()
|
|
|
|
|
|
def test_kiroide_install_claims_legacy_runtime_owner_from_matching_hooks(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
runtime_config_path = fixture.agent_directory / "config.json"
|
|
runtime_config = json.loads(runtime_config_path.read_text(encoding="utf-8"))
|
|
runtime_config.pop("workspace_root")
|
|
_write_json(runtime_config_path, runtime_config)
|
|
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
claimed = json.loads(runtime_config_path.read_text(encoding="utf-8"))
|
|
assert claimed["workspace_root"] == str(fixture.workspace.resolve())
|
|
|
|
|
|
def test_kiroide_install_migrates_exact_global_legacy_runtime_owner(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture))
|
|
runtime_config_path = fixture.agent_directory / "config.json"
|
|
_write_json(runtime_config_path, _legacy_runtime_config(fixture))
|
|
config = dict(fixture.config)
|
|
config.pop("token")
|
|
|
|
fixture.plugin.install(config)
|
|
|
|
migrated = json.loads(runtime_config_path.read_text(encoding="utf-8"))
|
|
assert migrated["workspace_root"] == str(fixture.workspace.resolve())
|
|
assert "token" not in migrated
|
|
assert fixture.config_path.exists()
|
|
assert not fixture.legacy_path.exists()
|
|
|
|
|
|
def test_kiroide_legacy_owner_migration_rolls_back_runtime_claim(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture))
|
|
runtime_config_path = fixture.agent_directory / "config.json"
|
|
original_runtime = _legacy_runtime_config(fixture)
|
|
_write_json(runtime_config_path, original_runtime)
|
|
original_capture = _transaction._capture_physical
|
|
|
|
def fail_legacy_removal(
|
|
source: str,
|
|
destination: str,
|
|
directory: _transaction.PinnedDirectory,
|
|
) -> None:
|
|
if Path(source) == fixture.legacy_path and destination.endswith(
|
|
".rollback"
|
|
):
|
|
raise OSError("injected legacy migration failure")
|
|
original_capture(source, destination, directory)
|
|
|
|
monkeypatch.setattr(_transaction, "_capture_physical", fail_legacy_removal)
|
|
|
|
with pytest.raises(OSError, match="injected legacy migration failure"):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert json.loads(runtime_config_path.read_text(encoding="utf-8")) == (
|
|
original_runtime
|
|
)
|
|
assert fixture.legacy_path.exists()
|
|
assert not fixture.config_path.exists()
|
|
|
|
|
|
def test_kiroide_cli_uninstall_accepts_exact_global_legacy_ownership(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture))
|
|
_write_json(
|
|
fixture.agent_directory / "config.json",
|
|
_legacy_runtime_config(fixture),
|
|
)
|
|
args = cli.build_parser().parse_args(
|
|
[
|
|
"uninstall",
|
|
"--agent",
|
|
"kiroide",
|
|
"--agent_id",
|
|
AGENT_ID,
|
|
]
|
|
)
|
|
|
|
cli.cmd_uninstall(args)
|
|
|
|
assert not fixture.legacy_path.exists()
|
|
assert not fixture.agent_directory.exists()
|
|
assert not fixture.config_path.exists()
|
|
|
|
|
|
def test_kiroide_rejects_unproved_legacy_runtime_owner(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture, "agent-2"))
|
|
runtime_config_path = fixture.agent_directory / "config.json"
|
|
original = _legacy_runtime_config(fixture)
|
|
_write_json(runtime_config_path, original)
|
|
|
|
with pytest.raises(ValueError, match="token is invalid"):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert json.loads(runtime_config_path.read_text(encoding="utf-8")) == original
|
|
assert fixture.legacy_path.exists()
|
|
assert not fixture.config_path.exists()
|
|
|
|
|
|
def test_kiroide_rejects_invalid_ownerless_legacy_runtime_config(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
_write_json(fixture.legacy_path, _legacy_document(fixture))
|
|
runtime_config_path = fixture.agent_directory / "config.json"
|
|
invalid = _legacy_runtime_config(fixture)
|
|
invalid["token"] = " "
|
|
_write_json(runtime_config_path, invalid)
|
|
|
|
with pytest.raises(ValueError, match="token is invalid"):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert json.loads(runtime_config_path.read_text(encoding="utf-8")) == invalid
|
|
assert fixture.legacy_path.exists()
|
|
assert not fixture.config_path.exists()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("owner", "message"),
|
|
[("relative", "must be absolute"), ("other", "bound to another workspace")],
|
|
)
|
|
def test_kiroide_status_rejects_invalid_runtime_workspace_owner(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
owner: str,
|
|
message: str,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
runtime_config_path = fixture.agent_directory / "config.json"
|
|
runtime_config = json.loads(runtime_config_path.read_text(encoding="utf-8"))
|
|
runtime_config["workspace_root"] = (
|
|
"relative" if owner == "relative" else str((tmp_path / "other").resolve())
|
|
)
|
|
_write_json(runtime_config_path, runtime_config)
|
|
|
|
with pytest.raises(ValueError, match=message):
|
|
fixture.plugin.status()
|
|
|
|
|
|
def test_kiroide_failed_install_removes_created_workspace_and_runtime_directories(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
|
|
def fail_first_commit(
|
|
_source: str,
|
|
_destination: str,
|
|
_directory: _transaction.PinnedDirectory,
|
|
) -> None:
|
|
raise OSError("injected Kiro IDE commit failure")
|
|
|
|
monkeypatch.setattr(_transaction, "_replace_physical", fail_first_commit)
|
|
|
|
with pytest.raises(OSError, match="injected Kiro IDE commit failure"):
|
|
fixture.plugin.install(fixture.config)
|
|
|
|
assert not (fixture.workspace / ".kiro").exists()
|
|
# The durable transaction state directory survives failures by design;
|
|
# everything else under the runtime root must be rolled back.
|
|
elydora_root = fixture.home / ".elydora"
|
|
if elydora_root.exists():
|
|
assert [entry.name for entry in elydora_root.iterdir()] == ["transactions"]
|
|
|
|
|
|
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission contract")
|
|
@pytest.mark.parametrize("relative_path", ["private.key", "config.json"])
|
|
def test_kiroide_noop_install_pins_runtime_secret_modes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
relative_path: str,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
sources, paths, prepared, _rendered = _prepare_installation(fixture)
|
|
target = fixture.agent_directory / relative_path
|
|
target.chmod(0o640)
|
|
|
|
with pytest.raises(OSError, match="changed during Install Kiro IDE hooks"):
|
|
kiroide_installation.commit_kiroide_installation(
|
|
prepared, sources, paths
|
|
)
|
|
|
|
assert stat.S_IMODE(target.stat().st_mode) == 0o640
|
|
|
|
|
|
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission contract")
|
|
def test_kiroide_key_rotation_rejects_concurrent_mode_changes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
rotated = InstallConfig(**fixture.config)
|
|
rotated["private_key"] = ROTATED_PRIVATE_KEY
|
|
sources, paths, prepared, _rendered = _prepare_installation(
|
|
fixture, rotated
|
|
)
|
|
key_path = fixture.agent_directory / "private.key"
|
|
assert any(change.file_path == str(key_path) for change in prepared.changes)
|
|
key_path.chmod(0o640)
|
|
|
|
with pytest.raises(OSError, match="changed during installation"):
|
|
kiroide_installation.commit_kiroide_installation(
|
|
prepared, sources, paths
|
|
)
|
|
|
|
assert key_path.read_text(encoding="utf-8") == PRIVATE_KEY
|
|
assert stat.S_IMODE(key_path.stat().st_mode) == 0o640
|
|
|
|
|
|
def test_kiroide_install_rejects_runtime_root_link_created_after_prepare(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
sources, paths, prepared, _rendered = _prepare_installation(fixture)
|
|
runtime_root = fixture.home / ".elydora"
|
|
redirected_root = tmp_path / "redirected-runtime-root"
|
|
redirected_root.mkdir()
|
|
try:
|
|
runtime_root.symlink_to(redirected_root, target_is_directory=True)
|
|
except OSError as error:
|
|
pytest.skip(f"Directory symbolic links are unavailable: {error}")
|
|
|
|
with pytest.raises(OSError, match="not a physical directory|changed during"):
|
|
kiroide_installation.commit_kiroide_installation(
|
|
prepared, sources, paths
|
|
)
|
|
|
|
assert not (redirected_root / AGENT_ID / "private.key").exists()
|
|
assert not fixture.config_path.exists()
|
|
|
|
|
|
def test_kiroide_install_rejects_agent_directory_identity_swap(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.agent_directory.mkdir(parents=True)
|
|
sources, paths, prepared, _rendered = _prepare_installation(fixture)
|
|
original_directory = fixture.agent_directory.with_name("agent-original")
|
|
fixture.agent_directory.rename(original_directory)
|
|
fixture.agent_directory.mkdir()
|
|
|
|
with pytest.raises(OSError, match="changed during"):
|
|
kiroide_installation.commit_kiroide_installation(
|
|
prepared, sources, paths
|
|
)
|
|
|
|
assert not (fixture.agent_directory / "private.key").exists()
|
|
assert not fixture.config_path.exists()
|
|
|
|
|
|
def test_kiroide_exact_managed_commands_survive_python_relocation(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
executable_name = "python.exe" if os.name == "nt" else "python3"
|
|
fake_executable = tmp_path / "other-runtime" / executable_name
|
|
with monkeypatch.context() as command_context:
|
|
command_context.setattr(
|
|
kiroide_command.sys, "executable", str(fake_executable)
|
|
)
|
|
guard_command = kiroide_command.build_kiroide_command(
|
|
str(fixture.guard_path)
|
|
)
|
|
audit_command = kiroide_command.build_kiroide_command(
|
|
str(fixture.audit_path)
|
|
)
|
|
stale_document = _managed_document(guard_command, audit_command)
|
|
_write_json(fixture.config_path, stale_document)
|
|
|
|
assert fixture.plugin.status() == {
|
|
"installed": False,
|
|
"agent": "kiroide",
|
|
"details": (
|
|
"Managed hooks are incomplete or invalid: "
|
|
f"{fixture.config_path}"
|
|
),
|
|
}
|
|
|
|
fixture.plugin.install(fixture.config)
|
|
repaired = json.loads(fixture.config_path.read_text(encoding="utf-8"))
|
|
assert kiroide_command.kiroide_runtime_reference(
|
|
_find_hook(repaired, "elydora-guard")["action"]["command"],
|
|
"guard.py",
|
|
) is not None
|
|
assert kiroide_command.kiroide_runtime_reference(
|
|
_find_hook(repaired, "elydora-audit")["action"]["command"],
|
|
"hook.py",
|
|
) is not None
|
|
|
|
_write_json(fixture.config_path, stale_document)
|
|
fixture.plugin.uninstall(AGENT_ID)
|
|
|
|
assert not fixture.config_path.exists()
|
|
|
|
|
|
@pytest.mark.parametrize("damage", ["field", "orphan"])
|
|
def test_kiroide_stale_command_ownership_survives_contract_drift(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
damage: str,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
executable_name = "python.exe" if os.name == "nt" else "python3"
|
|
fake_executable = tmp_path / "other-runtime" / executable_name
|
|
with monkeypatch.context() as command_context:
|
|
command_context.setattr(
|
|
kiroide_command.sys, "executable", str(fake_executable)
|
|
)
|
|
document = _managed_document(
|
|
kiroide_command.build_kiroide_command(str(fixture.guard_path)),
|
|
kiroide_command.build_kiroide_command(str(fixture.audit_path)),
|
|
)
|
|
if damage == "field":
|
|
_find_hook(document, "elydora-guard")["timeout"] = 9
|
|
else:
|
|
document["hooks"].remove(_find_hook(document, "elydora-audit"))
|
|
_write_json(fixture.config_path, document)
|
|
|
|
fixture.plugin.install(fixture.config)
|
|
repaired = json.loads(fixture.config_path.read_text(encoding="utf-8"))
|
|
assert len(repaired["hooks"]) == 2
|
|
assert all(hook["timeout"] == 10 for hook in repaired["hooks"])
|
|
|
|
_write_json(fixture.config_path, document)
|
|
fixture.plugin.uninstall(AGENT_ID)
|
|
|
|
assert json.loads(fixture.config_path.read_text(encoding="utf-8"))[
|
|
"hooks"
|
|
] == []
|
|
|
|
|
|
@pytest.mark.skipif(os.name != "nt", reason="Windows launcher contract")
|
|
def test_kiroide_current_commands_require_the_system_powershell_launcher(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
command = kiroide_command.build_kiroide_command(str(fixture.guard_path))
|
|
expected = f'"{kiroide_command._windows_powershell_path()}"'
|
|
altered = command.replace(expected, r'"C:\Other\powershell.exe"', 1)
|
|
|
|
assert kiroide_command.kiroide_runtime_reference(altered, "guard.py") is None
|
|
|
|
|
|
@pytest.mark.skipif(os.name != "nt", reason="Windows launcher contract")
|
|
def test_kiroide_windows_launcher_fails_when_python_is_missing(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
missing_python = tmp_path / "missing-runtime" / "python.exe"
|
|
script_path = tmp_path / "guard.py"
|
|
with monkeypatch.context() as command_context:
|
|
command_context.setattr(
|
|
kiroide_command.sys, "executable", str(missing_python)
|
|
)
|
|
command = kiroide_command.build_kiroide_command(str(script_path))
|
|
|
|
result = subprocess.run(
|
|
command,
|
|
shell=True,
|
|
capture_output=True,
|
|
check=False,
|
|
text=True,
|
|
)
|
|
|
|
assert result.returncode == 1
|
|
assert result.stderr
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"executable_name",
|
|
["python3.13t", "python3.14t", "python3.13d.exe", "pypy3", "pypy3.10.exe"],
|
|
)
|
|
def test_kiroide_ownership_accepts_supported_python_runtime_names(
|
|
executable_name: str,
|
|
) -> None:
|
|
assert kiroide_command._is_python_executable(executable_name)
|
|
|
|
|
|
def test_kiroide_builder_rejects_unrecoverable_executable_names(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
with monkeypatch.context() as command_context:
|
|
command_context.setattr(
|
|
kiroide_command.sys,
|
|
"executable",
|
|
str(tmp_path / "embedded-runtime"),
|
|
)
|
|
with pytest.raises(ValueError, match="absolute Python executable"):
|
|
kiroide_command.build_kiroide_command(str(tmp_path / "guard.py"))
|
|
|
|
|
|
@pytest.mark.parametrize("damage", ["orphan", "duplicate", "altered"])
|
|
def test_kiroide_status_rejects_every_incomplete_managed_hook_set(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
damage: str,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
document = json.loads(fixture.config_path.read_text(encoding="utf-8"))
|
|
audit = _find_hook(document, "elydora-audit")
|
|
if damage == "orphan":
|
|
document["hooks"].remove(audit)
|
|
elif damage == "duplicate":
|
|
document["hooks"].append(copy.deepcopy(audit))
|
|
else:
|
|
_find_hook(document, "elydora-guard")["timeout"] = 9
|
|
_write_json(fixture.config_path, document)
|
|
|
|
status = fixture.plugin.status()
|
|
assert status["installed"] is False
|
|
assert status["details"] == (
|
|
f"Managed hooks are incomplete or invalid: {fixture.config_path}"
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission contract")
|
|
@pytest.mark.parametrize(
|
|
("target", "message"),
|
|
[
|
|
("key", "private key must be accessible only by its owner"),
|
|
("config", "Token-bearing Elydora runtime config must be accessible"),
|
|
],
|
|
)
|
|
def test_kiroide_repairs_and_enforces_owner_only_runtime_secrets(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
target: str,
|
|
message: str,
|
|
) -> None:
|
|
fixture = _prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.plugin.install(fixture.config)
|
|
paths = {
|
|
"key": fixture.agent_directory / "private.key",
|
|
"config": fixture.agent_directory / "config.json",
|
|
}
|
|
paths[target].chmod(0o644)
|
|
|
|
with pytest.raises(ValueError, match=message):
|
|
fixture.plugin.status()
|
|
|
|
fixture.plugin.install(fixture.config)
|
|
assert stat.S_IMODE(paths[target].stat().st_mode) == 0o600
|
|
assert fixture.plugin.status()["installed"] is True
|