Skip to content

feat(install): gradata uninstall --agent <host> (GRA-1241 / GH #206)#215

Merged
Gradata merged 1 commit into
mainfrom
feat/gradata-uninstall-subcommand-gra-1241
May 20, 2026
Merged

feat(install): gradata uninstall --agent <host> (GRA-1241 / GH #206)#215
Gradata merged 1 commit into
mainfrom
feat/gradata-uninstall-subcommand-gra-1241

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented May 20, 2026

Summary

Adds symmetric gradata uninstall --agent <host> for every supported host (claude-code, codex, gemini, cursor, hermes, opencode, or all). Reverses what gradata install --agent <host> writes, with user-edit detection via SHA256 against the install manifest.

Architecture

New _install_manifest.py module at ~/.gradata/install_manifest.json:

  • Records (config_path, signature, sha256_after_install) per host on every successful install
  • Idempotent record/get/drop helpers
  • Survives upgrade / re-install — used by uninstall to detect user edits

Per-host uninstall() method on each adapter:

Host Config shape Approach
claude-code hooks.PreToolUse[] (JSON) Bespoke list-walk
cursor mcpServers.gradata (JSON) Bespoke key-pop
gemini tools.preCall[] (JSON) Shared helper
opencode hooks.preTool[] (JSON) Shared helper
hermes hooks.pre_tool_call[] (YAML) Bespoke (YAML not JSON)
codex [[hooks.pre_tool]] (TOML) Bespoke table-walker

Shared JSON helper _base.uninstall_from_list_in_dict(outer_key, inner_key) for the 3 JSON list-shaped configs.

CLI: cmd_uninstall matches install's --agent choices, reads manifest record OR falls back to canonical config path for legacy installs, SHA256 check, dispatches to per-host uninstall.

Action type expanded from added | already_present | failed to include removed.

cmd_install now records to the manifest on every successful install (best-effort, never fails the install).

Behaviour guarantees

  • Preserves user customizations: if a config file's SHA differs from the recorded SHA, prints skipped <host> — modified since install and leaves it. Never silently deletes user changes.
  • Idempotent: running uninstall twice returns already_present on the second call.
  • Empty containers pruned: hooks: {} after removing the last entry is cleaned up.
  • Preserves user-owned entries: any PreToolUse entries that don't carry our signature survive uninstall verbatim.

Test plan

pytest tests/test_uninstall_command.py
=> 11 passed in 0.44s

11 cases: round-trip for each of the 6 adapters, preserves user-owned entries, idempotent when never installed, missing config no-crash, manifest record/get/sha256 round trip, CLI clean error on unknown agent.

Live smoke

gradata uninstall --agent claude-code --brain /tmp/test-brain
→ ✓ claude-code → /home/olive/.claude/settings.json (already_present)

Diff size

10 files / +687 / -7. Largest additions: tests/ (168), _install_manifest.py (142), cli.py (+120 for cmd_uninstall + manifest recording in cmd_install).

Risk

Medium. New surface that mutates user config files. Mitigated by:

  • SHA256 check skips files the user has edited post-install
  • Idempotent: re-running is safe
  • Failure cases return InstallResult.action='failed' with a clear message, never raise
  • Other PreToolUse / mcpServers entries are preserved verbatim

Epic context

GRA-1198 (kill the plugin) / GH #206. Now that the SDK ships every subcommand the plugin had (PRs #208/#209/#210/#211 + #213), users who try the SDK and bounce off can cleanly uninstall.

Authoring note

Started by a delegate_task subagent (kanban t_a84fecce); subagent hit max_iterations after writing the manifest module + cli.py + scaffolding but before writing adapter uninstall() methods. Parent agent completed the 6 adapter implementations + the 11-case test suite + lint + ship. The cross-fleet artifact-URL rule verified the work end-to-end.

…1198)

Adds symmetric uninstall for every supported host: claude-code, codex,
gemini, cursor, hermes, opencode (or --agent all). Reverses what
gradata install --agent <host> writes, with user-edit detection via
SHA256 against the install manifest.

Architecture:
- New `_install_manifest.py` module: records (config_path, signature,
  sha256_after_install) per host at `~/.gradata/install_manifest.json`
  on every successful install. Idempotent record/get/drop helpers.
- Each adapter grows `uninstall(brain_dir, config_path) -> InstallResult`
  symmetrical to its existing `install()`. Per-host config shape:
  - claude-code: hooks.PreToolUse[] (JSON)
  - cursor: mcpServers.gradata (JSON)
  - gemini: tools.preCall[] (JSON)
  - opencode: hooks.preTool[] (JSON)
  - hermes: hooks.pre_tool_call[] (YAML)
  - codex: [[hooks.pre_tool]] (TOML)
- Shared helper `_base.uninstall_from_list_in_dict` for the 3 JSON
  list-shaped configs (gemini, opencode); hermes uses YAML so it has
  its own implementation; cursor/claude-code/codex are bespoke.
- New CLI subcommand wired in cmd_uninstall: matches install's --agent
  choices, reads manifest record OR falls back to canonical config path
  for legacy installs, SHA256 check, dispatches to per-host uninstall.
- Action type expanded to include 'removed' (was added|already_present|failed).
- cmd_install now records to the manifest on every successful install.

Preserves user customizations: if the config file's SHA differs from
the recorded SHA, print 'skipped <host> — modified since install' and
leave it. Don't silently delete user changes.

Idempotent: running uninstall twice returns 'already_present' on the
second call. Empty containers (e.g. `hooks: {}`) are pruned.

Test plan: pytest tests/test_uninstall_command.py
=> 11 passed in 0.44s

11 cases: round-trip for each of the 6 adapters, preserves user-owned
entries, idempotent when never installed, missing config no-crash,
manifest record_install/get_record/file_sha256 round trip, CLI clean
error on unknown agent.

Live smoke on oliver-admin:
  gradata uninstall --agent claude-code --brain /tmp/test-brain
  → ✓ claude-code → /home/olive/.claude/settings.json (already_present)

Authoring note: started by a delegate_task subagent which hit
max_iterations on the adapter changes; parent agent completed the
6 adapter uninstall() methods + tests + lint + ship.

Closes GRA-1241. Part of kill-the-plugin epic (GRA-1198 / GH #206).
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Review Change Stack

📝 Walkthrough
  • New symmetric uninstall command: gradata uninstall --agent <host> reverses install, supporting claude-code, codex, gemini, cursor, hermes, opencode, and all hosts
  • Install manifest module (_install_manifest.py): Tracks per-host config path, signature, and SHA256 after install at ~/.gradata/install_manifest.json; survives upgrades and enables user-edit detection
  • Per-adapter uninstall implementations: All 6 adapters now implement uninstall(brain_dir, config_path) to remove SDK-written hook entries idempotently and prune empty containers
  • Extended Action type: Action enum now includes "removed" outcome alongside "added", "already_present", and "failed"
  • Shared uninstall helper: New uninstall_from_list_in_dict() function in _base.py for JSON list-shaped configs (used by gemini and opencode)
  • CLI manifest integration: cmd_install now records installs to manifest (best-effort); cmd_uninstall consults manifest, verifies SHA256, skips if user modified, and dispatches to adapter-specific uninstall
  • Safety guarantees: Idempotent uninstall, skips if SHA differs from install-time, preserves user-owned entries, fails gracefully with InstallResult.action='failed' rather than raising exceptions
  • Comprehensive test coverage: 11 passing tests covering round-trip symmetry for all 6 adapters, user-edit preservation, idempotency, missing config handling, manifest integrity, and CLI error cases

Walkthrough

This PR introduces a complete agent uninstall capability for Gradata. It adds a new install manifest module to track hook installations, extends the CLI with an uninstall command that safely reverses prior installs by validating config SHA256 checksums, implements adapter-specific uninstall functions for six hook targets, and provides comprehensive test coverage across all components.

Changes

Agent Hook Uninstall with Manifest Tracking

Layer / File(s) Summary
Install manifest persistence
Gradata/src/gradata/_install_manifest.py
New module managing per-agent install records (config path, signature, post-install SHA256) under $GRADATA_HOME/install_manifest.json with CRUD operations and atomic JSON save.
CLI uninstall orchestration and manifest integration
Gradata/src/gradata/cli.py
Added cmd_uninstall that resolves target agents, loads manifest records, verifies config SHA256 to detect user edits, calls adapter.uninstall(...), and updates manifest on removal. Records installs to manifest during install --agent ... flow. Extended argparse with uninstall subcommand and dispatch mapping; minor datetime import cleanup in cmd_status.
Base JSON uninstall helper
Gradata/src/gradata/hooks/adapters/_base.py
Updated Action type with "removed" outcome. Added uninstall_from_list_in_dict() helper for JSON configs with nested {outer:{inner:[entries]}} structure; removes signature-matching entries, prunes empty containers, returns appropriate InstallResult statuses.
Adapter-specific uninstall implementations
Gradata/src/gradata/hooks/adapters/claude_code.py, cursor.py, gemini.py, opencode.py, hermes.py, codex.py
Added idempotent uninstall(brain_dir, agent_config_path) functions per adapter: JSON-based adapters (claude_code, cursor, gemini, opencode) use dict manipulation or the base helper; YAML adapter (hermes) scans event lists in both current and legacy hook sections; TOML adapter (codex) parses and removes matching table blocks by signature.
Test coverage for uninstall workflows
Gradata/tests/test_uninstall_command.py
Test suite validating round-trip install/uninstall symmetry, idempotency on repeated calls, user-owned entry preservation (entries without Gradata signature), missing config graceful handling, manifest round-trip integrity, and CLI behavior with unknown agents.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Gradata/gradata#189: Modifies the same install --agent CLI path in cli.py by adding optional install verification, creating a code-level dependency on the manifest recording behavior introduced in this PR.

Suggested labels

feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding a symmetric uninstall command for the gradata CLI that mirrors the install command with agent selection.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering architecture, behavior guarantees, test coverage, and risk assessment for the uninstall feature.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gradata-uninstall-subcommand-gra-1241

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.21.0)

OpenGrep fatal error (exit code 2):
┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m
[00.46][ERROR]: Error: exception Glob.Lexer.Syntax_error("malformed glob pattern: missing ']'")
Raised at Glob__Lexer.syntax_error in file "libs/glob/Lexer.mll", line 8, characters 2-26
Called from Glob__Lexer.__ocaml_lex_token_rec in file "libs/glob/Lexer.mll", line 29, characters 26-53
Cal


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Gradata/src/gradata/_install_manifest.py`:
- Around line 85-89: The manifest loader currently trusts that data["agents"] is
a dict and can crash if JSON had a non-dict value; update the validation logic
around SCHEMA_VERSION and data so that after confirming data is a dict you also
enforce data["agents"] is a dict (replace non-dict with {}), e.g., use
isinstance checks on data and data.get("agents") and call
data.setdefault("agents", {}) only when agents is a dict or otherwise assign an
empty dict; apply the same hardening to the later manifest-read/merge area (the
block that references data.get(...) / agents.get(...)) so all code paths treat
agents as a dict.

In `@Gradata/src/gradata/cli.py`:
- Around line 605-610: The current uninstall logic in the block that decides
whether to drop the manifest record incorrectly checks if result.action ==
"added"; change the check to look for the uninstall success action ("removed")
so manifest records are deleted only when the adapter signaled removal. Locate
the conditional that reads result.action == "added" in the uninstall/remove flow
in Gradata/src/gradata/cli.py and replace it to test for "removed" (leaving
handling for "already_present" untouched).

In `@Gradata/tests/test_uninstall_command.py`:
- Around line 23-30: The local fixture brain_dir creates a temp dir but does not
set the required BRAIN_DIR env var, bypassing test isolation; change tests to
use the shared BRAIN_DIR provided by conftest.py (or set
os.environ["BRAIN_DIR"]=str(tmp_path/"brain") in the fixture) and ensure you
refresh the module cache before reinitializing by reloading the _paths module
and then calling Brain.init() (reference symbols: brain_dir fixture, BRAIN_DIR
environment variable, Brain.init(), and the _paths module) so the code picks up
the new path.
- Around line 152-162: The test test_cli_unknown_agent_clean_error is intended
to exercise an invalid-agent path but overwrites args.agent from "foobar" to
"claude-code", so the invalid-agent branch is never tested; restore the original
invalid value or add a separate assertion: set args.agent = "foobar" (or remove
the later reassignment) before invoking cmd_uninstall (or call cmd_uninstall
twice with args.agent="foobar" and args.agent="claude-code") so the code path
handling truly unknown/invalid agents is executed and validated; locate the test
function test_cli_unknown_agent_clean_error and adjust the args.agent
assignments surrounding the call to cmd_uninstall accordingly.
- Line 166: The test calls capsys.readouterr() twice which can drop stderr;
instead call capsys.readouterr() once, store its result (e.g., assign to a
variable like captured = capsys.readouterr()), and then build out by
concatenating captured.out and captured.err so the traceback/assertions include
both stdout and stderr; update the usage around the existing out variable and
the call to capsys.readouterr() in the test_uninstall_command test to use the
single captured object.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0c9961ec-ee55-4d4b-807e-d0322ec0fc45

📥 Commits

Reviewing files that changed from the base of the PR and between ecc287b and 81a3513.

📒 Files selected for processing (10)
  • Gradata/src/gradata/_install_manifest.py
  • Gradata/src/gradata/cli.py
  • Gradata/src/gradata/hooks/adapters/_base.py
  • Gradata/src/gradata/hooks/adapters/claude_code.py
  • Gradata/src/gradata/hooks/adapters/codex.py
  • Gradata/src/gradata/hooks/adapters/cursor.py
  • Gradata/src/gradata/hooks/adapters/gemini.py
  • Gradata/src/gradata/hooks/adapters/hermes.py
  • Gradata/src/gradata/hooks/adapters/opencode.py
  • Gradata/tests/test_uninstall_command.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: pytest (py3.12)
  • GitHub Check: pytest windows-latest / py3.11
  • GitHub Check: pytest macos-latest / py3.12
  • GitHub Check: pytest windows-latest / py3.12
🧰 Additional context used
📓 Path-based instructions (2)
Gradata/src/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/src/**/*.py: Prefer sentence-transformers for local embeddings, google-genai for Gemini embeddings, cryptography for AES-GCM encrypted system.db, bm25s for BM25 rule ranking, and mem0ai for external memory adapters — guard all optional dependency imports with try / except ImportError at the call site, never at module level
Maintain strict layering: Layer 0 (Primitives: _types.py, _db.py, _events.py, _paths.py, _file_lock.py; Patterns: contrib/patterns/) must never import from Layer 1 (Enhancements: enhancements/, rules/) or Layer 2 (Public API: brain.py, cli.py, daemon.py, mcp_server.py)
Never use bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product
Never import from out-of-scope sibling directories ../Sprites/ or ../Hausgem/ within gradata/* code — that is a layering bug
Never leak private-sibling paths into public docs/code — no references to ../Sprites/, ../Hausgem/, email addresses, OneDrive paths, or Sprites-specific examples from inside gradata/*
Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes

Files:

  • Gradata/src/gradata/hooks/adapters/gemini.py
  • Gradata/src/gradata/hooks/adapters/opencode.py
  • Gradata/src/gradata/hooks/adapters/claude_code.py
  • Gradata/src/gradata/hooks/adapters/hermes.py
  • Gradata/src/gradata/hooks/adapters/_base.py
  • Gradata/src/gradata/hooks/adapters/cursor.py
  • Gradata/src/gradata/_install_manifest.py
  • Gradata/src/gradata/hooks/adapters/codex.py
  • Gradata/src/gradata/cli.py
Gradata/tests/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/tests/**/*.py: Set BRAIN_DIR environment variable via tmp_path in conftest.py for test isolation — ensure _paths.py module cache refreshes when calling Brain.init() directly inside tests
Add unit tests in tests/test_*.py for every CI push without LLM calls (deterministic); mark integration tests with @pytest.mark.integration and skip them by default (they hit real LLM APIs)

Files:

  • Gradata/tests/test_uninstall_command.py
🪛 Ruff (0.15.13)
Gradata/tests/test_uninstall_command.py

[warning] 162-165: Use contextlib.suppress(SystemExit) instead of try-except-pass

(SIM105)

🔇 Additional comments (9)
Gradata/tests/test_uninstall_command.py (1)

32-150: LGTM!

Gradata/src/gradata/cli.py (1)

159-159: LGTM!

Also applies to: 496-508, 543-603, 615-617, 875-889, 2047-2064, 2324-2324

Gradata/src/gradata/hooks/adapters/_base.py (1)

52-52: LGTM!

Also applies to: 168-221

Gradata/src/gradata/hooks/adapters/claude_code.py (1)

83-127: LGTM!

Gradata/src/gradata/hooks/adapters/cursor.py (1)

53-72: LGTM!

Gradata/src/gradata/hooks/adapters/gemini.py (1)

74-84: LGTM!

Gradata/src/gradata/hooks/adapters/opencode.py (1)

64-74: LGTM!

Gradata/src/gradata/hooks/adapters/hermes.py (1)

234-280: LGTM!

Gradata/src/gradata/hooks/adapters/codex.py (1)

94-154: LGTM!

Comment on lines +85 to +89
if not isinstance(data, dict):
return {"schema_version": SCHEMA_VERSION, "agents": {}}
data.setdefault("schema_version", SCHEMA_VERSION)
data.setdefault("agents", {})
return data
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden manifest shape validation before reading records.

Line 88 can leave agents as a non-dict (from malformed but valid JSON), and Line 121 then assumes .get() exists on it. That can crash uninstall/install flows instead of safely degrading.

🔧 Proposed fix
 def load(path: Path | None = None) -> dict:
@@
     if not isinstance(data, dict):
         return {"schema_version": SCHEMA_VERSION, "agents": {}}
     data.setdefault("schema_version", SCHEMA_VERSION)
-    data.setdefault("agents", {})
+    agents = data.get("agents")
+    if not isinstance(agents, dict):
+        data["agents"] = {}
     return data
@@
 def get_record(agent: str, path: Path | None = None) -> AgentRecord | None:
     data = load(path)
-    raw = data.get("agents", {}).get(agent)
+    agents = data.get("agents")
+    if not isinstance(agents, dict):
+        return None
+    raw = agents.get(agent)
@@
-    except KeyError:
+    except (KeyError, TypeError):
         return None

Also applies to: 119-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Gradata/src/gradata/_install_manifest.py` around lines 85 - 89, The manifest
loader currently trusts that data["agents"] is a dict and can crash if JSON had
a non-dict value; update the validation logic around SCHEMA_VERSION and data so
that after confirming data is a dict you also enforce data["agents"] is a dict
(replace non-dict with {}), e.g., use isinstance checks on data and
data.get("agents") and call data.setdefault("agents", {}) only when agents is a
dict or otherwise assign an empty dict; apply the same hardening to the later
manifest-read/merge area (the block that references data.get(...) /
agents.get(...)) so all code paths treat agents as a dict.

Comment on lines +605 to +610
# Drop manifest record only when we actually removed an entry
# (action == "added" in our adapter contract — "already_present"
# means nothing was there to remove, so we leave the manifest
# record alone in case the user re-installs).
if result.action == "added":
try:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the uninstall action when dropping manifest records.

Line 609 checks for "added", but uninstall adapters signal successful removal as "removed". This leaves stale manifest records after successful uninstall.

🔧 Proposed fix
-        # Drop manifest record only when we actually removed an entry
-        # (action == "added" in our adapter contract — "already_present"
-        # means nothing was there to remove, so we leave the manifest
-        # record alone in case the user re-installs).
-        if result.action == "added":
+        # Drop manifest record only when uninstall actually removed an entry.
+        if result.action == "removed":
             try:
                 drop_record(name)
             except Exception as exc:
                 print(f"  ⚠ install manifest update failed: {exc}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Gradata/src/gradata/cli.py` around lines 605 - 610, The current uninstall
logic in the block that decides whether to drop the manifest record incorrectly
checks if result.action == "added"; change the check to look for the uninstall
success action ("removed") so manifest records are deleted only when the adapter
signaled removal. Locate the conditional that reads result.action == "added" in
the uninstall/remove flow in Gradata/src/gradata/cli.py and replace it to test
for "removed" (leaving handling for "already_present" untouched).

Comment on lines +23 to +30
@pytest.fixture
def brain_dir(tmp_path: Path) -> Path:
"""Minimal brain dir — uninstall doesn't need data, just a path."""
d = tmp_path / "brain"
d.mkdir(parents=True, exist_ok=True)
(d / "system.db").write_bytes(b"")
return d

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use shared BRAIN_DIR test isolation instead of a local-only fixture.

This fixture creates a temp brain directory but never sets BRAIN_DIR, so it bypasses the required isolation contract for tests under this path.

As per coding guidelines, Gradata/tests/**/*.py: Set BRAIN_DIR environment variable via tmp_path in conftest.py for test isolation — ensure _paths.py module cache refreshes when calling Brain.init() directly inside tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Gradata/tests/test_uninstall_command.py` around lines 23 - 30, The local
fixture brain_dir creates a temp dir but does not set the required BRAIN_DIR env
var, bypassing test isolation; change tests to use the shared BRAIN_DIR provided
by conftest.py (or set os.environ["BRAIN_DIR"]=str(tmp_path/"brain") in the
fixture) and ensure you refresh the module cache before reinitializing by
reloading the _paths module and then calling Brain.init() (reference symbols:
brain_dir fixture, BRAIN_DIR environment variable, Brain.init(), and the _paths
module) so the code picks up the new path.

Comment on lines +152 to +162
def test_cli_unknown_agent_clean_error(tmp_path, capsys, brain_dir):
"""`gradata uninstall --agent foobar` returns a clean argparse error."""
args = SimpleNamespace(agent="foobar", brain=str(brain_dir))
# cmd_uninstall doesn't validate at the function level (argparse does);
# instead, exercise it with a known-bad agent and verify it doesn't crash.
# Argparse's choices= will already have rejected this before cmd_uninstall
# is called, so the function-level test is for unknown manifest records.
# Check that an unknown but valid host name (e.g. one not installed) goes
# through the fallback canonical-path machinery without exception.
args.agent = "claude-code" # valid name, but not installed
try:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The “unknown agent” path is not actually exercised.

At Line 154 the test starts with agent="foobar", but Line 161 overwrites it to "claude-code". This only validates “valid but not installed,” not invalid-agent handling described by the test name/docstring.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Gradata/tests/test_uninstall_command.py` around lines 152 - 162, The test
test_cli_unknown_agent_clean_error is intended to exercise an invalid-agent path
but overwrites args.agent from "foobar" to "claude-code", so the invalid-agent
branch is never tested; restore the original invalid value or add a separate
assertion: set args.agent = "foobar" (or remove the later reassignment) before
invoking cmd_uninstall (or call cmd_uninstall twice with args.agent="foobar" and
args.agent="claude-code") so the code path handling truly unknown/invalid agents
is executed and validated; locate the test function
test_cli_unknown_agent_clean_error and adjust the args.agent assignments
surrounding the call to cmd_uninstall accordingly.

cmd_uninstall(args)
except SystemExit:
pass # cmd_uninstall may exit with code on failure — that's OK
out = capsys.readouterr().out + capsys.readouterr().err
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Capture capsys output once to avoid dropping stderr.

Calling capsys.readouterr() twice can miss error output from the first read, weakening the traceback assertion.

Proposed fix
-    out = capsys.readouterr().out + capsys.readouterr().err
+    captured = capsys.readouterr()
+    out = captured.out + captured.err
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
out = capsys.readouterr().out + capsys.readouterr().err
captured = capsys.readouterr()
out = captured.out + captured.err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Gradata/tests/test_uninstall_command.py` at line 166, The test calls
capsys.readouterr() twice which can drop stderr; instead call
capsys.readouterr() once, store its result (e.g., assign to a variable like
captured = capsys.readouterr()), and then build out by concatenating
captured.out and captured.err so the traceback/assertions include both stdout
and stderr; update the usage around the existing out variable and the call to
capsys.readouterr() in the test_uninstall_command test to use the single
captured object.

@Gradata Gradata merged commit 01d53d9 into main May 20, 2026
8 of 9 checks passed
@Gradata Gradata deleted the feat/gradata-uninstall-subcommand-gra-1241 branch May 20, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant