460 lines
19 KiB
Python
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)
|