Rebuilt from the standalone 54e65eb tree (durable transactions, Kiro IDE adapter, hook retry with a bounded chain lock, version 2.0.2) with the Open-Source API trims re-applied: no token rotation, token issuance, or agent integration updates, and the mirror's registration contract and OpenCode plugin tests kept. Claude-Session: https://claude.ai/code/session_01QoUjdc7HGKTsgj5GhG8HvF
170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
import subprocess # nosec B404
|
|
import sys
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from elydora.plugins import _transaction_lock
|
|
|
|
|
|
if os.name == "nt":
|
|
from elydora.plugins._windows_security import verify_private_path
|
|
|
|
|
|
def _use_isolated_state(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
root: Path,
|
|
) -> None:
|
|
monkeypatch.setenv("HOME", str(root))
|
|
monkeypatch.setenv("USERPROFILE", str(root))
|
|
|
|
|
|
def test_transaction_state_directory_has_owner_boundary(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
_use_isolated_state(monkeypatch, tmp_path)
|
|
|
|
state = Path(_transaction_lock.transaction_state_directory())
|
|
|
|
assert state.is_dir()
|
|
if os.name == "nt":
|
|
verify_private_path(str(state), directory=True)
|
|
else:
|
|
metadata = state.stat()
|
|
assert metadata.st_uid == getattr(os, "geteuid")()
|
|
assert stat.S_IMODE(metadata.st_mode) == 0o700
|
|
|
|
|
|
@pytest.mark.skipif(os.name != "posix", reason="POSIX owner contract")
|
|
def test_transaction_state_directory_rejects_foreign_owner(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
_use_isolated_state(monkeypatch, tmp_path)
|
|
state = Path(_transaction_lock.transaction_state_directory())
|
|
owner = state.stat().st_uid
|
|
monkeypatch.setattr(os, "geteuid", lambda: owner + 1)
|
|
|
|
with pytest.raises(PermissionError, match="owned by the current user"):
|
|
_transaction_lock.transaction_state_directory()
|
|
|
|
|
|
@pytest.mark.skipif(os.name != "posix", reason="POSIX owner contract")
|
|
def test_posix_lock_rejects_foreign_owner(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
lock_path = tmp_path / "transaction.lock"
|
|
lock_path.write_bytes(b"")
|
|
owner = lock_path.stat().st_uid
|
|
monkeypatch.setattr(
|
|
_transaction_lock,
|
|
"transaction_state_directory",
|
|
lambda: str(tmp_path),
|
|
)
|
|
monkeypatch.setattr(os, "geteuid", lambda: owner + 1)
|
|
|
|
with pytest.raises(PermissionError, match="owned by the current user"):
|
|
_transaction_lock._PosixLock().__enter__()
|
|
|
|
|
|
def test_transaction_lock_rejects_reparse_path(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
_use_isolated_state(monkeypatch, tmp_path)
|
|
state = Path(_transaction_lock.transaction_state_directory())
|
|
target = tmp_path / "target.lock"
|
|
target.write_bytes(b"")
|
|
lock_path = state / "transaction.lock"
|
|
try:
|
|
lock_path.symlink_to(target)
|
|
except OSError as error:
|
|
pytest.skip(f"Symbolic links unavailable: {error}")
|
|
|
|
with pytest.raises(OSError, match="physical file"):
|
|
with _transaction_lock.serialized_transactions():
|
|
pass
|
|
|
|
|
|
@pytest.mark.skipif(os.name != "nt", reason="Windows DACL contract")
|
|
def test_windows_lock_file_has_owner_only_protected_dacl(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
_use_isolated_state(monkeypatch, tmp_path)
|
|
state = Path(_transaction_lock.transaction_state_directory())
|
|
|
|
with _transaction_lock.serialized_transactions():
|
|
lock_path = state / "transaction.lock"
|
|
assert lock_path.is_file()
|
|
verify_private_path(str(lock_path), directory=False)
|
|
|
|
|
|
def test_transaction_lock_serializes_independent_processes(tmp_path: Path) -> None:
|
|
state_root = tmp_path / "state-root"
|
|
state_root.mkdir()
|
|
entered = tmp_path / "entered"
|
|
release = tmp_path / "release"
|
|
second_entered = tmp_path / "second-entered"
|
|
script = """
|
|
import pathlib
|
|
import sys
|
|
import time
|
|
from elydora.plugins._transaction_lock import serialized_transactions
|
|
|
|
entered, release = map(pathlib.Path, sys.argv[1:3])
|
|
with serialized_transactions():
|
|
entered.write_text("entered", encoding="utf-8")
|
|
if len(sys.argv) == 4:
|
|
deadline = time.monotonic() + 10
|
|
while not release.exists():
|
|
if time.monotonic() >= deadline:
|
|
raise TimeoutError("release gate timed out")
|
|
time.sleep(0.01)
|
|
"""
|
|
environment = os.environ.copy()
|
|
environment["HOME"] = str(state_root)
|
|
environment["USERPROFILE"] = str(state_root)
|
|
repository = Path(__file__).resolve().parents[1]
|
|
first = subprocess.Popen( # nosec B603
|
|
[sys.executable, "-c", script, str(entered), str(release), "wait"],
|
|
cwd=repository,
|
|
env=environment,
|
|
)
|
|
second: subprocess.Popen[bytes] | None = None
|
|
try:
|
|
deadline = time.monotonic() + 10
|
|
while not entered.exists() and first.poll() is None:
|
|
if time.monotonic() >= deadline:
|
|
raise TimeoutError("first process did not acquire the lock")
|
|
time.sleep(0.01)
|
|
assert first.poll() is None
|
|
|
|
second = subprocess.Popen( # nosec B603
|
|
[sys.executable, "-c", script, str(second_entered), str(release)],
|
|
cwd=repository,
|
|
env=environment,
|
|
)
|
|
time.sleep(0.25)
|
|
assert second.poll() is None
|
|
assert not second_entered.exists()
|
|
|
|
release.write_text("release", encoding="utf-8")
|
|
assert first.wait(timeout=10) == 0
|
|
assert second.wait(timeout=10) == 0
|
|
assert second_entered.read_text(encoding="utf-8") == "entered"
|
|
finally:
|
|
if first.poll() is None:
|
|
first.kill()
|
|
first.wait()
|
|
if second is not None and second.poll() is None:
|
|
second.kill()
|
|
second.wait()
|