mirror of
https://github.com/himanshudongre/smriti.git
synced 2026-08-28 05:14:59 +00:00
328 lines
11 KiB
Python
328 lines
11 KiB
Python
"""Tests for the smriti init command.
|
|
|
|
Covers:
|
|
- Parser wiring
|
|
- Space creation (new) and connection (existing)
|
|
- Skill pack installation for both targets
|
|
- AGENTS.md safety: skips when non-Smriti content exists
|
|
- SessionStart hook generation and idempotence
|
|
- Idempotent re-run (everything already configured)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from smriti_cli import main as cli_main
|
|
from smriti_cli.client import SmritiClient
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_client(monkeypatch):
|
|
client = MagicMock(spec=SmritiClient)
|
|
client.base_url = "http://localhost:8000"
|
|
client.list_spaces.return_value = []
|
|
monkeypatch.setattr(cli_main, "SmritiClient", lambda **kw: client)
|
|
monkeypatch.setattr(
|
|
cli_main,
|
|
"_smriti_hook_executable",
|
|
lambda: "/opt/smriti/bin/smriti",
|
|
)
|
|
monkeypatch.setattr(
|
|
cli_main,
|
|
"_smriti_mcp_executable",
|
|
lambda: "/opt/smriti/bin/smriti-mcp",
|
|
)
|
|
return client
|
|
|
|
|
|
def test_init_parser_wiring():
|
|
parser = cli_main._build_parser()
|
|
args = parser.parse_args(["init", "my-project", "--description", "Test"])
|
|
assert args.command == "init"
|
|
assert args.space == "my-project"
|
|
assert args.description == "Test"
|
|
assert args.func is cli_main.cmd_init
|
|
|
|
|
|
def test_session_start_hook_command_shell_quotes(monkeypatch):
|
|
monkeypatch.setattr(
|
|
cli_main,
|
|
"_smriti_hook_executable",
|
|
lambda: "/Applications/Smriti Tools/bin/smriti",
|
|
)
|
|
|
|
command = cli_main._build_session_start_hook_command(
|
|
"my project",
|
|
"http://localhost:8000",
|
|
)
|
|
|
|
assert command.startswith(
|
|
"'/Applications/Smriti Tools/bin/smriti' --api-url "
|
|
"http://localhost:8000 state 'my project' --compact"
|
|
)
|
|
assert "backend/.venv/bin/smriti" not in command
|
|
|
|
|
|
def test_smriti_session_start_detector_handles_quoted_executable():
|
|
entry = {
|
|
"matcher": "startup",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": (
|
|
"'/Applications/Smriti Tools/bin/smriti' --api-url "
|
|
"http://localhost:8000 state p --compact 2>/dev/null"
|
|
),
|
|
}
|
|
],
|
|
}
|
|
|
|
assert cli_main._is_smriti_session_start_entry(entry) is True
|
|
|
|
|
|
def test_init_creates_space_and_skill_packs(mock_client, tmp_path, monkeypatch):
|
|
"""Init with a fresh project: creates space, installs both skill packs."""
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
# Space doesn't exist — resolve fails, create succeeds
|
|
from smriti_cli.client import SmritiError
|
|
mock_client.resolve_space.side_effect = SmritiError("not found")
|
|
mock_client.create_space.return_value = {
|
|
"id": "new-space-uuid",
|
|
"name": "test-project",
|
|
}
|
|
|
|
args = cli_main._build_parser().parse_args(["init", "test-project"])
|
|
args.api_url = None
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
# Space was created
|
|
mock_client.create_space.assert_called_once_with(name="test-project", description="")
|
|
|
|
# Claude skill pack was installed
|
|
assert (tmp_path / ".claude" / "skills" / "smriti" / "SKILL.md").exists()
|
|
|
|
# Codex skill pack was installed (no pre-existing AGENTS.md)
|
|
assert (tmp_path / "AGENTS.md").exists()
|
|
assert "smriti_skill_pack_version" in (tmp_path / "AGENTS.md").read_text()
|
|
|
|
# SessionStart hook was generated
|
|
settings = json.loads((tmp_path / ".claude" / "settings.json").read_text())
|
|
assert "SessionStart" in settings.get("hooks", {})
|
|
command = settings["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
|
assert command.startswith(
|
|
"/opt/smriti/bin/smriti --api-url "
|
|
"http://localhost:8000 state test-project --compact"
|
|
)
|
|
assert "backend/.venv/bin/smriti" not in command
|
|
assert "--preview" not in command
|
|
|
|
|
|
def test_init_connects_existing_space(mock_client, tmp_path, monkeypatch):
|
|
"""When space already exists, init connects instead of creating."""
|
|
monkeypatch.chdir(tmp_path)
|
|
mock_client.resolve_space.return_value = {
|
|
"id": "existing-uuid",
|
|
"name": "my-project",
|
|
}
|
|
|
|
args = cli_main._build_parser().parse_args(["init", "my-project"])
|
|
args.api_url = None
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
mock_client.create_space.assert_not_called()
|
|
|
|
|
|
def test_init_skips_agents_md_with_existing_non_smriti_content(
|
|
mock_client, tmp_path, monkeypatch
|
|
):
|
|
"""AGENTS.md with non-Smriti content should NOT be overwritten."""
|
|
monkeypatch.chdir(tmp_path)
|
|
mock_client.resolve_space.return_value = {"id": "uuid", "name": "p"}
|
|
|
|
# Pre-existing AGENTS.md with user content (no smriti frontmatter)
|
|
agents = tmp_path / "AGENTS.md"
|
|
agents.write_text("# My Custom Agent Instructions\n\nDo not touch this.\n")
|
|
|
|
args = cli_main._build_parser().parse_args(["init", "p"])
|
|
args.api_url = None
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
# AGENTS.md should be untouched
|
|
assert "My Custom Agent Instructions" in agents.read_text()
|
|
assert "smriti_skill_pack_version" not in agents.read_text()
|
|
|
|
|
|
def test_init_upgrades_existing_smriti_agents_md(
|
|
mock_client, tmp_path, monkeypatch
|
|
):
|
|
"""AGENTS.md that IS a Smriti skill pack should be upgraded normally."""
|
|
monkeypatch.chdir(tmp_path)
|
|
mock_client.resolve_space.return_value = {"id": "uuid", "name": "p"}
|
|
|
|
agents = tmp_path / "AGENTS.md"
|
|
agents.write_text("---\nsmriti_skill_pack_version: 0.1\n---\nOld content\n")
|
|
|
|
args = cli_main._build_parser().parse_args(["init", "p"])
|
|
args.api_url = None
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
# Should be upgraded (0.1 < current version)
|
|
content = agents.read_text()
|
|
assert "smriti_skill_pack_version" in content
|
|
assert "0.1" not in content # upgraded past 0.1
|
|
|
|
|
|
def test_init_idempotent_second_run(mock_client, tmp_path, monkeypatch):
|
|
"""Running init twice should not error or duplicate anything."""
|
|
monkeypatch.chdir(tmp_path)
|
|
mock_client.resolve_space.return_value = {"id": "uuid", "name": "p"}
|
|
|
|
args = cli_main._build_parser().parse_args(["init", "p"])
|
|
args.api_url = None
|
|
|
|
# First run
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
# Second run — should succeed silently
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
# Hook still has exactly 3 SessionStart entries (not 6)
|
|
settings = json.loads((tmp_path / ".claude" / "settings.json").read_text())
|
|
assert len(settings["hooks"]["SessionStart"]) == 3
|
|
|
|
|
|
def test_init_updates_stale_smriti_session_start_hook(
|
|
mock_client, tmp_path, monkeypatch
|
|
):
|
|
"""Repo-relative / preview-mode hooks should be upgraded in place."""
|
|
monkeypatch.chdir(tmp_path)
|
|
mock_client.resolve_space.return_value = {"id": "uuid", "name": "p"}
|
|
|
|
settings_dir = tmp_path / ".claude"
|
|
settings_dir.mkdir()
|
|
settings_file = settings_dir / "settings.json"
|
|
settings_file.write_text(
|
|
json.dumps({
|
|
"hooks": {
|
|
"SessionStart": [
|
|
{
|
|
"matcher": "startup",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": (
|
|
"backend/.venv/bin/smriti state p --preview "
|
|
"2>/dev/null || echo old"
|
|
),
|
|
}
|
|
],
|
|
}
|
|
]
|
|
}
|
|
})
|
|
)
|
|
|
|
args = cli_main._build_parser().parse_args(["init", "p"])
|
|
args.api_url = None
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
settings = json.loads(settings_file.read_text())
|
|
commands = [
|
|
entry["hooks"][0]["command"]
|
|
for entry in settings["hooks"]["SessionStart"]
|
|
]
|
|
assert len(commands) == 3
|
|
assert all(
|
|
command.startswith(
|
|
"/opt/smriti/bin/smriti --api-url http://localhost:8000 state p --compact"
|
|
)
|
|
for command in commands
|
|
)
|
|
assert all("backend/.venv/bin/smriti" not in command for command in commands)
|
|
assert all("--preview" not in command for command in commands)
|
|
|
|
|
|
def test_init_preserves_unrelated_session_start_hooks(
|
|
mock_client, tmp_path, monkeypatch
|
|
):
|
|
"""User-managed SessionStart hooks should survive init."""
|
|
monkeypatch.chdir(tmp_path)
|
|
mock_client.resolve_space.return_value = {"id": "uuid", "name": "p"}
|
|
|
|
settings_dir = tmp_path / ".claude"
|
|
settings_dir.mkdir()
|
|
settings_file = settings_dir / "settings.json"
|
|
user_hook = {
|
|
"matcher": "startup",
|
|
"hooks": [{"type": "command", "command": "echo user hook"}],
|
|
}
|
|
settings_file.write_text(json.dumps({"hooks": {"SessionStart": [user_hook]}}))
|
|
|
|
args = cli_main._build_parser().parse_args(["init", "p"])
|
|
args.api_url = None
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
settings = json.loads(settings_file.read_text())
|
|
entries = settings["hooks"]["SessionStart"]
|
|
assert entries[0] == user_hook
|
|
assert len(entries) == 4
|
|
assert any(
|
|
entry["hooks"][0]["command"].startswith(
|
|
"/opt/smriti/bin/smriti --api-url http://localhost:8000 state p --compact"
|
|
)
|
|
for entry in entries[1:]
|
|
)
|
|
|
|
|
|
def test_init_json_preserves_api_url_for_hooks_and_mcp(
|
|
mock_client, tmp_path, monkeypatch, capsys
|
|
):
|
|
"""Custom backend URLs should survive generated hooks and MCP hints."""
|
|
monkeypatch.chdir(tmp_path)
|
|
mock_client.base_url = "http://127.0.0.1:8999"
|
|
mock_client.resolve_space.return_value = {"id": "uuid", "name": "p"}
|
|
|
|
args = cli_main._build_parser().parse_args([
|
|
"--api-url",
|
|
"http://127.0.0.1:8999",
|
|
"init",
|
|
"p",
|
|
"--json",
|
|
])
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
payload = json.loads(capsys.readouterr().out)
|
|
settings = json.loads((tmp_path / ".claude" / "settings.json").read_text())
|
|
command = settings["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
|
|
|
assert "--api-url http://127.0.0.1:8999" in command
|
|
assert any("/opt/smriti/bin/smriti-mcp" in step for step in payload["next_steps"])
|
|
assert any("http://127.0.0.1:8999" in step for step in payload["next_steps"])
|
|
|
|
|
|
def test_init_merges_into_existing_settings_json(
|
|
mock_client, tmp_path, monkeypatch
|
|
):
|
|
"""If .claude/settings.json exists with other keys, init merges without overwriting."""
|
|
monkeypatch.chdir(tmp_path)
|
|
mock_client.resolve_space.return_value = {"id": "uuid", "name": "p"}
|
|
|
|
settings_dir = tmp_path / ".claude"
|
|
settings_dir.mkdir()
|
|
settings_file = settings_dir / "settings.json"
|
|
settings_file.write_text(json.dumps({"permissions": {"allow": ["Bash"]}}))
|
|
|
|
args = cli_main._build_parser().parse_args(["init", "p"])
|
|
args.api_url = None
|
|
cli_main.cmd_init(mock_client, args)
|
|
|
|
settings = json.loads(settings_file.read_text())
|
|
# Original key preserved
|
|
assert settings["permissions"]["allow"] == ["Bash"]
|
|
# Hook added
|
|
assert "SessionStart" in settings["hooks"]
|