agnes-the-ai-analyst/tests/test_lib_commands.py
minasarustamyan d9405a6888
Move marketplace plugin updates from hook to /update-agnes-plugins skill (#237)
* Move marketplace plugin updates from hook to /update-agnes-plugins skill

The SessionStart hook used to run `agnes refresh-marketplace --quiet`,
which performed a full fetch+reset+install cycle on every Claude Code
session start. That work was invisible to the user, slowed session
startup, and was unrecoverable interactively when something failed.

Split the responsibility:

- `agnes refresh-marketplace --check` is a new lightweight detector:
  `git fetch` only, compares local HEAD with remote FETCH_HEAD, emits
  a Claude Code hook JSON message pointing the user at
  `/update-agnes-plugins` when the marketplace has changes. No reset,
  no plugin install/update side effects.
- `/update-agnes-plugins` is a new slash command (installed by
  `agnes init` into `<workspace>/.claude/commands/`) that runs
  `agnes refresh-marketplace` (default chatty path). Output streams
  into the Claude Code transcript so the user sees install/update
  progress and can react to errors interactively.
- The SessionStart hook now runs `--check`. Existing workspaces
  auto-upgrade on next `agnes init` (substring marker matches both
  the old `--quiet` entry and the new `--check` one).

BREAKING: `agnes refresh-marketplace --quiet` is removed. Old hooks
calling it silent-noop after the CLI upgrade (the hook's `|| true`
swallows the unknown-flag error) until re-init rewrites them.

* Point marketplace 'Added to your stack' hint at /update-agnes-plugins

The post-install green panel on plugin and skill/agent detail pages
referenced the SessionStart auto-install path and a shell-prompt
`agnes refresh-marketplace` invocation. With the hook now being
detect-only, that copy was misleading — the actual install path is
the new slash command.

Condensed to a single instruction: "Open a new Claude Code session
and run:" followed by `/update-agnes-plugins` in a copy-chip.
JS clipboard string updated to match.

---------

Co-authored-by: Minas Arustamyan <arustamyan.minas@gmail.com>
2026-05-09 21:10:39 +02:00

83 lines
3.4 KiB
Python

"""Tests for cli/lib/commands.py:install_claude_commands."""
from __future__ import annotations
from pathlib import Path
from cli.lib.commands import install_claude_commands
def _read_managed_command(workspace: Path) -> str:
return (workspace / ".claude" / "commands" / "update-agnes-plugins.md").read_text(
encoding="utf-8"
)
def test_install_writes_slash_command_file(tmp_path):
"""install_claude_commands writes update-agnes-plugins.md into
<workspace>/.claude/commands/. The file is the bundled template
(frontmatter + body) — the test just pins that *something* lands and
that frontmatter shape is intact, so a future template edit doesn't
silently lose its description metadata."""
install_claude_commands(tmp_path)
body = _read_managed_command(tmp_path)
assert body.startswith("---"), body[:120]
assert "description:" in body.split("---", 2)[1], body[:200]
# The slash command's whole point: invoke `agnes refresh-marketplace`.
# Pin that the bundled body actually references the command.
assert "agnes refresh-marketplace" in body
def test_install_creates_commands_dir_when_missing(tmp_path):
"""Workspace without a .claude/ tree at all → install creates both
.claude/ and .claude/commands/."""
assert not (tmp_path / ".claude").exists()
install_claude_commands(tmp_path)
assert (tmp_path / ".claude" / "commands").is_dir()
def test_install_overwrites_existing_managed_file(tmp_path):
"""Even if the workspace has a hand-edited copy of the managed slash
command, install overwrites with the canonical template — server-
managed by design (Q2 of the design discussion). Users who want to
customize copy to a different filename, which is preserved by the
third-party-untouched test below."""
cmd_dir = tmp_path / ".claude" / "commands"
cmd_dir.mkdir(parents=True)
(cmd_dir / "update-agnes-plugins.md").write_text(
"USER EDIT THAT MUST BE OVERWRITTEN", encoding="utf-8",
)
install_claude_commands(tmp_path)
body = _read_managed_command(tmp_path)
assert "USER EDIT" not in body
assert "agnes refresh-marketplace" in body
def test_install_does_not_touch_other_command_files(tmp_path):
"""User's own slash commands under .claude/commands/ (e.g.
my-custom.md, project-specific helpers) must survive install
untouched. Only the Agnes-managed command files are overwritten."""
cmd_dir = tmp_path / ".claude" / "commands"
cmd_dir.mkdir(parents=True)
custom_path = cmd_dir / "my-custom.md"
custom_body = "---\ndescription: my own slash command\n---\n\nhello"
custom_path.write_text(custom_body, encoding="utf-8")
install_claude_commands(tmp_path)
assert custom_path.read_text(encoding="utf-8") == custom_body
def test_install_idempotent(tmp_path):
"""Two consecutive installs produce identical state. Important
because `agnes init --force` re-runs the installer and the
SessionStart hook chain (in some future world where we wire it up)
might too — neither should accumulate stray files or change content
on a no-op invocation."""
install_claude_commands(tmp_path)
first = _read_managed_command(tmp_path)
install_claude_commands(tmp_path)
second = _read_managed_command(tmp_path)
assert first == second
# And no extra files appeared.
cmd_dir_files = sorted(p.name for p in (tmp_path / ".claude" / "commands").iterdir())
assert cmd_dir_files == ["update-agnes-plugins.md"]