fix(python): sync fail-closed Cursor hooks
This commit is contained in:
1 parent
e9013d0422
commit
cb187de88e
15 files changed
+1402
-718
No files matched your search
@@ -0,0 +1,250 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from cursor_support import (
|
||||
AGENT_ID,
|
||||
prepare_fixture,
|
||||
symlink_or_skip,
|
||||
write_json,
|
||||
write_text,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"runtime_name",
|
||||
[
|
||||
"guard.py",
|
||||
"hook.py",
|
||||
"private.key",
|
||||
"chain-state.json",
|
||||
"status-cache.json",
|
||||
"error.log",
|
||||
],
|
||||
)
|
||||
def test_orphan_runtime_artifacts_are_rejected(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
runtime_name: str,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
artifact = fixture.agent_dir / runtime_name
|
||||
write_text(artifact, "orphan\n")
|
||||
|
||||
with pytest.raises(ValueError, match="identity cannot be verified"):
|
||||
fixture.install()
|
||||
|
||||
assert artifact.read_text(encoding="utf-8") == "orphan\n"
|
||||
assert fixture.config_path.exists() is False
|
||||
|
||||
|
||||
def test_mismatched_runtime_identity_is_rejected_before_writes(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
write_json(
|
||||
fixture.runtime_config_path,
|
||||
{"agent_id": "another-agent", "agent_name": "cursor"},
|
||||
)
|
||||
original = fixture.runtime_config_path.read_text(encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="identity does not match"):
|
||||
fixture.install()
|
||||
|
||||
assert fixture.runtime_config_path.read_text(encoding="utf-8") == original
|
||||
assert fixture.config_path.exists() is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"runtime_name",
|
||||
[
|
||||
"config.json",
|
||||
"private.key",
|
||||
"guard.py",
|
||||
"hook.py",
|
||||
"chain-state.json",
|
||||
"status-cache.json",
|
||||
"error.log",
|
||||
],
|
||||
)
|
||||
def test_symbolic_link_runtime_is_rejected_before_config_writes(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
runtime_name: str,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
target = fixture.home_dir / f"{runtime_name}.target"
|
||||
source = (
|
||||
json.dumps({"agent_id": AGENT_ID, "agent_name": "cursor"})
|
||||
if runtime_name == "config.json"
|
||||
else "target\n"
|
||||
)
|
||||
write_text(target, source)
|
||||
runtime_path = fixture.agent_dir / runtime_name
|
||||
symlink_or_skip(target, runtime_path)
|
||||
|
||||
with pytest.raises(OSError, match="physical file"):
|
||||
fixture.install()
|
||||
|
||||
assert target.read_text(encoding="utf-8") == source
|
||||
assert fixture.config_path.exists() is False
|
||||
|
||||
|
||||
def test_symbolic_link_config_is_rejected_and_target_is_preserved(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
target = fixture.home_dir / "cursor-hooks.target.json"
|
||||
original = '{"version":1,"hooks":{}}\n'
|
||||
write_text(target, original)
|
||||
fixture.config_path.parent.mkdir(parents=True)
|
||||
symlink_or_skip(target, fixture.config_path)
|
||||
|
||||
with pytest.raises(OSError, match="physical file"):
|
||||
fixture.install()
|
||||
|
||||
assert target.read_text(encoding="utf-8") == original
|
||||
assert fixture.config_path.is_symlink()
|
||||
|
||||
|
||||
def test_symbolic_link_config_directory_is_rejected(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
target = fixture.home_dir / "cursor-target"
|
||||
target.mkdir()
|
||||
symlink_or_skip(target, fixture.config_path.parent, directory=True)
|
||||
|
||||
with pytest.raises(OSError, match="not a physical directory"):
|
||||
fixture.install()
|
||||
|
||||
assert list(target.iterdir()) == []
|
||||
|
||||
|
||||
def test_symbolic_link_agent_directory_is_rejected(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
fixture.agent_dir.rmdir()
|
||||
target = fixture.home_dir / "agent-target"
|
||||
target.mkdir()
|
||||
symlink_or_skip(target, fixture.agent_dir, directory=True)
|
||||
|
||||
with pytest.raises(OSError, match="not a physical directory"):
|
||||
fixture.install()
|
||||
|
||||
assert fixture.config_path.exists() is False
|
||||
assert list(target.iterdir()) == []
|
||||
|
||||
|
||||
def test_symbolic_link_runtime_root_is_rejected(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
fixture.agent_dir.rmdir()
|
||||
fixture.agent_dir.parent.rmdir()
|
||||
target = fixture.home_dir / "runtime-target"
|
||||
(target / AGENT_ID).mkdir(parents=True)
|
||||
symlink_or_skip(target, fixture.agent_dir.parent, directory=True)
|
||||
|
||||
with pytest.raises(OSError, match="not a physical directory"):
|
||||
fixture.install()
|
||||
|
||||
assert fixture.config_path.exists() is False
|
||||
assert list((target / AGENT_ID).iterdir()) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"runtime_name",
|
||||
["config.json", "private.key", "guard.py", "hook.py"],
|
||||
)
|
||||
def test_status_rejects_symbolic_link_runtime_files(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
runtime_name: str,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(monkeypatch, tmp_path)
|
||||
fixture.install()
|
||||
runtime_path = fixture.agent_dir / runtime_name
|
||||
target = fixture.home_dir / f"status-{runtime_name}.target"
|
||||
target.write_bytes(runtime_path.read_bytes())
|
||||
runtime_path.unlink()
|
||||
symlink_or_skip(target, runtime_path)
|
||||
|
||||
with pytest.raises(OSError, match="physical file"):
|
||||
fixture.plugin.status()
|
||||
|
||||
|
||||
def test_transaction_rolls_back_all_files_when_config_commit_fails(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
existing_config={"version": 1, "hooks": {"sessionStart": []}},
|
||||
)
|
||||
original = fixture.config_path.read_text(encoding="utf-8")
|
||||
real_replace = os.replace
|
||||
failed = False
|
||||
|
||||
def fail_config_once(source: str, destination: str) -> None:
|
||||
nonlocal failed
|
||||
if os.path.abspath(destination) == str(fixture.config_path) and not failed:
|
||||
failed = True
|
||||
raise OSError("injected config commit failure")
|
||||
real_replace(source, destination)
|
||||
|
||||
monkeypatch.setattr(os, "replace", fail_config_once)
|
||||
|
||||
with pytest.raises(OSError, match="injected config commit failure"):
|
||||
fixture.install()
|
||||
|
||||
assert fixture.config_path.read_text(encoding="utf-8") == original
|
||||
for path in (
|
||||
fixture.guard_path,
|
||||
fixture.hook_path,
|
||||
fixture.runtime_config_path,
|
||||
fixture.private_key_path,
|
||||
):
|
||||
assert path.exists() is False
|
||||
assert not list(fixture.home_dir.rglob("*.tmp"))
|
||||
assert not list(fixture.home_dir.rglob("*.rollback"))
|
||||
|
||||
|
||||
def test_transaction_detects_concurrent_config_change(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from elydora.plugins._transaction import write_changes
|
||||
from elydora.plugins.cursor_contract import render_document
|
||||
from elydora.plugins.cursor_io import read_document, rendered_change
|
||||
|
||||
fixture = prepare_fixture(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
existing_config={
|
||||
"version": 1,
|
||||
"hooks": {"sessionStart": [{"command": "original"}]},
|
||||
},
|
||||
)
|
||||
document = read_document()
|
||||
change = rendered_change(render_document(document, {}))
|
||||
assert change is not None
|
||||
concurrent = '{"version":1,"hooks":{"sessionStart":[]}}\n'
|
||||
fixture.config_path.write_text(concurrent, encoding="utf-8")
|
||||
|
||||
with pytest.raises(OSError, match="changed during installation"):
|
||||
write_changes([change], "Update Cursor hooks")
|
||||
|
||||
assert fixture.config_path.read_text(encoding="utf-8") == concurrent
|
||||
assert not list(fixture.config_path.parent.glob("*.tmp"))
|
||||
Reference in new issue
Block a user