from pathlib import Path import pytest from rdb_discovery.tasks import ( _extract_test_commands_from_claude, generate_agent_prompt, get_next_task, ) def _write_claude_with_test_cmds(tmp_path: Path) -> None: """Write a CLAUDE.md with Test Commands section.""" (tmp_path / "CLAUDE.md").write_text( "# Claude Code Instructions\n\n" "## Test Commands\n\n" "Use the existing virtual environment.\n\n" "```bash\n" "source .venv/bin/activate\n" "python -m pytest\n" "```\n", encoding="utf-8", ) def test_generate_agent_prompt_includes_task_info(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: First goal.\n\n" "Acceptance Criteria:\n" "- a\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: Second goal.\n\n" "Acceptance Criteria:\n" "- b\n" "- c\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "TASK-002" in prompt assert "Second" in prompt assert "Todo" in prompt assert "Second goal." in prompt def test_generate_agent_prompt_includes_acceptance_criteria(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "Acceptance Criteria:\n" "- alpha\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n\n" "Acceptance Criteria:\n" "- bravo\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "- alpha" not in prompt assert "- bravo" in prompt def test_generate_agent_prompt_includes_read_instructions(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "README.md" in prompt assert "TASKS.md" in prompt assert "PROJECT_STATE.md" in prompt assert "AGENT_HANDOFF.md" in prompt assert "context/agent-guidelines.md" in prompt def test_generate_agent_prompt_includes_one_step_constraint(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "ONE small implementation step only" in prompt def test_generate_agent_prompt_includes_validation(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "Validation" in prompt assert "pytest" in prompt def test_generate_agent_prompt_includes_reporting(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "Reporting" in prompt def test_generate_agent_prompt_no_todo_task(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert prompt == "No Todo task found." def test_generate_agent_prompt_no_tasks_file(tmp_path: Path) -> None: prompt = generate_agent_prompt(tmp_path) assert prompt == "No Todo task found." def test_generate_agent_prompt_includes_implementation_gap(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n" "Implementation Gap:\n" "Missing validation handler in cli.py\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "Implementation Gap:" in prompt assert "Missing validation handler in cli.py" in prompt def test_generate_agent_prompt_omits_gap_when_missing(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "Implementation Gap:" not in prompt def test_generate_agent_prompt_includes_test_commands_from_claude(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) _write_claude_with_test_cmds(tmp_path) prompt = generate_agent_prompt(tmp_path) assert "Test Commands (from CLAUDE.md)" in prompt assert "source .venv/bin/activate" in prompt assert "python -m pytest" in prompt def test_generate_agent_prompt_includes_no_reread_constraint(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "reread" in prompt.lower() or "read" in prompt.lower() def test_generate_agent_prompt_includes_inspect_first_constraint(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "Inspect" in prompt or "inspect" in prompt def test_extract_test_commands_no_claude_file(tmp_path: Path) -> None: commands = _extract_test_commands_from_claude(tmp_path) assert commands == [] def test_extract_test_commands_from_claude(tmp_path: Path) -> None: (tmp_path / "CLAUDE.md").write_text( "# Instructions\n\n" "## Test Commands\n\n" "Run the following:\n\n" "```bash\n" "source .venv/bin/activate\n" "python -m pytest\n" "```\n", encoding="utf-8", ) commands = _extract_test_commands_from_claude(tmp_path) assert any(".venv/bin/activate" in cmd for cmd in commands) assert "python -m pytest" in commands def test_extract_test_commands_no_section(tmp_path: Path) -> None: (tmp_path / "CLAUDE.md").write_text( "# Instructions\n\n" "No test section here.\n", encoding="utf-8", ) commands = _extract_test_commands_from_claude(tmp_path) assert commands == [] def test_generate_agent_prompt_with_role(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Role: Architecture Agent\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "You are an architecture agent." in prompt def test_generate_agent_prompt_default_role_when_missing(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "You are an implementation agent." in prompt def test_generate_agent_prompt_with_empty_role_defaults(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Role:\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "You are an implementation agent." in prompt def test_generate_agent_prompt_with_custom_role(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Role: DevOps Engineer\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "You are a devops engineer." in prompt def test_architecture_agent_receives_architecture_context(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Role: Architecture Agent\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "context/architecture.md" in prompt assert "context/product-brief.md" in prompt assert "context/decisions.md" in prompt def test_implementation_agent_receives_implementation_context(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Role: Implementation Agent\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "AGENT_HANDOFF.md" in prompt assert "context/agent-guidelines.md" in prompt assert "CLAUDE.md" in prompt def test_documentation_agent_receives_documentation_context(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Role: Documentation Agent\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) assert "TEST_PLAN.md" in prompt assert "AGENT_HANDOFF.md" in prompt def test_context_files_differ_by_role(tmp_path: Path) -> None: (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Architect\nStatus: Todo\n\n" "Role: Architecture Agent\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) # Architecture role should have architecture-specific files assert "context/architecture.md" in prompt def test_no_role_uses_default_context(tmp_path: Path) -> None: """When no Role is set, the default file list is used (backward compatible).""" (tmp_path / "TASKS.md").write_text( "# TASKS\n\n" "## TASK-001 — First\nStatus: Done\n\n" "Goal: x.\n\n" "## TASK-002 — Second\nStatus: Todo\n\n" "Goal: y.\n", encoding="utf-8", ) prompt = generate_agent_prompt(tmp_path) # Default files must still be present assert "README.md" in prompt assert "TASKS.md" in prompt assert "PROJECT_STATE.md" in prompt assert "AGENT_HANDOFF.md" in prompt assert "context/agent-guidelines.md" in prompt