chore(python): audit tests

Split the three oversized test files into support modules and topic
files, evaluate the rmdir dir_fd capability once at import, and drop
unused imports.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
This commit is contained in:
雷电芽衣 committed 2026-09-03 00:30:17 -04:00
1 parent 6ec3e902f5
commit aa4b8a8bdf
20 files changed
+2608 -2415

No files matched your search

+4 -1
View File
@@ -12,9 +12,12 @@ from ._managed_files import DirectorySnapshot, FileSnapshot
from ._pinned_directory import PinnedDirectory
_RMDIR_SUPPORTS_DIR_FD = os.rmdir in os.supports_dir_fd
def _remove_posix_private_directory(parent_descriptor: int, name: str) -> None:
"""Remove one empty child through the platform's native *at semantics."""
if os.rmdir not in os.supports_dir_fd:
if not _RMDIR_SUPPORTS_DIR_FD:
raise OSError("Host does not support identity-bound directory removal")
os.rmdir(name, dir_fd=parent_descriptor)
+1 -1
View File
@@ -10,7 +10,7 @@ import tempfile
from typing import Dict, List, Optional, Set, Tuple
from ._dotenv import parse_dotenv
from ._managed_files import MAX_SOURCE_BYTES, FileSnapshot, read_physical_file
from ._managed_files import MAX_SOURCE_BYTES, read_physical_file
from ._runtime import same_path, unique_preconditions
from ._transaction import FilePrecondition
from .qwen_config import (
+286
View File
@@ -0,0 +1,286 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import subprocess
import sys
import threading
from typing import Any
import pytest
from elydora.plugins import kiroide
from elydora.plugins import (
kiroide_contract,
kiroide_installation,
kiroide_io,
)
from elydora.plugins.base import InstallConfig
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
+83
View File
@@ -0,0 +1,83 @@
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import os
from pathlib import Path
from typing import Any
import pytest
from elydora import cli
AGENT_ID = "agent-1"
AGENT_NAME = "opencode"
PRIVATE_KEY = base64.urlsafe_b64encode(bytes([19]) * 32).rstrip(b"=").decode()
def set_home(monkeypatch: pytest.MonkeyPatch, home: Path) -> None:
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home))
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 write_secret(path: Path, value: str) -> None:
path.write_text(value, encoding="utf-8")
if os.name != "nt":
path.chmod(0o600)
def write_error_log(path: Path, chunks: int = 1) -> str:
digest = hashlib.sha256()
block = b"opaque historical error\n" * 4096
with path.open("wb") as file:
for _index in range(chunks):
file.write(block)
digest.update(block)
if os.name != "nt":
path.chmod(0o600)
return digest.hexdigest()
def stream_digest(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
while chunk := file.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def prepare_runtime(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
*,
agent_name: str = AGENT_NAME,
) -> tuple[Path, Path]:
home = tmp_path / "home"
root = home / ".elydora"
agent = root / AGENT_ID
set_home(monkeypatch, home)
write_json(
agent / "config.json",
{"agent_id": AGENT_ID, "agent_name": agent_name},
)
return root, agent
def uninstall_args(agent_name: str = AGENT_NAME) -> argparse.Namespace:
return cli.build_parser().parse_args(
[
"uninstall",
"--agent",
agent_name,
"--agent_id",
AGENT_ID,
]
)
-1
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
from copy import deepcopy
import json
import os
from pathlib import Path
import sys
-1
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess # nosec B404
-1
View File
@@ -5,7 +5,6 @@ import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Any
import pytest
+222
View File
@@ -0,0 +1,222 @@
from __future__ import annotations
import copy
import json
import os
from pathlib import Path
import subprocess
import stat
import pytest
from elydora.plugins._shell_command import is_python_executable, windows_powershell_path
from elydora.plugins import (
kiroide_command,
)
from kiroide_support import (
AGENT_ID,
find_hook,
managed_document,
prepare_fixture,
write_json,
)
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'"{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 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
File diff suppressed because it is too large. Load diff
+359
View File
@@ -0,0 +1,359 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from elydora.plugins import (
_transaction,
kiroide_installation,
)
from elydora.plugins.base import InstallConfig
from kiroide_support import (
AGENT_ID,
PRIVATE_KEY,
ROTATED_PRIVATE_KEY,
find_hook,
legacy_document,
prepare_fixture,
prepare_installation,
write_json,
write_large_error_log,
write_legacy_runtime,
)
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
+367
View File
@@ -0,0 +1,367 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import stat
import pytest
from elydora import cli
from elydora.plugins import (
_transaction,
kiroide_installation,
)
from elydora.plugins.base import InstallConfig
from kiroide_support import (
AGENT_ID,
PRIVATE_KEY,
ROTATED_PRIVATE_KEY,
legacy_document,
legacy_runtime_config,
prepare_fixture,
prepare_installation,
write_json,
)
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()
-1
View File
@@ -3,7 +3,6 @@ from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
import pytest
+1 -1
View File
@@ -21,7 +21,7 @@ def test_darwin_private_directory_removal_uses_stdlib_dir_fd(
calls.append((path, dir_fd))
monkeypatch.setattr(_private_artifact.os, "rmdir", record_rmdir)
monkeypatch.setattr(_private_artifact.os, "supports_dir_fd", {record_rmdir})
monkeypatch.setattr(_private_artifact, "_RMDIR_SUPPORTS_DIR_FD", True)
_private_artifact._remove_posix_private_directory(41, "private-container")
+23 -550
View File
@@ -1,23 +1,14 @@
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import os
from pathlib import Path
import stat
import subprocess
import sys
from typing import Any
import pytest
from elydora import cli
from elydora.plugins import (
_directory_removal,
_opaque_cleanup,
_opaque_removal,
_transaction,
)
from elydora.plugins._runtime_removal import (
@@ -25,75 +16,14 @@ from elydora.plugins._runtime_removal import (
finalize_runtime_removal,
prepare_runtime_removal,
)
AGENT_ID = "agent-1"
AGENT_NAME = "opencode"
PRIVATE_KEY = base64.urlsafe_b64encode(bytes([19]) * 32).rstrip(b"=").decode()
def _set_home(monkeypatch: pytest.MonkeyPatch, home: Path) -> None:
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home))
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 _write_secret(path: Path, value: str) -> None:
path.write_text(value, encoding="utf-8")
if os.name != "nt":
path.chmod(0o600)
def _write_error_log(path: Path, chunks: int = 1) -> str:
digest = hashlib.sha256()
block = b"opaque historical error\n" * 4096
with path.open("wb") as file:
for _index in range(chunks):
file.write(block)
digest.update(block)
if os.name != "nt":
path.chmod(0o600)
return digest.hexdigest()
def _stream_digest(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
while chunk := file.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _runtime(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
*,
agent_name: str = AGENT_NAME,
) -> tuple[Path, Path]:
home = tmp_path / "home"
root = home / ".elydora"
agent = root / AGENT_ID
_set_home(monkeypatch, home)
_write_json(
agent / "config.json",
{"agent_id": AGENT_ID, "agent_name": agent_name},
)
return root, agent
def _uninstall_args(agent_name: str = AGENT_NAME) -> argparse.Namespace:
return cli.build_parser().parse_args(
[
"uninstall",
"--agent",
agent_name,
"--agent_id",
from runtime_removal_support import (
AGENT_ID,
]
AGENT_NAME,
prepare_runtime,
stream_digest,
uninstall_args,
write_error_log,
write_json,
)
@@ -103,7 +33,7 @@ def test_cli_runtime_removal_rejects_unknown_entries_before_provider_mutation(
tmp_path: Path,
entry_kind: str,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
_root, agent = prepare_runtime(monkeypatch, tmp_path)
unknown = agent / "user-owned"
if entry_kind == "file":
unknown.write_text("preserve", encoding="utf-8")
@@ -127,7 +57,7 @@ def test_cli_runtime_removal_rejects_unknown_entries_before_provider_mutation(
monkeypatch.setattr(cli, "_get_plugin", lambda _name: Plugin())
with pytest.raises(OSError, match="unmanaged entries"):
cli.cmd_uninstall(_uninstall_args())
cli.cmd_uninstall(uninstall_args())
assert provider_calls == []
assert unknown.exists() or unknown.is_symlink()
@@ -138,7 +68,7 @@ def test_cli_skips_generic_cleanup_for_plugin_managed_runtime_removal(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path, agent_name="kiroide")
_root, agent = prepare_runtime(monkeypatch, tmp_path, agent_name="kiroide")
provider_calls: list[str] = []
class Plugin:
@@ -153,7 +83,7 @@ def test_cli_skips_generic_cleanup_for_plugin_managed_runtime_removal(
monkeypatch.setattr(cli, "_get_plugin", lambda _name: Plugin())
monkeypatch.setattr(cli, "prepare_runtime_removal", reject_generic_prepare)
cli.cmd_uninstall(_uninstall_args("kiroide"))
cli.cmd_uninstall(uninstall_args("kiroide"))
assert provider_calls == [AGENT_ID]
assert (agent / "config.json").is_file()
@@ -163,11 +93,11 @@ def test_runtime_removal_rejects_config_identity_drift(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
_root, agent = prepare_runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
concurrent = {"agent_id": AGENT_ID, "agent_name": "cursor"}
_write_json(agent / "config.json", concurrent)
write_json(agent / "config.json", concurrent)
with pytest.raises(OSError, match="changed"):
commit_runtime_removal(plan)
@@ -181,7 +111,7 @@ def test_runtime_removal_treats_other_plugin_wrappers_as_unmanaged(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
_root, agent = prepare_runtime(monkeypatch, tmp_path)
wrapper_name = "augment-guard.cmd" if os.name == "nt" else "augment-guard.sh"
user_file = agent / wrapper_name
user_file.write_text("user-owned payload", encoding="utf-8")
@@ -198,7 +128,7 @@ def test_runtime_removal_rejects_directory_swaps_without_deleting_replacements(
tmp_path: Path,
swap: str,
) -> None:
root, agent = _runtime(monkeypatch, tmp_path)
root, agent = prepare_runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
@@ -228,7 +158,7 @@ def test_runtime_removal_rejects_concurrent_creation_of_missing_managed_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
_root, agent = prepare_runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
concurrent = agent / "status-cache.json"
@@ -247,7 +177,7 @@ def test_runtime_removal_preserves_unknown_entry_created_before_finalize(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
_root, agent = prepare_runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
_transaction.write_changes(
@@ -270,7 +200,7 @@ def test_runtime_removal_deletes_all_known_dynamic_files(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
root, agent = _runtime(monkeypatch, tmp_path)
root, agent = prepare_runtime(monkeypatch, tmp_path)
dynamic = {
"chain-state.json": '{"sequence":1}\n',
"status-cache.json": '{"status":"active"}\n',
@@ -293,8 +223,8 @@ def test_runtime_removal_streams_oversized_error_log(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
root, agent = _runtime(monkeypatch, tmp_path)
expected_digest = _write_error_log(agent / "error.log", chunks=32)
root, agent = prepare_runtime(monkeypatch, tmp_path)
expected_digest = write_error_log(agent / "error.log", chunks=32)
assert (agent / "error.log").stat().st_size > 2 * 1024 * 1024
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
@@ -312,9 +242,9 @@ def test_oversized_error_log_rolls_back_after_later_text_failure(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
_root, agent = prepare_runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
expected_digest = _write_error_log(error_log, chunks=32)
expected_digest = write_error_log(error_log, chunks=32)
expected_mode = stat.S_IMODE(error_log.stat().st_mode)
original_commit = _transaction._commit
@@ -329,464 +259,7 @@ def test_oversized_error_log_rolls_back_after_later_text_failure(
commit_runtime_removal(plan)
monkeypatch.setattr(_transaction, "_commit", original_commit)
assert _stream_digest(error_log) == expected_digest
assert stream_digest(error_log) == expected_digest
assert stat.S_IMODE(error_log.stat().st_mode) == expected_mode
assert (agent / "config.json").is_file()
assert list(agent.glob("*.opaque-rollback")) == []
def test_process_exit_after_opaque_removal_restores_error_log_on_restart(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
expected_digest = _write_error_log(error_log, chunks=4)
script = """
import os
import sys
from elydora.plugins import _transaction
from elydora.plugins._runtime_removal import prepare_runtime_removal, commit_runtime_removal
agent_id, agent_name = sys.argv[1:]
plan = prepare_runtime_removal(agent_id, agent_name)
assert plan is not None
_transaction._commit = lambda staged: os._exit(78)
commit_runtime_removal(plan)
"""
environment = os.environ.copy()
environment["HOME"] = os.environ["HOME"]
environment["USERPROFILE"] = os.environ["USERPROFILE"]
completed = subprocess.run(
[sys.executable, "-c", script, AGENT_ID, AGENT_NAME],
cwd=Path(__file__).resolve().parents[1],
env=environment,
check=False,
)
assert completed.returncode == 78
assert not error_log.exists()
_transaction.recover_pending_transactions()
assert _stream_digest(error_log) == expected_digest
assert (agent / "config.json").is_file()
assert list(agent.glob(".*.opaque-rollback")) == []
def test_error_log_append_at_capture_boundary_fails_and_preserves_append(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
_write_error_log(error_log)
appended = b"concurrent append\n"
original_capture = _opaque_removal._capture_physical
def append_then_capture(staged: Any) -> None:
with error_log.open("ab") as file:
file.write(appended)
file.flush()
os.fsync(file.fileno())
original_capture(staged)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_removal,
"_capture_physical",
append_then_capture,
)
with pytest.raises(OSError, match="changed at commit boundary"):
commit_runtime_removal(plan)
assert error_log.read_bytes().endswith(appended)
assert (agent / "config.json").is_file()
def test_error_log_replace_at_capture_boundary_preserves_both_files(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
original_digest = _write_error_log(error_log)
displaced = agent / "displaced-error.log"
replacement = b"concurrent replacement\n"
original_capture = _opaque_removal._capture_physical
def replace_then_capture(staged: Any) -> None:
error_log.rename(displaced)
error_log.write_bytes(replacement)
if os.name != "nt":
error_log.chmod(0o600)
original_capture(staged)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_removal,
"_capture_physical",
replace_then_capture,
)
with pytest.raises(OSError, match="changed at commit boundary"):
commit_runtime_removal(plan)
assert error_log.read_bytes() == replacement
assert _stream_digest(displaced) == original_digest
assert (agent / "config.json").is_file()
def test_error_log_rollback_destination_collision_preserves_victim(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
original_digest = _write_error_log(error_log)
victim = b"concurrent opaque rollback destination\n"
rollback_path: Path | None = None
original_capture = _opaque_removal._capture_physical
def collide_with_rollback_destination(staged: Any) -> None:
nonlocal rollback_path
assert staged.rollback_path is not None
rollback_path = Path(staged.rollback_path)
rollback_path.write_bytes(victim)
if os.name != "nt":
rollback_path.chmod(0o600)
original_capture(staged)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_removal,
"_capture_physical",
collide_with_rollback_destination,
)
with pytest.raises(OSError, match="exist"):
commit_runtime_removal(plan)
assert rollback_path is not None
assert rollback_path.read_bytes() == victim
assert _stream_digest(error_log) == original_digest
assert (agent / "config.json").is_file()
def test_error_log_cleanup_boundary_append_is_preserved_and_reported(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
_write_error_log(error_log)
appended = b"cleanup boundary append\n"
original_capture = _opaque_cleanup._capture_cleanup_physical
def append_then_capture(
staged: Any,
source_path: str,
) -> None:
with Path(source_path).open("ab") as file:
file.write(appended)
file.flush()
os.fsync(file.fileno())
original_capture(staged, source_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_cleanup,
"_capture_cleanup_physical",
append_then_capture,
)
with pytest.raises(OSError, match="changed at cleanup boundary"):
commit_runtime_removal(plan)
preserved = list(agent.glob(".error.log.*.opaque-rollback"))
assert len(preserved) == 1
assert preserved[0].read_bytes().endswith(appended)
assert list(agent.glob(".*.elydora-quarantine")) == []
def test_error_log_cleanup_replacement_preserves_both_identities(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
original_digest = _write_error_log(error_log)
replacement = b"concurrent cleanup replacement\n"
displaced = agent / "displaced-opaque-rollback"
rollback_path: Path | None = None
original_capture = _opaque_cleanup._capture_cleanup_physical
def replace_before_cleanup_capture(staged: Any, source_path: str) -> None:
nonlocal rollback_path
rollback_path = Path(source_path)
rollback_path.rename(displaced)
rollback_path.write_bytes(replacement)
if os.name != "nt":
rollback_path.chmod(0o600)
original_capture(staged, source_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_cleanup,
"_capture_cleanup_physical",
replace_before_cleanup_capture,
)
with pytest.raises(OSError, match="changed at cleanup boundary") as raised:
commit_runtime_removal(plan)
assert rollback_path is not None
assert f"concurrent data preserved at {rollback_path}" in str(raised.value)
assert rollback_path.read_bytes() == replacement
assert _stream_digest(displaced) == original_digest
assert list(agent.glob(".*.elydora-quarantine")) == []
@pytest.mark.parametrize("entry_kind", ["directory", "symlink"])
def test_error_log_rejects_nonphysical_entries_before_runtime_mutation(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
entry_kind: str,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
original_config = (agent / "config.json").read_bytes()
if entry_kind == "directory":
error_log.mkdir()
(error_log / "marker").write_text("preserve", encoding="utf-8")
else:
target = tmp_path / "external-error.log"
target.write_text("preserve", encoding="utf-8")
try:
error_log.symlink_to(target)
except OSError as error:
pytest.skip(f"File symbolic links are unavailable: {error}")
with pytest.raises(OSError, match="not a physical file"):
prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert (agent / "config.json").read_bytes() == original_config
assert error_log.exists() or error_log.is_symlink()
@pytest.mark.skipif(os.name == "nt", reason="POSIX owner-only contract")
def test_error_log_requires_owner_only_permissions_during_prepare(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
error_log.write_text("historical error\n", encoding="utf-8")
error_log.chmod(0o640)
with pytest.raises(PermissionError, match="accessible only by its owner"):
prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert error_log.read_text(encoding="utf-8") == "historical error\n"
@pytest.mark.skipif(os.name == "nt", reason="POSIX parent-dirfd contract")
def test_posix_runtime_directory_removal_cleans_exact_quarantine(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
original_rename = _directory_removal._rename_posix_into_quarantine
calls: list[tuple[int, str, int, str, str]] = []
def record_rename(
parent_descriptor: int,
source_name: str,
quarantine_descriptor: int,
source_path: str,
destination_path: str,
) -> None:
calls.append(
(
parent_descriptor,
source_name,
quarantine_descriptor,
source_path,
destination_path,
)
)
original_rename(
parent_descriptor,
source_name,
quarantine_descriptor,
source_path,
destination_path,
)
monkeypatch.setattr(
_directory_removal,
"_rename_posix_into_quarantine",
record_rename,
)
commit_runtime_removal(plan)
assert len(calls) == 1
assert isinstance(calls[0][0], int)
assert calls[0][1] == AGENT_ID
assert isinstance(calls[0][2], int)
assert Path(calls[0][3]) == agent
assert Path(calls[0][4]).name == "entry"
quarantined = list(
Path(plan.runtime_root).glob(
f".{AGENT_ID}.*.elydora-removed/entry"
)
)
assert quarantined == []
assert list(Path(plan.runtime_root).glob(f".{AGENT_ID}.*.elydora-removed")) == []
assert not agent.exists()
@pytest.mark.skipif(os.name == "nt", reason="POSIX quarantine contract")
def test_posix_runtime_quarantine_preserves_atomic_boundary_swap(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
root, agent = _runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
replacement = root / "replacement"
replacement.mkdir()
marker = replacement / "user-owned"
marker.write_text("preserve", encoding="utf-8")
original = root / "agent-original"
real_rename = _directory_removal._rename_posix_into_quarantine
def swap_then_quarantine(
parent_descriptor: int,
source_name: str,
quarantine_descriptor: int,
source_path: str,
destination_path: str,
) -> None:
agent.rename(original)
replacement.rename(agent)
real_rename(
parent_descriptor,
source_name,
quarantine_descriptor,
source_path,
destination_path,
)
real_rmdir = _directory_removal.os.rmdir
def reject_path_rmdir(
path: str,
*,
dir_fd: int | None = None,
) -> None:
if dir_fd is None:
raise AssertionError("POSIX runtime cleanup must use a parent dirfd")
real_rmdir(path, dir_fd=dir_fd)
monkeypatch.setattr(
_directory_removal,
"_rename_posix_into_quarantine",
swap_then_quarantine,
)
monkeypatch.setattr(_directory_removal.os, "rmdir", reject_path_rmdir)
with pytest.raises(OSError, match="changed while quarantining"):
commit_runtime_removal(plan)
quarantined_markers = list(
root.glob(f".{AGENT_ID}.*.elydora-removed/entry/user-owned")
)
assert len(quarantined_markers) == 1
assert quarantined_markers[0].read_text(encoding="utf-8") == "preserve"
assert original.is_dir()
@pytest.mark.skipif(os.name != "nt", reason="Windows exact-handle contract")
def test_windows_runtime_directory_removal_marks_exact_pinned_handle(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = _runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
original_mark = _directory_removal._mark_windows_directory_for_deletion
observed: list[tuple[int, int]] = []
def record_mark(directory: Any) -> None:
assert directory.descriptor is not None
opened = os.fstat(directory.descriptor)
observed.append((opened.st_dev, opened.st_ino))
assert observed[-1] == (
plan.agent_snapshot.device,
plan.agent_snapshot.inode,
)
original_mark(directory)
monkeypatch.setattr(
_directory_removal,
"_mark_windows_directory_for_deletion",
record_mark,
)
commit_runtime_removal(plan)
assert observed == [(plan.agent_snapshot.device, plan.agent_snapshot.inode)]
assert not agent.exists()
def test_kiroide_cli_install_failure_leaves_no_runtime_root(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
home = tmp_path / "home"
workspace = tmp_path / "workspace"
home.mkdir()
workspace.mkdir()
_set_home(monkeypatch, home)
monkeypatch.chdir(workspace)
key_path = tmp_path / "install-private.key"
_write_secret(key_path, PRIVATE_KEY)
args = cli.build_parser().parse_args(
[
"install",
"--agent",
"kiroide",
"--org_id",
"org-1",
"--agent_id",
AGENT_ID,
"--private_key_file",
str(key_path),
"--kid",
"kid-1",
]
)
def fail_commit(_staged: object) -> None:
raise OSError("injected Kiro IDE CLI commit failure")
monkeypatch.setattr(_transaction, "_commit", fail_commit)
with pytest.raises(OSError, match="injected Kiro IDE CLI commit failure"):
cli.cmd_install(args)
# The durable transaction state directory survives failures by design;
# everything else under the runtime root must be rolled back.
elydora_root = home / ".elydora"
if elydora_root.exists():
assert [entry.name for entry in elydora_root.iterdir()] == ["transactions"]
assert not (workspace / ".kiro").exists()
+222
View File
@@ -0,0 +1,222 @@
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
import pytest
from elydora import cli
from elydora.plugins import (
_directory_removal,
_transaction,
)
from elydora.plugins._runtime_removal import (
commit_runtime_removal,
prepare_runtime_removal,
)
from runtime_removal_support import (
AGENT_ID,
AGENT_NAME,
PRIVATE_KEY,
prepare_runtime,
set_home,
write_secret,
)
@pytest.mark.skipif(os.name == "nt", reason="POSIX parent-dirfd contract")
def test_posix_runtime_directory_removal_cleans_exact_quarantine(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
original_rename = _directory_removal._rename_posix_into_quarantine
calls: list[tuple[int, str, int, str, str]] = []
def record_rename(
parent_descriptor: int,
source_name: str,
quarantine_descriptor: int,
source_path: str,
destination_path: str,
) -> None:
calls.append(
(
parent_descriptor,
source_name,
quarantine_descriptor,
source_path,
destination_path,
)
)
original_rename(
parent_descriptor,
source_name,
quarantine_descriptor,
source_path,
destination_path,
)
monkeypatch.setattr(
_directory_removal,
"_rename_posix_into_quarantine",
record_rename,
)
commit_runtime_removal(plan)
assert len(calls) == 1
assert isinstance(calls[0][0], int)
assert calls[0][1] == AGENT_ID
assert isinstance(calls[0][2], int)
assert Path(calls[0][3]) == agent
assert Path(calls[0][4]).name == "entry"
quarantined = list(
Path(plan.runtime_root).glob(
f".{AGENT_ID}.*.elydora-removed/entry"
)
)
assert quarantined == []
assert list(Path(plan.runtime_root).glob(f".{AGENT_ID}.*.elydora-removed")) == []
assert not agent.exists()
@pytest.mark.skipif(os.name == "nt", reason="POSIX quarantine contract")
def test_posix_runtime_quarantine_preserves_atomic_boundary_swap(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
root, agent = prepare_runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
replacement = root / "replacement"
replacement.mkdir()
marker = replacement / "user-owned"
marker.write_text("preserve", encoding="utf-8")
original = root / "agent-original"
real_rename = _directory_removal._rename_posix_into_quarantine
def swap_then_quarantine(
parent_descriptor: int,
source_name: str,
quarantine_descriptor: int,
source_path: str,
destination_path: str,
) -> None:
agent.rename(original)
replacement.rename(agent)
real_rename(
parent_descriptor,
source_name,
quarantine_descriptor,
source_path,
destination_path,
)
real_rmdir = _directory_removal.os.rmdir
def reject_path_rmdir(
path: str,
*,
dir_fd: int | None = None,
) -> None:
if dir_fd is None:
raise AssertionError("POSIX runtime cleanup must use a parent dirfd")
real_rmdir(path, dir_fd=dir_fd)
monkeypatch.setattr(
_directory_removal,
"_rename_posix_into_quarantine",
swap_then_quarantine,
)
monkeypatch.setattr(_directory_removal.os, "rmdir", reject_path_rmdir)
with pytest.raises(OSError, match="changed while quarantining"):
commit_runtime_removal(plan)
quarantined_markers = list(
root.glob(f".{AGENT_ID}.*.elydora-removed/entry/user-owned")
)
assert len(quarantined_markers) == 1
assert quarantined_markers[0].read_text(encoding="utf-8") == "preserve"
assert original.is_dir()
@pytest.mark.skipif(os.name != "nt", reason="Windows exact-handle contract")
def test_windows_runtime_directory_removal_marks_exact_pinned_handle(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
original_mark = _directory_removal._mark_windows_directory_for_deletion
observed: list[tuple[int, int]] = []
def record_mark(directory: Any) -> None:
assert directory.descriptor is not None
opened = os.fstat(directory.descriptor)
observed.append((opened.st_dev, opened.st_ino))
assert observed[-1] == (
plan.agent_snapshot.device,
plan.agent_snapshot.inode,
)
original_mark(directory)
monkeypatch.setattr(
_directory_removal,
"_mark_windows_directory_for_deletion",
record_mark,
)
commit_runtime_removal(plan)
assert observed == [(plan.agent_snapshot.device, plan.agent_snapshot.inode)]
assert not agent.exists()
def test_kiroide_cli_install_failure_leaves_no_runtime_root(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
home = tmp_path / "home"
workspace = tmp_path / "workspace"
home.mkdir()
workspace.mkdir()
set_home(monkeypatch, home)
monkeypatch.chdir(workspace)
key_path = tmp_path / "install-private.key"
write_secret(key_path, PRIVATE_KEY)
args = cli.build_parser().parse_args(
[
"install",
"--agent",
"kiroide",
"--org_id",
"org-1",
"--agent_id",
AGENT_ID,
"--private_key_file",
str(key_path),
"--kid",
"kid-1",
]
)
def fail_commit(_staged: object) -> None:
raise OSError("injected Kiro IDE CLI commit failure")
monkeypatch.setattr(_transaction, "_commit", fail_commit)
with pytest.raises(OSError, match="injected Kiro IDE CLI commit failure"):
cli.cmd_install(args)
# The durable transaction state directory survives failures by design;
# everything else under the runtime root must be rolled back.
elydora_root = home / ".elydora"
if elydora_root.exists():
assert [entry.name for entry in elydora_root.iterdir()] == ["transactions"]
assert not (workspace / ".kiro").exists()
+286
View File
@@ -0,0 +1,286 @@
from __future__ import annotations
import os
from pathlib import Path
import subprocess
import sys
from typing import Any
import pytest
from elydora.plugins import (
_opaque_cleanup,
_opaque_removal,
_transaction,
)
from elydora.plugins._runtime_removal import (
commit_runtime_removal,
prepare_runtime_removal,
)
from runtime_removal_support import (
AGENT_ID,
AGENT_NAME,
prepare_runtime,
stream_digest,
write_error_log,
)
def test_process_exit_after_opaque_removal_restores_error_log_on_restart(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
expected_digest = write_error_log(error_log, chunks=4)
script = """
import os
import sys
from elydora.plugins import _transaction
from elydora.plugins._runtime_removal import prepare_runtime_removal, commit_runtime_removal
agent_id, agent_name = sys.argv[1:]
plan = prepare_runtime_removal(agent_id, agent_name)
assert plan is not None
_transaction._commit = lambda staged: os._exit(78)
commit_runtime_removal(plan)
"""
environment = os.environ.copy()
environment["HOME"] = os.environ["HOME"]
environment["USERPROFILE"] = os.environ["USERPROFILE"]
completed = subprocess.run(
[sys.executable, "-c", script, AGENT_ID, AGENT_NAME],
cwd=Path(__file__).resolve().parents[1],
env=environment,
check=False,
)
assert completed.returncode == 78
assert not error_log.exists()
_transaction.recover_pending_transactions()
assert stream_digest(error_log) == expected_digest
assert (agent / "config.json").is_file()
assert list(agent.glob(".*.opaque-rollback")) == []
def test_error_log_append_at_capture_boundary_fails_and_preserves_append(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
write_error_log(error_log)
appended = b"concurrent append\n"
original_capture = _opaque_removal._capture_physical
def append_then_capture(staged: Any) -> None:
with error_log.open("ab") as file:
file.write(appended)
file.flush()
os.fsync(file.fileno())
original_capture(staged)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_removal,
"_capture_physical",
append_then_capture,
)
with pytest.raises(OSError, match="changed at commit boundary"):
commit_runtime_removal(plan)
assert error_log.read_bytes().endswith(appended)
assert (agent / "config.json").is_file()
def test_error_log_replace_at_capture_boundary_preserves_both_files(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
original_digest = write_error_log(error_log)
displaced = agent / "displaced-error.log"
replacement = b"concurrent replacement\n"
original_capture = _opaque_removal._capture_physical
def replace_then_capture(staged: Any) -> None:
error_log.rename(displaced)
error_log.write_bytes(replacement)
if os.name != "nt":
error_log.chmod(0o600)
original_capture(staged)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_removal,
"_capture_physical",
replace_then_capture,
)
with pytest.raises(OSError, match="changed at commit boundary"):
commit_runtime_removal(plan)
assert error_log.read_bytes() == replacement
assert stream_digest(displaced) == original_digest
assert (agent / "config.json").is_file()
def test_error_log_rollback_destination_collision_preserves_victim(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
original_digest = write_error_log(error_log)
victim = b"concurrent opaque rollback destination\n"
rollback_path: Path | None = None
original_capture = _opaque_removal._capture_physical
def collide_with_rollback_destination(staged: Any) -> None:
nonlocal rollback_path
assert staged.rollback_path is not None
rollback_path = Path(staged.rollback_path)
rollback_path.write_bytes(victim)
if os.name != "nt":
rollback_path.chmod(0o600)
original_capture(staged)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_removal,
"_capture_physical",
collide_with_rollback_destination,
)
with pytest.raises(OSError, match="exist"):
commit_runtime_removal(plan)
assert rollback_path is not None
assert rollback_path.read_bytes() == victim
assert stream_digest(error_log) == original_digest
assert (agent / "config.json").is_file()
def test_error_log_cleanup_boundary_append_is_preserved_and_reported(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
write_error_log(error_log)
appended = b"cleanup boundary append\n"
original_capture = _opaque_cleanup._capture_cleanup_physical
def append_then_capture(
staged: Any,
source_path: str,
) -> None:
with Path(source_path).open("ab") as file:
file.write(appended)
file.flush()
os.fsync(file.fileno())
original_capture(staged, source_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_cleanup,
"_capture_cleanup_physical",
append_then_capture,
)
with pytest.raises(OSError, match="changed at cleanup boundary"):
commit_runtime_removal(plan)
preserved = list(agent.glob(".error.log.*.opaque-rollback"))
assert len(preserved) == 1
assert preserved[0].read_bytes().endswith(appended)
assert list(agent.glob(".*.elydora-quarantine")) == []
def test_error_log_cleanup_replacement_preserves_both_identities(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
original_digest = write_error_log(error_log)
replacement = b"concurrent cleanup replacement\n"
displaced = agent / "displaced-opaque-rollback"
rollback_path: Path | None = None
original_capture = _opaque_cleanup._capture_cleanup_physical
def replace_before_cleanup_capture(staged: Any, source_path: str) -> None:
nonlocal rollback_path
rollback_path = Path(source_path)
rollback_path.rename(displaced)
rollback_path.write_bytes(replacement)
if os.name != "nt":
rollback_path.chmod(0o600)
original_capture(staged, source_path)
plan = prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert plan is not None
monkeypatch.setattr(
_opaque_cleanup,
"_capture_cleanup_physical",
replace_before_cleanup_capture,
)
with pytest.raises(OSError, match="changed at cleanup boundary") as raised:
commit_runtime_removal(plan)
assert rollback_path is not None
assert f"concurrent data preserved at {rollback_path}" in str(raised.value)
assert rollback_path.read_bytes() == replacement
assert stream_digest(displaced) == original_digest
assert list(agent.glob(".*.elydora-quarantine")) == []
@pytest.mark.parametrize("entry_kind", ["directory", "symlink"])
def test_error_log_rejects_nonphysical_entries_before_runtime_mutation(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
entry_kind: str,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
original_config = (agent / "config.json").read_bytes()
if entry_kind == "directory":
error_log.mkdir()
(error_log / "marker").write_text("preserve", encoding="utf-8")
else:
target = tmp_path / "external-error.log"
target.write_text("preserve", encoding="utf-8")
try:
error_log.symlink_to(target)
except OSError as error:
pytest.skip(f"File symbolic links are unavailable: {error}")
with pytest.raises(OSError, match="not a physical file"):
prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert (agent / "config.json").read_bytes() == original_config
assert error_log.exists() or error_log.is_symlink()
@pytest.mark.skipif(os.name == "nt", reason="POSIX owner-only contract")
def test_error_log_requires_owner_only_permissions_during_prepare(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
_root, agent = prepare_runtime(monkeypatch, tmp_path)
error_log = agent / "error.log"
error_log.write_text("historical error\n", encoding="utf-8")
error_log.chmod(0o640)
with pytest.raises(PermissionError, match="accessible only by its owner"):
prepare_runtime_removal(AGENT_ID, AGENT_NAME)
assert error_log.read_text(encoding="utf-8") == "historical error\n"
+245
View File
@@ -0,0 +1,245 @@
from __future__ import annotations
import os
from pathlib import Path
import pytest
from elydora.plugins import _transaction, _transaction_staging
from elydora.plugins._managed_files import read_physical_directory
from transaction_support import (
make_change,
swap_directory,
)
@pytest.mark.skipif(os.name == "nt", reason="Windows handle blocks directory rename")
def test_staging_directory_swap_never_writes_secret_to_redirected_path(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
parent = tmp_path / "runtime"
backup = tmp_path / "runtime-original"
redirected = tmp_path / "runtime-redirected"
parent.mkdir()
redirected.mkdir()
target = parent / "private.key"
real_write = _transaction_staging.write_staged
swapped = False
def swap_then_stage(*args: object, **kwargs: object):
nonlocal swapped
if not swapped:
swapped = True
swap_directory(parent, backup, redirected)
return real_write(*args, **kwargs)
monkeypatch.setattr(_transaction_staging, "write_staged", swap_then_stage)
with pytest.raises(OSError, match="changed during transaction"):
_transaction.write_changes(
[make_change(target, None, "secret\n")],
"staging swap",
)
assert not (parent / "private.key").exists()
assert not (backup / "private.key").exists()
assert list(parent.iterdir()) == []
assert list(backup.iterdir()) == []
@pytest.mark.skipif(os.name == "nt", reason="Windows handle blocks directory rename")
def test_commit_directory_swap_rolls_back_inside_pinned_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
parent = tmp_path / "runtime"
backup = tmp_path / "runtime-original"
redirected = tmp_path / "runtime-redirected"
parent.mkdir()
redirected.mkdir()
target = parent / "private.key"
real_replace = _transaction._replace_physical
swapped = False
def swap_then_replace(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal swapped
if not swapped:
swapped = True
swap_directory(parent, backup, redirected)
real_replace(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", swap_then_replace)
with pytest.raises(OSError, match="changed during transaction"):
_transaction.write_changes(
[make_change(target, None, "secret\n")],
"commit swap",
)
assert not (parent / "private.key").exists()
assert not (backup / "private.key").exists()
assert list(parent.iterdir()) == []
assert list(backup.iterdir()) == []
def test_failed_transaction_removes_only_created_empty_directories(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
kiro = tmp_path / ".kiro"
hooks = kiro / "hooks"
hooks.mkdir(parents=True)
kiro_snapshot = read_physical_directory(str(kiro), "Kiro directory")
hooks_snapshot = read_physical_directory(str(hooks), "hooks directory")
target = hooks / "elydora-audit.json"
def fail_replace(
_source: str,
_destination: str,
_directory: _transaction.PinnedDirectory,
) -> None:
raise OSError("injected commit failure")
monkeypatch.setattr(_transaction, "_replace_physical", fail_replace)
with pytest.raises(OSError, match="injected commit failure"):
_transaction.write_changes(
[make_change(target, None, "{}\n")],
"created directory recovery",
directory_preconditions=[
_transaction.DirectoryPrecondition(
str(kiro), "Kiro directory", kiro_snapshot, created=True
),
_transaction.DirectoryPrecondition(
str(hooks), "hooks directory", hooks_snapshot, created=True
),
],
)
assert not hooks.exists()
assert not kiro.exists()
def test_created_directory_recovery_keeps_parent_and_child_pinned(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
kiro = tmp_path / ".kiro"
hooks = kiro / "hooks"
hooks.mkdir(parents=True)
kiro_snapshot = read_physical_directory(str(kiro), "Kiro directory")
hooks_snapshot = read_physical_directory(str(hooks), "hooks directory")
target = hooks / "elydora-audit.json"
observed: list[tuple[Path, Path]] = []
real_remove = _transaction_staging.remove_pinned_empty_directory
def observe_pins(
parent: object,
child: object,
*,
require_empty: bool = True,
) -> None:
assert isinstance(parent, _transaction.PinnedDirectory)
assert isinstance(child, _transaction.PinnedDirectory)
assert parent.descriptor is not None
assert child.descriptor is not None
assert os.fstat(child.descriptor).st_ino == child.snapshot.inode
observed.append((Path(parent.path), Path(child.path)))
real_remove(parent, child, require_empty=require_empty)
def fail_replace(
_source: str,
_destination: str,
_directory: _transaction.PinnedDirectory,
) -> None:
raise OSError("injected pinned cleanup failure")
monkeypatch.setattr(
_transaction_staging,
"remove_pinned_empty_directory",
observe_pins,
)
monkeypatch.setattr(_transaction, "_replace_physical", fail_replace)
with pytest.raises(OSError, match="injected pinned cleanup failure"):
_transaction.write_changes(
[make_change(target, None, "{}\n")],
"pinned directory recovery",
directory_preconditions=[
_transaction.DirectoryPrecondition(
str(kiro), "Kiro directory", kiro_snapshot, created=True
),
_transaction.DirectoryPrecondition(
str(hooks), "hooks directory", hooks_snapshot, created=True
),
],
)
expected = [(kiro, hooks), (tmp_path, kiro)]
assert observed == expected
assert not kiro.exists()
@pytest.mark.skipif(os.name == "nt", reason="Windows handle blocks directory rename")
def test_created_directory_recovery_preserves_swapped_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
created = tmp_path / "created"
created.mkdir()
created_snapshot = read_physical_directory(str(created), "created directory")
target = created / "managed.json"
replacement = tmp_path / "replacement"
replacement.mkdir()
marker = replacement / "user-owned"
marker.write_text("preserve", encoding="utf-8")
original = tmp_path / "created-original"
real_remove = _transaction_staging.remove_pinned_empty_directory
def swap_before_removal(
parent: _transaction.PinnedDirectory,
child: _transaction.PinnedDirectory,
*,
require_empty: bool = True,
) -> None:
created.rename(original)
replacement.rename(created)
real_remove(parent, child, require_empty=require_empty)
def fail_replace(
_source: str,
_destination: str,
_directory: _transaction.PinnedDirectory,
) -> None:
raise OSError("injected swap cleanup failure")
monkeypatch.setattr(
_transaction_staging,
"remove_pinned_empty_directory",
swap_before_removal,
)
monkeypatch.setattr(_transaction, "_replace_physical", fail_replace)
with pytest.raises(OSError, match="recovery failed"):
_transaction.write_changes(
[make_change(target, None, "{}\n")],
"swapped directory recovery",
directory_preconditions=[
_transaction.DirectoryPrecondition(
str(created),
"created directory",
created_snapshot,
created=True,
)
],
)
assert (created / marker.name).read_text(encoding="utf-8") == "preserve"
assert original.is_dir()
assert Path(_transaction.active_journal_path()).is_file()
_transaction.clear_journal()
+404
View File
@@ -0,0 +1,404 @@
from __future__ import annotations
import os
from pathlib import Path
import pytest
from elydora.plugins import _transaction
from transaction_support import (
make_change,
write_source,
)
def test_final_replace_exception_after_success_restores_original(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
write_source(target, "old\n")
real_replace = _transaction._replace_physical
def replace_then_raise(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
real_replace(source, destination, directory)
if os.path.abspath(destination) == os.path.abspath(str(target)):
raise OSError("injected post-replace failure")
monkeypatch.setattr(_transaction, "_replace_physical", replace_then_raise)
with pytest.raises(OSError, match="injected post-replace failure"):
_transaction.write_changes(
[make_change(target, "old\n", "new\n")],
"post-replace failure",
)
assert target.read_text(encoding="utf-8") == "old\n"
assert list(tmp_path.glob(".*.rollback")) == []
def test_capture_exception_after_success_restores_original(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
write_source(target, "old\n")
real_capture = _transaction._capture_physical
def capture_then_raise(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
real_capture(source, destination, directory)
raise OSError("injected post-capture failure")
monkeypatch.setattr(_transaction, "_capture_physical", capture_then_raise)
with pytest.raises(OSError, match="injected post-capture failure"):
_transaction.write_changes(
[make_change(target, "old\n", "new\n")],
"post-capture failure",
)
assert target.read_text(encoding="utf-8") == "old\n"
assert list(tmp_path.glob(".*.rollback")) == []
def test_rollback_reservation_collision_preserves_victim_and_original(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
write_source(target, "old\n")
victim = "concurrent rollback destination\n"
collision_path: Path | None = None
def collide_with_rollback_destination(
_source: str,
destination: str,
_directory: _transaction.PinnedDirectory,
) -> None:
nonlocal collision_path
if destination.endswith(".rollback"):
collision_path = Path(destination)
write_source(collision_path, victim)
monkeypatch.setattr(
_transaction,
"_capture_physical",
collide_with_rollback_destination,
)
with pytest.raises(OSError, match="exist"):
_transaction.write_changes(
[make_change(target, "old\n", "new\n")],
"rollback destination collision",
)
assert collision_path is not None
assert target.read_text(encoding="utf-8") == "old\n"
assert collision_path.read_text(encoding="utf-8") == victim
def test_final_install_does_not_overwrite_concurrent_target(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
write_source(target, "old\n")
real_replace = _transaction._replace_physical
injected = False
def create_target_before_install(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal injected
if not injected and os.path.abspath(destination) == os.path.abspath(target):
injected = True
write_source(target, "concurrent\n")
real_replace(source, destination, directory)
monkeypatch.setattr(
_transaction,
"_replace_physical",
create_target_before_install,
)
with pytest.raises(OSError, match="original content preserved"):
_transaction.write_changes(
[make_change(target, "old\n", "new\n")],
"no-clobber install",
)
assert injected
assert target.read_text(encoding="utf-8") == "concurrent\n"
rollbacks = list(tmp_path.glob(".*.rollback"))
assert len(rollbacks) == 1
assert rollbacks[0].read_text(encoding="utf-8") == "old\n"
def test_restore_does_not_overwrite_concurrent_target(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
second = tmp_path / "second.txt"
write_source(target, "old\n")
write_source(second, "second-old\n")
real_replace = _transaction._replace_physical
real_restore = _transaction._restore_physical
injected = False
def fail_second_commit(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(second):
raise OSError("injected second failure")
real_replace(source, destination, directory)
def create_target_before_restore(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal injected
if not injected and os.path.abspath(destination) == os.path.abspath(target):
injected = True
write_source(target, "concurrent recovery\n")
real_restore(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", fail_second_commit)
monkeypatch.setattr(
_transaction,
"_restore_physical",
create_target_before_restore,
)
with pytest.raises(OSError, match="original content preserved"):
_transaction.write_changes(
[
make_change(target, "old\n", "new\n"),
make_change(second, "second-old\n", "second-new\n"),
],
"no-clobber restoration",
)
assert injected
assert target.read_text(encoding="utf-8") == "concurrent recovery\n"
rollbacks = list(tmp_path.glob(".*.rollback"))
assert len(rollbacks) == 1
assert rollbacks[0].read_text(encoding="utf-8") == "old\n"
assert Path(_transaction.active_journal_path()).is_file()
_transaction.clear_journal()
def test_restore_source_replacement_reports_existing_preserved_path(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
second = tmp_path / "second.txt"
displaced = tmp_path / "displaced-original.txt"
write_source(target, "old\n")
write_source(second, "second-old\n")
real_replace = _transaction._replace_physical
real_restore = _transaction._restore_physical
rollback_path: Path | None = None
def fail_second_commit(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(second):
raise OSError("injected second failure")
real_replace(source, destination, directory)
def replace_restore_source(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal rollback_path
if (
rollback_path is None
and source.endswith(".rollback")
and os.path.abspath(destination) == os.path.abspath(target)
):
rollback_path = Path(source)
rollback_path.rename(displaced)
write_source(rollback_path, "concurrent recovery source\n")
real_restore(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", fail_second_commit)
monkeypatch.setattr(_transaction, "_restore_physical", replace_restore_source)
with pytest.raises(OSError, match="recovery source changed") as raised:
_transaction.write_changes(
[
make_change(target, "old\n", "new\n"),
make_change(second, "second-old\n", "second-new\n"),
],
"restore source replacement",
)
assert rollback_path is not None
assert f"concurrent data preserved at {rollback_path}" in str(raised.value)
assert rollback_path.read_text(encoding="utf-8") == "concurrent recovery source\n"
assert displaced.read_text(encoding="utf-8") == "old\n"
assert target.read_text(encoding="utf-8") == "new\n"
assert second.read_text(encoding="utf-8") == "second-old\n"
assert Path(_transaction.active_journal_path()).is_file()
_transaction.clear_journal()
def test_creation_rollback_captures_and_preserves_concurrent_replacement(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
created = tmp_path / "created.txt"
second = tmp_path / "second.txt"
write_source(second, "second-old\n")
real_replace = _transaction._replace_physical
real_capture = _transaction._capture_physical
injected = False
def fail_second_commit(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(second):
raise OSError("injected second failure")
real_replace(source, destination, directory)
def replace_created_before_recovery_capture(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal injected
if (
not injected
and os.path.abspath(source) == os.path.abspath(created)
and destination.endswith(".recovery")
):
injected = True
write_source(created, "concurrent replacement\n")
real_capture(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", fail_second_commit)
monkeypatch.setattr(
_transaction,
"_capture_physical",
replace_created_before_recovery_capture,
)
with pytest.raises(OSError, match="changed at recovery boundary"):
_transaction.write_changes(
[
make_change(created, None, "generated\n"),
make_change(second, "second-old\n", "second-new\n"),
],
"creation recovery capture",
)
assert injected
assert created.read_text(encoding="utf-8") == "concurrent replacement\n"
assert Path(_transaction.active_journal_path()).is_file()
_transaction.clear_journal()
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode contract")
def test_mode_repair_uses_private_rollback_and_restores_original_mode(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
secret = tmp_path / "private.key"
second = tmp_path / "config.json"
write_source(secret, "secret\n")
write_source(second, "old\n")
secret.chmod(0o644)
real_replace = _transaction._replace_physical
rollback_modes: list[int] = []
def inspect_then_fail_second(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(str(secret)):
rollback = next(tmp_path.glob(".private.key.*.rollback"))
rollback_modes.append(rollback.stat().st_mode & 0o777)
if os.path.abspath(destination) == os.path.abspath(str(second)):
raise OSError("injected second commit failure")
real_replace(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", inspect_then_fail_second)
with pytest.raises(OSError, match="injected second commit failure"):
_transaction.write_changes(
[
make_change(secret, "secret\n", "secret\n", enforce_mode=True),
make_change(second, "old\n", "new\n"),
],
"mode repair",
)
assert rollback_modes
assert set(rollback_modes) == {0o600}
assert secret.read_text(encoding="utf-8") == "secret\n"
assert secret.stat().st_mode & 0o777 == 0o644
assert second.read_text(encoding="utf-8") == "old\n"
assert list(tmp_path.glob(".*.rollback")) == []
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode contract")
def test_deletion_uses_private_rollback_and_restores_original_mode(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
removed = tmp_path / "removed.txt"
second = tmp_path / "second.txt"
write_source(removed, "remove me\n")
write_source(second, "old\n")
removed.chmod(0o644)
real_replace = _transaction._replace_physical
rollback_modes: list[int] = []
def inspect_then_fail_second(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(str(second)):
rollback = next(tmp_path.glob(".removed.txt.*.rollback"))
rollback_modes.append(rollback.stat().st_mode & 0o777)
raise OSError("injected second commit failure")
real_replace(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", inspect_then_fail_second)
with pytest.raises(OSError, match="injected second commit failure"):
_transaction.write_changes(
[
make_change(removed, "remove me\n", None),
make_change(second, "old\n", "new\n"),
],
"deletion rollback mode",
)
assert rollback_modes == [0o600]
assert removed.read_text(encoding="utf-8") == "remove me\n"
assert removed.stat().st_mode & 0o777 == 0o644
assert second.read_text(encoding="utf-8") == "old\n"
assert list(tmp_path.glob(".*.rollback")) == []
+26 -680
View File
@@ -10,31 +10,13 @@ import time
import pytest
from elydora.plugins import _transaction, _transaction_staging
from elydora.plugins._managed_files import read_physical_directory, read_physical_file
def _write(path: Path, source: str) -> None:
path.write_bytes(source.encode("utf-8"))
def _change(
path: Path,
original: str | None,
next_source: str | None,
mode: int = 0o600,
enforce_mode: bool = False,
) -> _transaction.FileChange:
change = _transaction.source_change(
str(path),
path.name,
original,
next_source,
mode,
enforce_mode=enforce_mode,
from elydora.plugins import _transaction
from elydora.plugins._managed_files import read_physical_file
from transaction_support import (
make_change,
transaction_subprocess_environment,
write_source,
)
assert change is not None
return change
def test_post_replace_precondition_failure_restores_original_target(
@@ -43,8 +25,8 @@ def test_post_replace_precondition_failure_restores_original_target(
) -> None:
target = tmp_path / "target.txt"
provider = tmp_path / "provider.txt"
_write(target, "old\n")
_write(provider, "stable\n")
write_source(target, "old\n")
write_source(provider, "stable\n")
provider_snapshot = read_physical_file(str(provider), "provider")
real_replace = _transaction._replace_physical
@@ -55,7 +37,7 @@ def test_post_replace_precondition_failure_restores_original_target(
) -> None:
real_replace(source, destination, directory)
if os.path.abspath(destination) == os.path.abspath(str(target)):
_write(provider, "concurrent\n")
write_source(provider, "concurrent\n")
monkeypatch.setattr(
_transaction, "_replace_physical", replace_then_change_provider
@@ -63,7 +45,7 @@ def test_post_replace_precondition_failure_restores_original_target(
with pytest.raises(OSError, match="provider changed during"):
_transaction.write_changes(
[_change(target, "old\n", "new\n")],
[make_change(target, "old\n", "new\n")],
"provider transaction",
[_transaction.FilePrecondition(str(provider), "provider", provider_snapshot)],
)
@@ -78,7 +60,7 @@ def test_commit_boundary_concurrent_write_is_restored_and_rejected(
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
_write(target, "old\n")
write_source(target, "old\n")
real_capture = _transaction._capture_physical
changed = False
@@ -94,14 +76,14 @@ def test_commit_boundary_concurrent_write_is_restored_and_rejected(
and destination.endswith(".rollback")
):
changed = True
_write(target, "concurrent\n")
write_source(target, "concurrent\n")
real_capture(source, destination, directory)
monkeypatch.setattr(_transaction, "_capture_physical", write_before_capture)
with pytest.raises(OSError, match="changed at commit boundary"):
_transaction.write_changes(
[_change(target, "old\n", "new\n")],
[make_change(target, "old\n", "new\n")],
"commit boundary",
)
@@ -116,7 +98,7 @@ def test_existing_target_remains_visible_during_atomic_update(
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
_write(target, "old\n")
write_source(target, "old\n")
replace_started = threading.Event()
release_replace = threading.Event()
failures: list[BaseException] = []
@@ -135,7 +117,7 @@ def test_existing_target_remains_visible_during_atomic_update(
def update() -> None:
try:
_transaction.write_changes(
[_change(target, "old\n", "new\n")],
[make_change(target, "old\n", "new\n")],
"visible atomic update",
)
except BaseException as error:
@@ -156,13 +138,6 @@ def test_existing_target_remains_visible_during_atomic_update(
assert target.read_text(encoding="utf-8") == "new\n"
def _transaction_subprocess_environment(home: Path) -> dict[str, str]:
environment = os.environ.copy()
environment["HOME"] = str(home)
environment["USERPROFILE"] = str(home)
return environment
def test_process_exit_after_atomic_replace_recovers_on_restart(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
@@ -170,7 +145,7 @@ def test_process_exit_after_atomic_replace_recovers_on_restart(
home = tmp_path / "home"
home.mkdir()
target = tmp_path / "target.txt"
_write(target, "old\n")
write_source(target, "old\n")
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home))
script = """
@@ -193,7 +168,7 @@ _transaction.write_changes([change], "crash recovery")
completed = subprocess.run(
[sys.executable, "-c", script, str(target)],
cwd=Path(__file__).resolve().parents[1],
env=_transaction_subprocess_environment(home),
env=transaction_subprocess_environment(home),
check=False,
)
@@ -212,7 +187,7 @@ def test_process_exit_after_committed_marker_keeps_atomic_update(
home = tmp_path / "home"
home.mkdir()
target = tmp_path / "target.txt"
_write(target, "old\n")
write_source(target, "old\n")
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home))
script = """
@@ -230,7 +205,7 @@ _transaction.write_changes([change], "committed crash recovery")
completed = subprocess.run(
[sys.executable, "-c", script, str(target)],
cwd=Path(__file__).resolve().parents[1],
env=_transaction_subprocess_environment(home),
env=transaction_subprocess_environment(home),
check=False,
)
@@ -250,8 +225,8 @@ def test_process_exit_mid_transaction_restores_all_targets(
home.mkdir()
first = tmp_path / "first.txt"
second = tmp_path / "second.txt"
_write(first, "first-old\n")
_write(second, "second-old\n")
write_source(first, "first-old\n")
write_source(second, "second-old\n")
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home))
script = """
@@ -281,7 +256,7 @@ _transaction.write_changes(changes, "multi-file crash recovery")
completed = subprocess.run(
[sys.executable, "-c", script, str(first), str(second)],
cwd=Path(__file__).resolve().parents[1],
env=_transaction_subprocess_environment(home),
env=transaction_subprocess_environment(home),
check=False,
)
@@ -313,7 +288,7 @@ def test_process_exit_recovers_atomic_create_and_remove(
home.mkdir()
target = tmp_path / "target.txt"
if original is not None:
_write(target, original)
write_source(target, original)
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home))
script = """
@@ -343,7 +318,7 @@ _transaction.write_changes([change], "create-remove crash recovery")
str(exit_code),
],
cwd=Path(__file__).resolve().parents[1],
env=_transaction_subprocess_environment(home),
env=transaction_subprocess_environment(home),
check=False,
)
@@ -366,8 +341,8 @@ def test_cross_process_writers_are_serialized(
ready = tmp_path / "ready"
second_ready = tmp_path / "second-ready"
release = tmp_path / "release"
_write(target, "old\n")
environment = _transaction_subprocess_environment(home)
write_source(target, "old\n")
environment = transaction_subprocess_environment(home)
first_script = """
import pathlib
import sys
@@ -443,632 +418,3 @@ except OSError as error:
assert second.returncode == 19, output
assert "changed during" in output
assert target.read_text(encoding="utf-8") == "first\n"
def test_final_replace_exception_after_success_restores_original(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
_write(target, "old\n")
real_replace = _transaction._replace_physical
def replace_then_raise(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
real_replace(source, destination, directory)
if os.path.abspath(destination) == os.path.abspath(str(target)):
raise OSError("injected post-replace failure")
monkeypatch.setattr(_transaction, "_replace_physical", replace_then_raise)
with pytest.raises(OSError, match="injected post-replace failure"):
_transaction.write_changes(
[_change(target, "old\n", "new\n")],
"post-replace failure",
)
assert target.read_text(encoding="utf-8") == "old\n"
assert list(tmp_path.glob(".*.rollback")) == []
def test_capture_exception_after_success_restores_original(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
_write(target, "old\n")
real_capture = _transaction._capture_physical
def capture_then_raise(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
real_capture(source, destination, directory)
raise OSError("injected post-capture failure")
monkeypatch.setattr(_transaction, "_capture_physical", capture_then_raise)
with pytest.raises(OSError, match="injected post-capture failure"):
_transaction.write_changes(
[_change(target, "old\n", "new\n")],
"post-capture failure",
)
assert target.read_text(encoding="utf-8") == "old\n"
assert list(tmp_path.glob(".*.rollback")) == []
def test_rollback_reservation_collision_preserves_victim_and_original(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
_write(target, "old\n")
victim = "concurrent rollback destination\n"
collision_path: Path | None = None
def collide_with_rollback_destination(
_source: str,
destination: str,
_directory: _transaction.PinnedDirectory,
) -> None:
nonlocal collision_path
if destination.endswith(".rollback"):
collision_path = Path(destination)
_write(collision_path, victim)
monkeypatch.setattr(
_transaction,
"_capture_physical",
collide_with_rollback_destination,
)
with pytest.raises(OSError, match="exist"):
_transaction.write_changes(
[_change(target, "old\n", "new\n")],
"rollback destination collision",
)
assert collision_path is not None
assert target.read_text(encoding="utf-8") == "old\n"
assert collision_path.read_text(encoding="utf-8") == victim
def test_final_install_does_not_overwrite_concurrent_target(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
_write(target, "old\n")
real_replace = _transaction._replace_physical
injected = False
def create_target_before_install(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal injected
if not injected and os.path.abspath(destination) == os.path.abspath(target):
injected = True
_write(target, "concurrent\n")
real_replace(source, destination, directory)
monkeypatch.setattr(
_transaction,
"_replace_physical",
create_target_before_install,
)
with pytest.raises(OSError, match="original content preserved"):
_transaction.write_changes(
[_change(target, "old\n", "new\n")],
"no-clobber install",
)
assert injected
assert target.read_text(encoding="utf-8") == "concurrent\n"
rollbacks = list(tmp_path.glob(".*.rollback"))
assert len(rollbacks) == 1
assert rollbacks[0].read_text(encoding="utf-8") == "old\n"
def test_restore_does_not_overwrite_concurrent_target(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
second = tmp_path / "second.txt"
_write(target, "old\n")
_write(second, "second-old\n")
real_replace = _transaction._replace_physical
real_restore = _transaction._restore_physical
injected = False
def fail_second_commit(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(second):
raise OSError("injected second failure")
real_replace(source, destination, directory)
def create_target_before_restore(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal injected
if not injected and os.path.abspath(destination) == os.path.abspath(target):
injected = True
_write(target, "concurrent recovery\n")
real_restore(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", fail_second_commit)
monkeypatch.setattr(
_transaction,
"_restore_physical",
create_target_before_restore,
)
with pytest.raises(OSError, match="original content preserved"):
_transaction.write_changes(
[
_change(target, "old\n", "new\n"),
_change(second, "second-old\n", "second-new\n"),
],
"no-clobber restoration",
)
assert injected
assert target.read_text(encoding="utf-8") == "concurrent recovery\n"
rollbacks = list(tmp_path.glob(".*.rollback"))
assert len(rollbacks) == 1
assert rollbacks[0].read_text(encoding="utf-8") == "old\n"
assert Path(_transaction.active_journal_path()).is_file()
_transaction.clear_journal()
def test_restore_source_replacement_reports_existing_preserved_path(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
target = tmp_path / "target.txt"
second = tmp_path / "second.txt"
displaced = tmp_path / "displaced-original.txt"
_write(target, "old\n")
_write(second, "second-old\n")
real_replace = _transaction._replace_physical
real_restore = _transaction._restore_physical
rollback_path: Path | None = None
def fail_second_commit(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(second):
raise OSError("injected second failure")
real_replace(source, destination, directory)
def replace_restore_source(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal rollback_path
if (
rollback_path is None
and source.endswith(".rollback")
and os.path.abspath(destination) == os.path.abspath(target)
):
rollback_path = Path(source)
rollback_path.rename(displaced)
_write(rollback_path, "concurrent recovery source\n")
real_restore(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", fail_second_commit)
monkeypatch.setattr(_transaction, "_restore_physical", replace_restore_source)
with pytest.raises(OSError, match="recovery source changed") as raised:
_transaction.write_changes(
[
_change(target, "old\n", "new\n"),
_change(second, "second-old\n", "second-new\n"),
],
"restore source replacement",
)
assert rollback_path is not None
assert f"concurrent data preserved at {rollback_path}" in str(raised.value)
assert rollback_path.read_text(encoding="utf-8") == "concurrent recovery source\n"
assert displaced.read_text(encoding="utf-8") == "old\n"
assert target.read_text(encoding="utf-8") == "new\n"
assert second.read_text(encoding="utf-8") == "second-old\n"
assert Path(_transaction.active_journal_path()).is_file()
_transaction.clear_journal()
def test_creation_rollback_captures_and_preserves_concurrent_replacement(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
created = tmp_path / "created.txt"
second = tmp_path / "second.txt"
_write(second, "second-old\n")
real_replace = _transaction._replace_physical
real_capture = _transaction._capture_physical
injected = False
def fail_second_commit(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(second):
raise OSError("injected second failure")
real_replace(source, destination, directory)
def replace_created_before_recovery_capture(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal injected
if (
not injected
and os.path.abspath(source) == os.path.abspath(created)
and destination.endswith(".recovery")
):
injected = True
_write(created, "concurrent replacement\n")
real_capture(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", fail_second_commit)
monkeypatch.setattr(
_transaction,
"_capture_physical",
replace_created_before_recovery_capture,
)
with pytest.raises(OSError, match="changed at recovery boundary"):
_transaction.write_changes(
[
_change(created, None, "generated\n"),
_change(second, "second-old\n", "second-new\n"),
],
"creation recovery capture",
)
assert injected
assert created.read_text(encoding="utf-8") == "concurrent replacement\n"
assert Path(_transaction.active_journal_path()).is_file()
_transaction.clear_journal()
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode contract")
def test_mode_repair_uses_private_rollback_and_restores_original_mode(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
secret = tmp_path / "private.key"
second = tmp_path / "config.json"
_write(secret, "secret\n")
_write(second, "old\n")
secret.chmod(0o644)
real_replace = _transaction._replace_physical
rollback_modes: list[int] = []
def inspect_then_fail_second(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(str(secret)):
rollback = next(tmp_path.glob(".private.key.*.rollback"))
rollback_modes.append(rollback.stat().st_mode & 0o777)
if os.path.abspath(destination) == os.path.abspath(str(second)):
raise OSError("injected second commit failure")
real_replace(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", inspect_then_fail_second)
with pytest.raises(OSError, match="injected second commit failure"):
_transaction.write_changes(
[
_change(secret, "secret\n", "secret\n", enforce_mode=True),
_change(second, "old\n", "new\n"),
],
"mode repair",
)
assert rollback_modes
assert set(rollback_modes) == {0o600}
assert secret.read_text(encoding="utf-8") == "secret\n"
assert secret.stat().st_mode & 0o777 == 0o644
assert second.read_text(encoding="utf-8") == "old\n"
assert list(tmp_path.glob(".*.rollback")) == []
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode contract")
def test_deletion_uses_private_rollback_and_restores_original_mode(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
removed = tmp_path / "removed.txt"
second = tmp_path / "second.txt"
_write(removed, "remove me\n")
_write(second, "old\n")
removed.chmod(0o644)
real_replace = _transaction._replace_physical
rollback_modes: list[int] = []
def inspect_then_fail_second(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
if os.path.abspath(destination) == os.path.abspath(str(second)):
rollback = next(tmp_path.glob(".removed.txt.*.rollback"))
rollback_modes.append(rollback.stat().st_mode & 0o777)
raise OSError("injected second commit failure")
real_replace(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", inspect_then_fail_second)
with pytest.raises(OSError, match="injected second commit failure"):
_transaction.write_changes(
[
_change(removed, "remove me\n", None),
_change(second, "old\n", "new\n"),
],
"deletion rollback mode",
)
assert rollback_modes == [0o600]
assert removed.read_text(encoding="utf-8") == "remove me\n"
assert removed.stat().st_mode & 0o777 == 0o644
assert second.read_text(encoding="utf-8") == "old\n"
assert list(tmp_path.glob(".*.rollback")) == []
def _swap_directory(parent: Path, backup: Path, redirected: Path) -> None:
parent.rename(backup)
redirected.rename(parent)
@pytest.mark.skipif(os.name == "nt", reason="Windows handle blocks directory rename")
def test_staging_directory_swap_never_writes_secret_to_redirected_path(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
parent = tmp_path / "runtime"
backup = tmp_path / "runtime-original"
redirected = tmp_path / "runtime-redirected"
parent.mkdir()
redirected.mkdir()
target = parent / "private.key"
real_write = _transaction_staging.write_staged
swapped = False
def swap_then_stage(*args: object, **kwargs: object):
nonlocal swapped
if not swapped:
swapped = True
_swap_directory(parent, backup, redirected)
return real_write(*args, **kwargs)
monkeypatch.setattr(_transaction_staging, "write_staged", swap_then_stage)
with pytest.raises(OSError, match="changed during transaction"):
_transaction.write_changes(
[_change(target, None, "secret\n")],
"staging swap",
)
assert not (parent / "private.key").exists()
assert not (backup / "private.key").exists()
assert list(parent.iterdir()) == []
assert list(backup.iterdir()) == []
@pytest.mark.skipif(os.name == "nt", reason="Windows handle blocks directory rename")
def test_commit_directory_swap_rolls_back_inside_pinned_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
parent = tmp_path / "runtime"
backup = tmp_path / "runtime-original"
redirected = tmp_path / "runtime-redirected"
parent.mkdir()
redirected.mkdir()
target = parent / "private.key"
real_replace = _transaction._replace_physical
swapped = False
def swap_then_replace(
source: str,
destination: str,
directory: _transaction.PinnedDirectory,
) -> None:
nonlocal swapped
if not swapped:
swapped = True
_swap_directory(parent, backup, redirected)
real_replace(source, destination, directory)
monkeypatch.setattr(_transaction, "_replace_physical", swap_then_replace)
with pytest.raises(OSError, match="changed during transaction"):
_transaction.write_changes(
[_change(target, None, "secret\n")],
"commit swap",
)
assert not (parent / "private.key").exists()
assert not (backup / "private.key").exists()
assert list(parent.iterdir()) == []
assert list(backup.iterdir()) == []
def test_failed_transaction_removes_only_created_empty_directories(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
kiro = tmp_path / ".kiro"
hooks = kiro / "hooks"
hooks.mkdir(parents=True)
kiro_snapshot = read_physical_directory(str(kiro), "Kiro directory")
hooks_snapshot = read_physical_directory(str(hooks), "hooks directory")
target = hooks / "elydora-audit.json"
def fail_replace(
_source: str,
_destination: str,
_directory: _transaction.PinnedDirectory,
) -> None:
raise OSError("injected commit failure")
monkeypatch.setattr(_transaction, "_replace_physical", fail_replace)
with pytest.raises(OSError, match="injected commit failure"):
_transaction.write_changes(
[_change(target, None, "{}\n")],
"created directory recovery",
directory_preconditions=[
_transaction.DirectoryPrecondition(
str(kiro), "Kiro directory", kiro_snapshot, created=True
),
_transaction.DirectoryPrecondition(
str(hooks), "hooks directory", hooks_snapshot, created=True
),
],
)
assert not hooks.exists()
assert not kiro.exists()
def test_created_directory_recovery_keeps_parent_and_child_pinned(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
kiro = tmp_path / ".kiro"
hooks = kiro / "hooks"
hooks.mkdir(parents=True)
kiro_snapshot = read_physical_directory(str(kiro), "Kiro directory")
hooks_snapshot = read_physical_directory(str(hooks), "hooks directory")
target = hooks / "elydora-audit.json"
observed: list[tuple[Path, Path]] = []
real_remove = _transaction_staging.remove_pinned_empty_directory
def observe_pins(
parent: object,
child: object,
*,
require_empty: bool = True,
) -> None:
assert isinstance(parent, _transaction.PinnedDirectory)
assert isinstance(child, _transaction.PinnedDirectory)
assert parent.descriptor is not None
assert child.descriptor is not None
assert os.fstat(child.descriptor).st_ino == child.snapshot.inode
observed.append((Path(parent.path), Path(child.path)))
real_remove(parent, child, require_empty=require_empty)
def fail_replace(
_source: str,
_destination: str,
_directory: _transaction.PinnedDirectory,
) -> None:
raise OSError("injected pinned cleanup failure")
monkeypatch.setattr(
_transaction_staging,
"remove_pinned_empty_directory",
observe_pins,
)
monkeypatch.setattr(_transaction, "_replace_physical", fail_replace)
with pytest.raises(OSError, match="injected pinned cleanup failure"):
_transaction.write_changes(
[_change(target, None, "{}\n")],
"pinned directory recovery",
directory_preconditions=[
_transaction.DirectoryPrecondition(
str(kiro), "Kiro directory", kiro_snapshot, created=True
),
_transaction.DirectoryPrecondition(
str(hooks), "hooks directory", hooks_snapshot, created=True
),
],
)
expected = [(kiro, hooks), (tmp_path, kiro)]
assert observed == expected
assert not kiro.exists()
@pytest.mark.skipif(os.name == "nt", reason="Windows handle blocks directory rename")
def test_created_directory_recovery_preserves_swapped_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
created = tmp_path / "created"
created.mkdir()
created_snapshot = read_physical_directory(str(created), "created directory")
target = created / "managed.json"
replacement = tmp_path / "replacement"
replacement.mkdir()
marker = replacement / "user-owned"
marker.write_text("preserve", encoding="utf-8")
original = tmp_path / "created-original"
real_remove = _transaction_staging.remove_pinned_empty_directory
def swap_before_removal(
parent: _transaction.PinnedDirectory,
child: _transaction.PinnedDirectory,
*,
require_empty: bool = True,
) -> None:
created.rename(original)
replacement.rename(created)
real_remove(parent, child, require_empty=require_empty)
def fail_replace(
_source: str,
_destination: str,
_directory: _transaction.PinnedDirectory,
) -> None:
raise OSError("injected swap cleanup failure")
monkeypatch.setattr(
_transaction_staging,
"remove_pinned_empty_directory",
swap_before_removal,
)
monkeypatch.setattr(_transaction, "_replace_physical", fail_replace)
with pytest.raises(OSError, match="recovery failed"):
_transaction.write_changes(
[_change(target, None, "{}\n")],
"swapped directory recovery",
directory_preconditions=[
_transaction.DirectoryPrecondition(
str(created),
"created directory",
created_snapshot,
created=True,
)
],
)
assert (created / marker.name).read_text(encoding="utf-8") == "preserve"
assert original.is_dir()
assert Path(_transaction.active_journal_path()).is_file()
_transaction.clear_journal()
+42
View File
@@ -0,0 +1,42 @@
from __future__ import annotations
import os
from pathlib import Path
from elydora.plugins import _transaction
def write_source(path: Path, source: str) -> None:
path.write_bytes(source.encode("utf-8"))
def make_change(
path: Path,
original: str | None,
next_source: str | None,
mode: int = 0o600,
enforce_mode: bool = False,
) -> _transaction.FileChange:
change = _transaction.source_change(
str(path),
path.name,
original,
next_source,
mode,
enforce_mode=enforce_mode,
)
assert change is not None
return change
def transaction_subprocess_environment(home: Path) -> dict[str, str]:
environment = os.environ.copy()
environment["HOME"] = str(home)
environment["USERPROFILE"] = str(home)
return environment
def swap_directory(parent: Path, backup: Path, redirected: Path) -> None:
parent.rename(backup)
redirected.rename(parent)