First version. I can init

This commit is contained in:
2026-04-13 22:04:05 +02:00
commit 40ea9d5c1f
18 changed files with 713 additions and 0 deletions
View File
+24
View File
@@ -0,0 +1,24 @@
"""Shared fixtures for myclaude tests."""
from pathlib import Path
import pytest
@pytest.fixture
def tmp_repo(tmp_path: Path) -> Path:
"""Create a minimal repo directory structure with two skills and a CLAUDE.md."""
repo = tmp_path / "repo"
for skill in ("skill_a", "skill_b"):
(repo / "skills" / skill).mkdir(parents=True)
(repo / "skills" / skill / "SKILL.md").write_text(f"# {skill}")
(repo / "CLAUDE.md").write_text("# Claude Template")
return repo
@pytest.fixture
def tmp_project(tmp_path: Path) -> Path:
"""Create an empty project directory."""
project = tmp_path / "project"
project.mkdir()
return project
+50
View File
@@ -0,0 +1,50 @@
"""Tests for myclaude.config module."""
import json
from pathlib import Path
from unittest.mock import patch
import pytest
from myclaude.config import load_config, save_config
@pytest.mark.parametrize("repo_url", [
"git@gitea.example.com:user/skills.git",
"git@gitea.internal:org/claude.git",
])
def test_i_can_save_config(tmp_path: Path, repo_url: str) -> None:
config_file = tmp_path / "config.json"
with patch("myclaude.config.CONFIG_DIR", tmp_path), \
patch("myclaude.config.CONFIG_FILE", config_file):
save_config(repo_url)
content = json.loads(config_file.read_text())
assert content["repo_url"] == repo_url
@pytest.mark.parametrize("repo_url", [
"git@gitea.example.com:user/skills.git",
"git@gitea.internal:org/claude.git",
])
def test_i_can_load_config(tmp_path: Path, repo_url: str) -> None:
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps({"repo_url": repo_url}))
with patch("myclaude.config.CONFIG_FILE", config_file):
result = load_config()
assert result["repo_url"] == repo_url
def test_i_can_update_repo_url(tmp_path: Path) -> None:
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps({"repo_url": "git@old.example.com:user/old.git"}))
with patch("myclaude.config.CONFIG_DIR", tmp_path), \
patch("myclaude.config.CONFIG_FILE", config_file):
save_config("git@new.example.com:user/new.git")
content = json.loads(config_file.read_text())
assert content["repo_url"] == "git@new.example.com:user/new.git"
def test_i_cannot_load_config_when_file_missing(tmp_path: Path) -> None:
with patch("myclaude.config.CONFIG_FILE", tmp_path / "config.json"):
with pytest.raises(FileNotFoundError):
load_config()
+76
View File
@@ -0,0 +1,76 @@
"""Tests for myclaude.fs_ops module."""
from pathlib import Path
import pytest
from myclaude.fs_ops import (
collect_local_skills,
copy_claude_md_to_project,
copy_skills_to_project,
copy_skills_to_repo,
)
def test_i_can_copy_all_skills(tmp_project: Path, tmp_repo: Path) -> None:
copy_skills_to_project(tmp_repo, tmp_project)
assert (tmp_project / ".claude" / "skills" / "skill_a" / "SKILL.md").exists()
assert (tmp_project / ".claude" / "skills" / "skill_b" / "SKILL.md").exists()
@pytest.mark.parametrize("requested,expected", [
(["skill_a"], ["skill_a"]),
(["skill_a", "skill_b"], ["skill_a", "skill_b"]),
])
def test_i_can_copy_filtered_skills(
tmp_project: Path,
tmp_repo: Path,
requested: list[str],
expected: list[str],
) -> None:
copy_skills_to_project(tmp_repo, tmp_project, skills=requested)
installed = collect_local_skills(tmp_project)
assert installed == sorted(expected)
def test_i_can_copy_claude_md(tmp_project: Path, tmp_repo: Path) -> None:
copy_claude_md_to_project(tmp_repo, tmp_project)
assert (tmp_project / "CLAUDE.md").read_text() == "# Claude Template"
def test_i_cannot_init_when_skills_dir_exists(tmp_project: Path, tmp_repo: Path) -> None:
(tmp_project / ".claude" / "skills").mkdir(parents=True)
with pytest.raises(FileExistsError):
copy_skills_to_project(tmp_repo, tmp_project)
def test_i_can_force_overwrite_skills_dir(tmp_project: Path, tmp_repo: Path) -> None:
skills_dir = tmp_project / ".claude" / "skills"
(skills_dir / "old_skill").mkdir(parents=True)
copy_skills_to_project(tmp_repo, tmp_project, force=True)
assert not (skills_dir / "old_skill").exists()
assert (skills_dir / "skill_a").exists()
@pytest.mark.parametrize("unknown_skill", ["ghost", "unknown", "skill99"])
def test_i_cannot_copy_unknown_skill(
tmp_project: Path,
tmp_repo: Path,
unknown_skill: str,
) -> None:
with pytest.raises(ValueError, match=unknown_skill):
copy_skills_to_project(tmp_repo, tmp_project, skills=[unknown_skill])
def test_i_can_collect_local_skills(tmp_project: Path) -> None:
for skill in ("skill_a", "skill_b"):
(tmp_project / ".claude" / "skills" / skill).mkdir(parents=True)
assert collect_local_skills(tmp_project) == ["skill_a", "skill_b"]
def test_i_can_copy_skills_to_repo(tmp_project: Path, tmp_repo: Path) -> None:
skill_dir = tmp_project / ".claude" / "skills" / "skill_a"
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text("# Updated A")
copy_skills_to_repo(tmp_project, tmp_repo)
assert (tmp_repo / "skills" / "skill_a" / "SKILL.md").read_text() == "# Updated A"
+45
View File
@@ -0,0 +1,45 @@
"""Tests for myclaude.git_ops module."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from git import GitCommandError
from myclaude.git_ops import commit_and_push, temp_clone
def test_i_can_clone_repo_to_temp_dir() -> None:
mock_repo = MagicMock()
with patch("myclaude.git_ops.Repo.clone_from", return_value=mock_repo) as mock_clone:
with temp_clone("git@gitea.example.com:user/skills.git") as (path, repo):
mock_clone.assert_called_once()
assert isinstance(path, Path)
assert repo is mock_repo
@pytest.mark.parametrize("bad_url", ["not-a-url", "", "http://no-ssh.com/repo"])
def test_i_cannot_clone_invalid_repo(bad_url: str) -> None:
with patch(
"myclaude.git_ops.Repo.clone_from",
side_effect=GitCommandError("clone", 128),
):
with pytest.raises(RuntimeError, match="Failed to clone"):
with temp_clone(bad_url):
pass
def test_i_can_commit_and_push() -> None:
mock_repo = MagicMock()
mock_repo.is_dirty.return_value = True
commit_and_push(mock_repo, "chore: update skills skill_a")
mock_repo.git.add.assert_called_once_with(A=True)
mock_repo.index.commit.assert_called_once_with("chore: update skills skill_a")
mock_repo.remotes.origin.push.assert_called_once()
def test_i_cannot_push_when_nothing_to_commit() -> None:
mock_repo = MagicMock()
mock_repo.is_dirty.return_value = False
with pytest.raises(RuntimeError, match="Nothing to commit"):
commit_and_push(mock_repo, "chore: update skills skill_a")