"""Tests for context file generation from discovery answers.""" import pytest from pathlib import Path from textwrap import dedent # Use a helper to find the project root def _get_project_root() -> Path: current = Path(__file__).resolve().parent.parent while current != current.parent: if (current / "pyproject.toml").exists() or (current / ".git").exists(): return current current = current.parent return Path.cwd() @pytest.fixture() def tmp_project(tmp_path): """Create a minimal project root with discovery-log.md and context files.""" root = tmp_path / "testproject" root.mkdir() # Create directory structure (root / "context").mkdir() # Write a discovery log with various confidence levels (root / "context" / "discovery-log.md").write_text( "# Discovery Log\n\n" "| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n" "|---|---|---|---|---|---|---|---|\n" '| Q-001 | What problem are we solving? | \\|A CLI tool for discovery workflow\\| | High | No | | | 2026-06-03 |\n' '| Q-002 | Who is the user? | Internal developers and small dev teams | Medium | No | | | 2026-06-03 |\n' '| Q-003 | What does success look like? | 80% reduction in onboarding time | High | No | | | 2026-06-03 |\n' '| Q-007 | What are the risks? | Scope creep if requirements change frequently | Medium | Yes | | | 2026-06-03 |\n' '| Q-008 | What must not happen? | No third-party SaaS dependencies | Low | No | | | 2026-06-03 |\n', encoding="utf-8", ) # Write product-brief.md with TBD placeholders (root / "context" / "product-brief.md").write_text( "# Product Brief\n\n## Problem\n\nTBD\n\n## Users\n\nTBD\n\n## Success Criteria\n\nTBD\n\n## Minimum Useful Version\n\nTBD\n", encoding="utf-8", ) # Write architecture.md with TBD placeholders (root / "context" / "architecture.md").write_text( "# Architecture\n\n## Overview\n\nTBD\n\n## Core Components\n\nTBD\n\n## Data Flow\n\nTDB\n\n## External Integrations\n\nTBD\n\n## Deployment Architecture\n\nTBD\n", encoding="utf-8", ) # Write risks.md with existing table header only (root / "context" / "risks.md").write_text( "# Risks\n\n| ID | Risk | Impact | Mitigation | Status |\n|---|---|---|---|---|\n", encoding="utf-8", ) # Write assumptions.md with existing table header only (root / "context" / "assumptions.md").write_text( "# Assumptions\n\n| ID | Assumption | Confidence | Validation Needed |\n|---|---|---|---|\n", encoding="utf-8", ) # Write infrastructure-context.md with existing content (root / "context" / "infrastructure-context.md").write_text( "# Infrastructure Context\n\n## Hosting\n\nTBD — Cloud provider details.\n\n## Environments\n\nTBD — Dev/staging/prod setup.\n", encoding="utf-8", ) # Write development-context.md with existing content (non-TBD) (root / "context" / "development-context.md").write_text( "# Development Context\n\n## Tech Stack\n\nPython 3.12, Typer, Rich.\n\n## IDEs and Editors\n\nVS Code and Cursor.\n\n## Dependencies\n\nPostgreSQL, Docker.\n\n## Build & Test\n\npytest for unit tests.\n", encoding="utf-8", ) return root class TestGenerateContextFiles: """Tests for the generate_context_files function.""" def test_mapping_file_exists(self): """The mapping document must exist.""" root = _get_project_root() mapping_path = root / "context/discovery-context-mapping.md" assert mapping_path.exists() def test_mapping_covers_all_10_questions(self): """All 10 core questions should have mappings.""" from rdb_discovery.generate_context import CONTEXT_MAP for i in range(1, 11): qid = f"Q-{i:03d}" assert qid in CONTEXT_MAP, f"Missing mapping for {qid}" def test_mapping_all_target_files_are_valid(self): """All target files in the map should exist as templates.""" from rdb_discovery.generate_context import CONTEXT_MAP from rdb_discovery.templates import CONTEXT_FILES template_paths = set(CONTEXT_FILES.keys()) | {"TASKS.md", "TEST_PLAN.md"} for qid, targets in CONTEXT_MAP.items(): for file_path, _, _ in targets: # The file should either be a known template or be context/ assert any(file_path.startswith(p.rstrip('/').split('/')[-1] if '/' not in p else '') for p in template_paths) or 'context/' in file_path, \ f"{qid} maps to unknown file: {file_path}" def test_no_discovery_answers_returns_skipped(self, tmp_project): """When no discovery answers exist, return skipped message.""" # Overwrite discovery log with empty content (tmp_project / "context" / "discovery-log.md").write_text( "# Discovery Log\n\n| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n|---|---|---|---|---|---|---|---|\n", encoding="utf-8", ) from rdb_discovery.generate_context import generate_context_files result = generate_context_files(tmp_project) assert result["generated"] == [] assert len(result["skipped"]) > 0 assert "No discovery answers" in result["skipped"][0] class TestLowConfidenceFiltering: """Tests for low-confidence answer filtering.""" def test_low_confidence_answers_are_skipped(self, tmp_project): """Answers with Low confidence should not generate any context content.""" from rdb_discovery.generate_context import generate_context_files # Overwrite to have only Low confidence answers (tmp_project / "context" / "discovery-log.md").write_text( "# Discovery Log\n\n" "| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n" "|---|---|---|---|---|---|---|---|\n" '| Q-001 | What problem are we solving? | Some answer | Low | No | | | 2026-06-03 |\n', encoding="utf-8", ) result = generate_context_files(tmp_project) assert result["generated"] == [] assert "skipped" in result assert len(result["skipped"]) > 0 class TestBodyTextGeneration: """Tests for body-text section filling.""" def test_tbd_placeholder_replaced(self, tmp_project): """TBD placeholders should be replaced with discovery answer.""" from rdb_discovery.generate_context import generate_context_files result = generate_context_files(tmp_project) assert "context/product-brief.md" in result["generated"] content = (tmp_project / "context/product-brief.md").read_text() # Q-001 answer should replace TBD under ## Problem assert "CLI tool for discovery workflow" in content assert "## Problem" in content # The rest of the file must be preserved assert "## Success Criteria" in content assert "## Minimum Useful Version" in content class TestTableGeneration: """Tests for table-based section row generation.""" def test_risks_table_gets_row(self, tmp_project): """Q-007 (risks) should add a row to risks.md table.""" from rdb_discovery.generate_context import generate_context_files result = generate_context_files(tmp_project) assert "context/risks.md" in result["generated"] content = (tmp_project / "context/risks.md").read_text() assert "| RISK-007 |" in content assert "Scope creep" in content class TestContentPreservation: """Tests that existing content is preserved.""" def test_existing_development_context_preserved(self, tmp_project): """Existing non-TBD content should be preserved and new content appended.""" from rdb_discovery.generate_context import generate_context_files generate_context_files(tmp_project) content = (tmp_project / "context/development-context.md").read_text() assert "Python 3.12, Typer, Rich." in content assert "VS Code and Cursor." in content class TestContextMapCompleteness: """Tests for mapping document completeness.""" def test_all_10_questions_mapped(self): """Every core question should have a mapping entry.""" from rdb_discovery.generate_context import CONTEXT_MAP expected_ids = {f"Q-{i:03d}" for i in range(1, 11)} mapped_ids = set(CONTEXT_MAP.keys()) assert expected_ids == mapped_ids, f"Missing mappings: {expected_ids - mapped_ids}" def test_each_question_has_at_least_one_target(self): """Every question mapping should target at least one file.""" from rdb_discovery.generate_context import CONTEXT_MAP for qid, targets in CONTEXT_MAP.items(): assert len(targets) >= 1, f"{qid} has no target files" for file_path, section_name, fmt in targets: assert file_path, f"{qid}: empty file path" assert fmt in ("body", "table", "append"), f"{qid}: invalid format {fmt}" class TestCLICommand: """Tests that the CLI 'generate' command is available.""" def test_generate_command_exists(self): """The 'rdb generate' command should be registered in the CLI.""" from typer.testing import CliRunner from rdb_discovery.cli import app runner = CliRunner() result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 assert "generate" in result.output.lower() or "-g" in result.output.lower() def test_generate_command_with_empty_discovery(self): """The CLI generate command handles empty discovery gracefully.""" from typer.testing import CliRunner from rdb_discovery.cli import app from pathlib import Path import tempfile with tempfile.TemporaryDirectory() as td: root = Path(td) / "proj" root.mkdir() (root / "context").mkdir() (root / "context" / "discovery-log.md").write_text( "# Discovery Log\n\n| ID | Question |\n|---|---|\n", encoding="utf-8", ) runner = CliRunner() result = runner.invoke(app, ["generate"], catch_exceptions=False) assert result.exit_code == 0 def test_generate_creates_missing_files(self): """Generate should create context files that don't yet exist.""" from pathlib import Path import tempfile import shutil tmp_path = Path(tempfile.mkdtemp()) root = tmp_path / "testproject" root.mkdir() (root / "context").mkdir() # Write only discovery-log.md — no context files (root / "context" / "discovery-log.md").write_text( "# Discovery Log\n\n" "| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n" "|---|---|---|---|---|---|---|---|\n" '| Q-007 | What are the risks? | Some risk description | High | No | | | 2026-06-03 |\n', encoding="utf-8", ) from rdb_discovery.generate_context import generate_context_files result = generate_context_files(root) assert "context/risks.md" in result["generated"] assert (root / "context" / "risks.md").exists() shutil.rmtree(tmp_path) class TestIntegration: """End-to-end integration tests for the full generation pipeline.""" def test_full_generation_flow(self, tmp_project): """All mapped answers are written to correct files with correct confidence filtering.""" from rdb_discovery.generate_context import generate_context_files result = generate_context_files(tmp_project) # High/Medium answers should be generated assert "context/product-brief.md" in result["generated"] assert "context/project-brief.md" in result["generated"] assert "context/risks.md" in result["generated"] assert "context/assumptions.md" not in result["generated"] # Q-008 is Low confidence # Check that product-brief has filled sections content = (tmp_project / "context/product-brief.md").read_text() assert "CLI tool for discovery workflow" in content assert "Internal developers and small dev teams" in content # Check that risks.md got a table row risks = (tmp_project / "context/risks.md").read_text() assert "| RISK-007 |" in risks assert "Scope creep" in risks def test_preserves_existing_infrastructure_content(self, tmp_project): """Existing content in infrastructure-context.md is preserved.""" from rdb_discovery.generate_context import generate_context_files original = (tmp_project / "context/infrastructure-context.md").read_text() generate_context_files(tmp_project) result = (tmp_project / "context/infrastructure-context.md").read_text() assert "Cloud provider details" in result # Original content preserved