Files
rdb-discovery/tests/test_generate_context.py

460 lines
19 KiB
Python

"""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_questions(self):
"""All core questions should have mappings."""
from rdb_discovery.generate_context import CONTEXT_MAP
from rdb_discovery.templates import CORE_QUESTIONS
expected_count = len(CORE_QUESTIONS)
for i in range(1, expected_count + 1):
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_questions_mapped(self):
"""Every core question should have a mapping entry."""
from rdb_discovery.generate_context import CONTEXT_MAP
from rdb_discovery.templates import CORE_QUESTIONS
expected_count = len(CORE_QUESTIONS)
expected_ids = {f"Q-{i:03d}" for i in range(1, expected_count + 1)}
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/company-context.md" in result["generated"] # Q-002 → company-context
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 company-context got users data from Q-002
company = (tmp_project / "context/company-context.md").read_text()
assert "## Customers / Users" in company
assert "Internal developers and small dev teams" in company
# 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
class TestContextQuality:
"""Tests for TASK-023: improved context generation quality."""
def test_company_context_populated_from_users(self):
"""company-context.md should be populated when discovery answers include users/stakeholders."""
from pathlib import Path
import tempfile
import shutil
tmp_path = Path(tempfile.mkdtemp())
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Only Q-002 (users) - no other discovery data
(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-002 | Who is the user? | Internal dev teams and external customers | 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/company-context.md" in result["generated"]
assert "context/product-brief.md" in result["generated"]
company = (root / "context/company-context.md").read_text()
assert "## Customers / Users" in company
assert "Internal dev teams and external customers" in company
shutil.rmtree(tmp_path)
def test_agent_guidelines_gets_testing_preferences(self):
"""agent-guidelines.md should get Testing Preferences from testing discovery answers."""
from pathlib import Path
import tempfile
import shutil
tmp_path = Path(tempfile.mkdtemp())
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Only Q-009 (testing) - no agent tooling questions
(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-009 | How will we test it? | pytest with coverage, integration tests via docker-compose | Medium | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(root)
assert "context/agent-guidelines.md" in result["generated"]
guidelines = (root / "context/agent-guidelines.md").read_text()
assert "## Testing Preferences" in guidelines
assert "pytest with coverage" in guidelines
shutil.rmtree(tmp_path)
def test_no_testing_answers_in_timeline(self):
"""Testing answers must not be mapped to Timeline & Milestones sections."""
from pathlib import Path
import tempfile
import shutil
tmp_path = Path(tempfile.mkdtemp())
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Only Q-009 (testing) - verify no timeline data appears
(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-009 | How will we test it? | pytest with coverage | High | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
# Create project-brief.md that has a Timeline section already
(root / "context" / "project-brief.md").write_text(
"# Project Brief\n\n## Timeline & Milestones\n\nTBD\n",
encoding="utf-8",
)
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(root)
assert "context/project-brief.md" not in result["generated"]
timeline_content = (root / "context/project-brief.md").read_text()
# The Timeline section should remain as TBD (not filled with testing data)
assert "pytest with coverage" not in timeline_content
# Original TBD placeholder preserved
assert "TBD" in timeline_content
shutil.rmtree(tmp_path)
def test_repository_context_populated_from_dependencies(self):
"""repository-context.md should be populated when answers include dependencies."""
from pathlib import Path
import tempfile
import shutil
tmp_path = Path(tempfile.mkdtemp())
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Q-012 includes technology stack info that maps to repository dependencies
(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-012 | What is the technology stack, repository structure, and architecture approach? | Python 3.12, Typer, Rich; Docker; PostgreSQL | 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/repository-context.md" in result["generated"]
repo_ctx = (root / "context/repository-context.md").read_text()
assert "## Dependencies" in repo_ctx
assert "Python 3.12, Typer, Rich" in repo_ctx
shutil.rmtree(tmp_path)