Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions tests/integration/test_audit_primitive_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,12 +702,20 @@ def test_preserved_nested_command_layout_is_visible_non_prompt(
marker = project / "HOOK_MUST_NOT_RUN"
command = {"type": "command", "command": f"touch {marker}{_BIDI}"}
entry = {"matcher": "*", "hooks": [command]} if nested else command
payload = json.dumps({"version": 1, "hooks": {"PreToolUse": [entry]}})
event = "preToolUse" if target == "cursor" else "PreToolUse"
payload = json.dumps({"version": 1, "hooks": {event: [entry]}})
path.write_text(payload, encoding="utf-8")
result = scan_project_result(project, targets=(profile,))
assert result.incomplete == ()
assert result.findings_by_file == {}
assert [entry.status for entry in result.inventory] == ["not-applicable"]
if target == "cursor" and nested:
# Cursor loads only flat handlers, so a nested group is reported, not read.
assert [(entry.pointer, entry.diagnostic) for entry in result.incomplete] == [
("/hooks/preToolUse/0", "native hook event requires flat handlers")
]
assert [entry.status for entry in result.inventory] == ["incomplete"]
else:
assert result.incomplete == ()
assert [entry.status for entry in result.inventory] == ["not-applicable"]
assert path.read_text() == payload
assert not list(project.glob("HOOK_MUST_NOT_RUN*"))

Expand Down
31 changes: 15 additions & 16 deletions tests/integration/test_hook_root_source_drift_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _run(apm_binary_path: Path, cwd: Path, *args: str) -> subprocess.CompletedPr
"cursor",
".cursor/hooks.json",
".cursor/apm-hooks.json",
"PreToolUse",
"preToolUse",
id="cursor",
),
pytest.param(
Expand All @@ -68,6 +68,17 @@ def _run(apm_binary_path: Path, cwd: Path, *args: str) -> subprocess.CompletedPr
),
]

# Cursor's native schema accepts only flat handlers, so a nested user entry
# would make the second install reject the config instead of preserving it.
_FLAT_HANDLER_TARGETS = frozenset({"cursor"})


def _user_owned_entry(target: str) -> dict:
command = {"type": "command", "command": "echo user-owned"}
if target in _FLAT_HANDLER_TARGETS:
return command
return {"matcher": "Bash", "hooks": [command]}


def _load_sources(
project: Path,
Expand Down Expand Up @@ -178,12 +189,8 @@ def test_root_hook_source_drift_heals_on_reinstall(
# Append a user-owned entry directly to the settings file (never in the sidecar).
settings_path = project / settings_rel
settings_data = json.loads(settings_path.read_text(encoding="utf-8"))
settings_data.setdefault("hooks", {}).setdefault(event_key, []).append(
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "echo user-owned"}],
}
)
user_entry = _user_owned_entry(target)
settings_data.setdefault("hooks", {}).setdefault(event_key, []).append(user_entry)
settings_path.write_text(json.dumps(settings_data), encoding="utf-8")

# Second install: must heal the stale marker without touching the user-owned entry.
Expand All @@ -197,15 +204,7 @@ def test_root_hook_source_drift_heals_on_reinstall(

settings_data = json.loads(settings_path.read_text(encoding="utf-8"))
entries = settings_data.get("hooks", {}).get(event_key, [])
user_owned = [
e
for e in entries
if isinstance(e, dict)
and isinstance(e.get("hooks"), list)
and e["hooks"]
and isinstance(e["hooks"][0], dict)
and e["hooks"][0].get("command") == "echo user-owned"
]
user_owned = [e for e in entries if e == user_entry]
assert len(user_owned) == 1, (
f"User-owned hook entry must survive healing for {target}; entries={entries}"
)
Expand Down