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
239 lines
6.8 KiB
Python
239 lines
6.8 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess # nosec B404
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from codex_support import (
|
|
AGENT_ID,
|
|
VALID_PRIVATE_KEY,
|
|
prepare_fixture,
|
|
symlink_or_skip,
|
|
write_json,
|
|
write_text,
|
|
)
|
|
from elydora.plugins import _transaction, codex
|
|
|
|
|
|
def assert_no_transaction_files(root: Path) -> None:
|
|
assert not list(root.rglob("*.tmp"))
|
|
assert not list(root.rglob("*.rollback"))
|
|
|
|
|
|
def test_transaction_rolls_back_all_five_files_when_hooks_commit_fails(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = prepare_fixture(
|
|
monkeypatch,
|
|
tmp_path,
|
|
existing_config={"hooks": {"SessionStart": []}},
|
|
)
|
|
original = fixture.config_path.read_text(encoding="utf-8")
|
|
real_replace = _transaction._replace_physical
|
|
failed = False
|
|
|
|
def fail_hooks_once(
|
|
source: str,
|
|
destination: str,
|
|
directory: _transaction.PinnedDirectory,
|
|
) -> None:
|
|
nonlocal failed
|
|
if os.path.abspath(destination) == str(fixture.config_path) and not failed:
|
|
failed = True
|
|
raise OSError("injected hooks commit failure")
|
|
real_replace(source, destination, directory)
|
|
|
|
monkeypatch.setattr(_transaction, "_replace_physical", fail_hooks_once)
|
|
|
|
with pytest.raises(OSError, match="injected hooks commit failure"):
|
|
fixture.install()
|
|
|
|
assert fixture.config_path.read_text(encoding="utf-8") == original
|
|
for path in (
|
|
fixture.guard_path,
|
|
fixture.hook_path,
|
|
fixture.runtime_config_path,
|
|
fixture.private_key_path,
|
|
):
|
|
assert path.exists() is False
|
|
assert_no_transaction_files(fixture.home_dir)
|
|
|
|
|
|
def test_install_detects_a_concurrent_hooks_change_before_staging(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = prepare_fixture(
|
|
monkeypatch,
|
|
tmp_path,
|
|
existing_config={"hooks": {"SessionStart": []}},
|
|
)
|
|
concurrent = '{"hooks":{"SessionStart":[{"hooks":[]}]}}\n'
|
|
real_rendered_change = codex.rendered_change
|
|
|
|
def mutate_before_change(rendered: object):
|
|
write_text(fixture.config_path, concurrent)
|
|
return real_rendered_change(rendered) # type: ignore[arg-type]
|
|
|
|
monkeypatch.setattr(codex, "rendered_change", mutate_before_change)
|
|
|
|
with pytest.raises(OSError, match="changed before staging"):
|
|
fixture.install()
|
|
|
|
assert fixture.config_path.read_text(encoding="utf-8") == concurrent
|
|
assert list(fixture.agent_dir.iterdir()) == []
|
|
assert_no_transaction_files(fixture.home_dir)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"runtime_name",
|
|
[
|
|
"private.key",
|
|
"guard.py",
|
|
"hook.py",
|
|
"chain-state.json",
|
|
"status-cache.json",
|
|
"error.log",
|
|
],
|
|
)
|
|
def test_orphan_runtime_artifacts_are_rejected_before_hooks_writes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
runtime_name: str,
|
|
) -> None:
|
|
fixture = prepare_fixture(monkeypatch, tmp_path)
|
|
artifact = fixture.agent_dir / runtime_name
|
|
write_text(artifact, "orphan\n")
|
|
|
|
with pytest.raises(ValueError, match="identity cannot be verified"):
|
|
fixture.install()
|
|
|
|
assert artifact.read_text(encoding="utf-8") == "orphan\n"
|
|
assert fixture.config_path.exists() is False
|
|
|
|
|
|
def test_mismatched_runtime_identity_is_rejected_before_writes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = prepare_fixture(monkeypatch, tmp_path)
|
|
write_json(
|
|
fixture.runtime_config_path,
|
|
{"agent_id": "another-agent", "agent_name": "codex"},
|
|
)
|
|
original = fixture.runtime_config_path.read_text(encoding="utf-8")
|
|
|
|
with pytest.raises(ValueError, match="identity does not match"):
|
|
fixture.install()
|
|
|
|
assert fixture.runtime_config_path.read_text(encoding="utf-8") == original
|
|
assert fixture.config_path.exists() is False
|
|
|
|
|
|
@pytest.mark.parametrize("kind", ["codex", "runtime"])
|
|
def test_linked_hooks_and_runtime_directories_are_rejected(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
kind: str,
|
|
) -> None:
|
|
fixture = prepare_fixture(monkeypatch, tmp_path)
|
|
target = tmp_path / f"{kind}-target"
|
|
target.mkdir()
|
|
if kind == "codex":
|
|
link = fixture.config_path.parent
|
|
else:
|
|
link = fixture.agent_dir.parent
|
|
shutil.rmtree(link)
|
|
link.parent.mkdir(parents=True, exist_ok=True)
|
|
symlink_or_skip(target, link, directory=True)
|
|
|
|
with pytest.raises(OSError, match="physical directory"):
|
|
fixture.install()
|
|
|
|
assert list(target.iterdir()) == []
|
|
assert fixture.config_path.exists() is False
|
|
|
|
|
|
def test_uninstall_rejects_a_linked_default_hooks_directory(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.install()
|
|
codex_directory = fixture.config_path.parent
|
|
target = tmp_path / "codex-hooks-target"
|
|
codex_directory.rename(target)
|
|
symlink_or_skip(target, codex_directory, directory=True)
|
|
|
|
with pytest.raises(OSError, match="physical directory"):
|
|
fixture.plugin.uninstall(AGENT_ID)
|
|
|
|
assert (target / "hooks.json").is_file()
|
|
|
|
|
|
def test_install_protects_credentials_and_cleans_transaction_files(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = prepare_fixture(monkeypatch, tmp_path)
|
|
fixture.install()
|
|
|
|
assert_no_transaction_files(fixture.home_dir)
|
|
if os.name != "nt":
|
|
for path in (
|
|
fixture.config_path,
|
|
fixture.runtime_config_path,
|
|
fixture.private_key_path,
|
|
):
|
|
assert path.stat().st_mode & 0o777 == 0o600
|
|
|
|
|
|
def test_cli_preflight_preserves_malformed_hooks_before_runtime_creation(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
home_dir = tmp_path / "cli-home"
|
|
hooks_path = home_dir / ".codex" / "hooks.json"
|
|
key_path = tmp_path / "private.key"
|
|
write_text(hooks_path, "{ malformed")
|
|
write_text(key_path, VALID_PRIVATE_KEY)
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"elydora.cli",
|
|
"install",
|
|
"--agent",
|
|
"codex",
|
|
"--org_id",
|
|
"org-1",
|
|
"--agent_id",
|
|
AGENT_ID,
|
|
"--private_key_file",
|
|
str(key_path),
|
|
"--kid",
|
|
"key-1",
|
|
"--base_url",
|
|
"https://api.elydora.com",
|
|
],
|
|
capture_output=True,
|
|
check=False,
|
|
cwd=Path(__file__).parents[1],
|
|
env={
|
|
**os.environ,
|
|
"HOME": str(home_dir),
|
|
"USERPROFILE": str(home_dir),
|
|
"CODEX_HOME": "",
|
|
},
|
|
text=True,
|
|
)
|
|
|
|
assert result.returncode == 1
|
|
assert "parse Codex user hooks" in result.stderr
|
|
assert hooks_path.read_text(encoding="utf-8") == "{ malformed"
|
|
assert (home_dir / ".elydora").exists() is False
|