Files

416 lines
12 KiB
Python

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