From 22182e0fd3b0a87937291609e3181bf57d7ddedd Mon Sep 17 00:00:00 2001 From: gradata-eng Date: Tue, 26 May 2026 14:54:16 -0700 Subject: [PATCH 1/3] test: add install smoke matrix --- Gradata/tests/test_install_smoke_matrix.py | 115 +++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 Gradata/tests/test_install_smoke_matrix.py diff --git a/Gradata/tests/test_install_smoke_matrix.py b/Gradata/tests/test_install_smoke_matrix.py new file mode 100644 index 00000000..231d4475 --- /dev/null +++ b/Gradata/tests/test_install_smoke_matrix.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tomllib +from pathlib import Path + +import pytest + + +HOST_MATRIX = ( + pytest.param( + "claude-code", + Path(".claude/settings.json"), + {"PreToolUse"}, + id="claude-code", + ), + pytest.param( + "codex", + Path(".codex/config.toml"), + {"pre_tool"}, + id="codex", + ), + pytest.param( + "hermes", + Path(".hermes/config.yaml"), + {"pre_tool_call"}, + id="hermes", + ), + pytest.param( + "opencode", + Path(".config/opencode/config.json"), + {"preTool"}, + id="opencode", + ), +) + + +def _run_install(tmp_path: Path, host: str, brain: Path) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + for key in list(env): + if key.startswith("GRADATA_"): + env.pop(key, None) + env["HOME"] = str(tmp_path) + env["USERPROFILE"] = str(tmp_path) + env["XDG_CONFIG_HOME"] = str(tmp_path / ".config") + env["PYTHONPATH"] = str(Path.cwd() / "src") + return subprocess.run( + [sys.executable, "-m", "gradata.cli", "install", "--agent", host, "--brain", str(brain)], + cwd=Path.cwd(), + env=env, + text=True, + capture_output=True, + check=False, + ) + + +@pytest.mark.parametrize(("host", "config_relpath", "expected_events"), HOST_MATRIX) +def test_cli_install_smoke_matrix_writes_host_hook_config( + tmp_path: Path, + host: str, + config_relpath: Path, + expected_events: set[str], +) -> None: + """Smoke the current Python CLI install path for supported hook hosts. + + The deprecated npm one-command installer is intentionally not part of this + matrix: it predates Hermes/OpenCode and is documented as superseded by the + Python package install path. The matrix pins the hook events emitted by the + current main-branch adapters; hosts with only a pre-tool hook are covered by + that supported path instead of silently pretending they have broader hooks. + """ + brain = tmp_path / "brain" + brain.mkdir() + + result = _run_install(tmp_path, host, brain) + + assert result.returncode == 0, result.stderr + assert host in result.stdout + config_path = tmp_path / config_relpath + assert config_path.exists(), f"missing {host} config at {config_path}" + + content = config_path.read_text(encoding="utf-8") + assert f"gradata:{host}:" in content + assert "BRAIN_DIR=" in content + assert str(brain) in content + + if host == "claude-code": + settings = json.loads(content) + hooks = settings["hooks"] + assert expected_events <= set(hooks) + for event in expected_events: + assert hooks[event], f"missing Claude hook entries for {event}" + elif host == "codex": + config = tomllib.loads(content) + pre_tool_hooks = config["hooks"]["pre_tool"] + assert pre_tool_hooks[0]["id"].startswith("gradata:codex:") + assert "gradata.hooks.inject_brain_rules" in pre_tool_hooks[0]["command"] + elif host == "hermes": + for event in expected_events: + assert f"{event}:" in content + # Hermes ignores Claude-style legacy names; pin the supported event names. + legacy_event_lines = {line.strip() for line in content.splitlines()} + assert "pre_tool_use:" not in legacy_event_lines + assert "post_tool_use:" not in legacy_event_lines + assert "session_end:" not in legacy_event_lines + elif host == "opencode": + config = json.loads(content) + pre_tool_hooks = config["hooks"]["preTool"] + assert pre_tool_hooks[0]["id"].startswith("gradata:opencode:") + assert "gradata.hooks.inject_brain_rules" in pre_tool_hooks[0]["command"] + else: # pragma: no cover - matrix guard + raise AssertionError(f"unhandled host {host}") From 051a7fd12b8b01172367ef68d77415ce4e2aad78 Mon Sep 17 00:00:00 2001 From: data-engineer Date: Tue, 26 May 2026 15:18:51 -0700 Subject: [PATCH 2/3] test: harden install smoke matrix --- Gradata/tests/test_install_smoke_matrix.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Gradata/tests/test_install_smoke_matrix.py b/Gradata/tests/test_install_smoke_matrix.py index 231d4475..9b6da88a 100644 --- a/Gradata/tests/test_install_smoke_matrix.py +++ b/Gradata/tests/test_install_smoke_matrix.py @@ -2,6 +2,7 @@ import json import os +import re import subprocess import sys import tomllib @@ -35,6 +36,13 @@ {"preTool"}, id="opencode", ), + pytest.param( + "cursor", + Path(".cursor/mcp.json"), + set(), + marks=pytest.mark.skip(reason="GRA-1680: Cursor is MCP-only; no hook/slash-command install path to smoke-test"), + id="cursor-mcp-only-skipped", + ), ) @@ -85,7 +93,7 @@ def test_cli_install_smoke_matrix_writes_host_hook_config( content = config_path.read_text(encoding="utf-8") assert f"gradata:{host}:" in content assert "BRAIN_DIR=" in content - assert str(brain) in content + assert brain.resolve().as_posix() in content if host == "claude-code": settings = json.loads(content) @@ -102,10 +110,10 @@ def test_cli_install_smoke_matrix_writes_host_hook_config( for event in expected_events: assert f"{event}:" in content # Hermes ignores Claude-style legacy names; pin the supported event names. - legacy_event_lines = {line.strip() for line in content.splitlines()} - assert "pre_tool_use:" not in legacy_event_lines - assert "post_tool_use:" not in legacy_event_lines - assert "session_end:" not in legacy_event_lines + legacy_event_lines = content.splitlines() + assert not any(re.search(r"(^|\s)pre_tool_use\s*:", line) for line in legacy_event_lines) + assert not any(re.search(r"(^|\s)post_tool_use\s*:", line) for line in legacy_event_lines) + assert not any(re.search(r"(^|\s)session_end\s*:", line) for line in legacy_event_lines) elif host == "opencode": config = json.loads(content) pre_tool_hooks = config["hooks"]["preTool"] From b2cbe1fe9bcea2f506e6b64eac38522fe9e6eb6c Mon Sep 17 00:00:00 2001 From: gradata-eng Date: Thu, 28 May 2026 20:28:16 -0700 Subject: [PATCH 3/3] fix: close install verifier brain on Windows --- Gradata/src/gradata/cli.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Gradata/src/gradata/cli.py b/Gradata/src/gradata/cli.py index a752674a..ae1360b9 100644 --- a/Gradata/src/gradata/cli.py +++ b/Gradata/src/gradata/cli.py @@ -514,16 +514,25 @@ def _cmd_install_agent(args) -> None: from gradata import Brain verification_marker = f"gradata-install-verify-{name}-{os.urandom(4).hex()}" - with tempfile.TemporaryDirectory(prefix="gradata-verify-") as verification_tmp: + with tempfile.TemporaryDirectory( + prefix="gradata-verify-", + ignore_cleanup_errors=True, + ) as verification_tmp: verification_dir = Path(verification_tmp) / "brain" - Brain.init(verification_dir) - verification_brain = Brain(verification_dir) - correction = verification_brain.correct( - draft=f"test draft for {name} install verification {verification_marker}", - final=f"test final for {name} install verification {verification_marker}", - dry_run=False, - ) - results = verification_brain.search(verification_marker, mode="rules", top_k=3) + verification_brain = Brain.init(verification_dir) + try: + verification_brain.correct( + draft=f"test draft for {name} install verification {verification_marker}", + final=f"test final for {name} install verification {verification_marker}", + dry_run=False, + ) + results = verification_brain.search( + verification_marker, + mode="rules", + top_k=3, + ) + finally: + verification_brain.close() marker_found = any( verification_marker in (r.get("text") or "").lower() for r in results )