"""Tests for context health report (TASK-021).""" import pytest from pathlib import Path from textwrap import dedent 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 context files and discovery answers.""" root = tmp_path / "testproject" root.mkdir() (root / "context").mkdir() # Write discovery-log.md 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-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\nDone\n", encoding="utf-8", ) # Write architecture.md with some TBDs (root / "context" / "architecture.md").write_text( "# Architecture\n\n## Overview\n\nTDB — Not yet defined.\n\n## Core Components\n\nTBD\n\n## Data Flow\n\nDone\n", encoding="utf-8", ) # Write decisions.md (present, non-empty) (root / "context" / "decisions.md").write_text( "# Decisions\n\n| ID | Decision | Reason | Date |\n|---|---|---|---|\n", encoding="utf-8", ) # Write risks.md and assumptions.md (root / "context" / "risks.md").write_text( "# Risks\n\n| ID | Risk | Impact | Mitigation | Status |\n|---|---|---|---|---|\n", encoding="utf-8", ) (root / "context" / "assumptions.md").write_text( "# Assumptions\n\n| ID | Assumption | Confidence | Validation Needed |\n|---|---|---|---|\n", encoding="utf-8", ) # Write open-questions.md (present) (root / "context" / "open-questions.md").write_text( "# Open Questions\n\n| ID | Question | Reason | Owner | Status |\n|---|---|---|---|---|\n", encoding="utf-8", ) # Write company-context.md, development-context.md, infrastructure-context.md with TBDs (root / "context" / "company-context.md").write_text( "# Company Context\n\n## Mission\n\nTBD\n\n## Products & Services\n\nDone.\n", encoding="utf-8", ) (root / "context" / "development-context.md").write_text( "# Development Context\n\n## Tech Stack\n\nPython 3.12, Typer, Rich.\n\n## Coding Standards\n\nTBD\n", encoding="utf-8", ) (root / "context" / "infrastructure-context.md").write_text( "# Infrastructure Context\n\n## Hosting\n\nTBD — Cloud provider details.\n", encoding="utf-8", ) # Write agent-guidelines.md and repository-context.md (present) (root / "context" / "agent-guidelines.md").write_text("# Agent Guidelines\n\nDone.\n", encoding="utf-8") (root / "context" / "repository-context.md").write_text("# Repository Context\n\nPurpose: Test project.\n", encoding="utf-8") # Write PROJECT_STATE.md, TASKS.md, TEST_PLAN.md, RUN_LOG.md, AGENT_HANDOFF.md (root / "PROJECT_STATE.md").write_text("# Project State\n\nCurrent Stage: BUILDING\n", encoding="utf-8") (root / "TASKS.md").write_text("# TASKS.md\n## TASK-001\nStatus: Done\n", encoding="utf-8") (root / "TEST_PLAN.md").write_text("# TEST_PLAN.md\n## Tests\nDone.\n", encoding="utf-8") (root / "RUN_LOG.md").write_text("# RUN_LOG.md\n| Date | Event | Task | Notes |\n|---|---|---|---|\n", encoding="utf-8") (root / "AGENT_HANDOFF.md").write_text("# Agent Handoff\n## Current Stage\nBUILDING\n", encoding="utf-8") # Create .rdb directory (root / ".rdb").mkdir() return root class TestExpectedFiles: """Tests for expected file reporting.""" def test_all_expected_files_returned(self, tmp_project): """Every standard context file should appear in the expected list.""" from rdb_discovery.context_status import _expected_files files = _expected_files(tmp_project) assert len(files) == 18 def test_missing_file_detected(self, tmp_project): """Files that don't exist should be marked as missing.""" # project-brief.md is not created by this fixture (tmp_project / "context" / "project-brief.md").unlink(missing_ok=True) from rdb_discovery.context_status import _check_expected_files results = _check_expected_files(tmp_project) paths_by_status = {r["path"]: r["status"] for r in results} assert paths_by_status.get("context/project-brief.md") == "missing" def test_present_file_detected(self, tmp_project): """Files that exist should be marked as present.""" from rdb_discovery.context_status import _check_expected_files results = _check_expected_files(tmp_project) paths_by_status = {r["path"]: r["status"] for r in results} assert paths_by_status["TASKS.md"] == "present" assert paths_by_status["PROJECT_STATE.md"] == "present" class TestTbdDetection: """Tests for TBD placeholder detection.""" def test_tbd_sections_detected(self, tmp_project): """Sections with TBD/TDB placeholders should be reported.""" from rdb_discovery.context_status import _check_tbd_sections findings = _check_tbd_sections(tmp_project) assert len(findings) > 0 paths = {f["file"] for f in findings} assert "product-brief.md" in paths # has TBD sections assert "architecture.md" in paths # has TDB section def test_no_tbd_in_clean_files(self, tmp_project): """Files without TBD placeholders should not appear.""" from rdb_discovery.context_status import _check_tbd_sections findings = _check_tbd_sections(tmp_project) # TASKS.md has "Status: Done" — no TBD for f in findings: assert f["file"] != "TASKS.md" or not any( "TBD" in str(getattr(f, "placeholder", "")) or "TDB" in str(getattr(f, "placeholder", "")) for _ in [1] ) def test_tbd_sections_returns_empty_for_no_context_dir(self): """Should return empty list when no context dir exists.""" from rdb_discovery.context_status import _check_tbd_sections findings = _check_tbd_sections(_get_project_root() / "nonexistent") assert findings == [] class TestLowConfidence: """Tests for low-confidence discovery answer detection.""" def test_low_confidence_detected(self, tmp_project): """Low-confidence answers should be detected.""" from rdb_discovery.context_status import _check_low_confidence low = _check_low_confidence(tmp_project) assert len(low) == 1 assert low[0]["id"] == "Q-008" assert low[0]["confidence"] == "Low" def test_high_confidence_not_flagged(self, tmp_project): """High-confidence answers should not appear in low_confidence results.""" from rdb_discovery.context_status import _check_low_confidence low = _check_low_confidence(tmp_project) ids = {a["id"] for a in low} assert "Q-001" not in ids # Q-001 is High confidence def test_medium_confidence_not_flagged(self, tmp_project): """Medium-confidence answers should not appear in low_confidence results.""" from rdb_discovery.context_status import _check_low_confidence low = _check_low_confidence(tmp_project) ids = {a["id"] for a in low} assert "Q-002" not in ids # Q-002 is Medium confidence class TestHealthScore: """Tests for health score computation.""" def test_full_score_when_everything_complete(self): """Score should be near 100 when all files present, no TBDs, no low-conf.""" from rdb_discovery.context_status import compute_health_score expected = [{"path": "a", "status": "present", "size": "0"} for _ in range(18)] score = compute_health_score(expected, tbd_sections=0, low_conf_count=0) # 45 (files) + 30 (no TBD) + 15 (no low-conf) = 90, discovery bonus later adds to it assert score >= 90 def test_score_degrades_with_missing_files(self): """Score should decrease as files are missing.""" from rdb_discovery.context_status import compute_health_score full = [ {"path": f"file{i}", "status": "present", "size": "0"} for i in range(18) ] score_full = compute_health_score(full, tbd_sections=0, low_conf_count=0) partial = [ {"path": "a", "status": "present", "size": "0"}, {"path": "b", "status": "missing", "size": "0"}, {"path": "c", "status": "missing", "size": "0"}, ] + [ {"path": f"file{i}", "status": "present", "size": "0"} for i in range(15) ] score_partial = compute_health_score(partial, tbd_sections=0, low_conf_count=0) assert score_full > score_partial def test_score_degrades_with_tbd_sections(self): """Score should decrease as TBD sections increase.""" from rdb_discovery.context_status import compute_health_score expected = [{"path": "a", "status": "present", "size": "0"} for _ in range(18)] score_0 = compute_health_score(expected, tbd_sections=0, low_conf_count=0) score_5 = compute_health_score(expected, tbd_sections=5, low_conf_count=0) score_20 = compute_health_score(expected, tbd_sections=20, low_conf_count=0) assert score_0 > score_5 assert score_5 > score_20 # At 20+ TBDs, TBD score component is 0 assert score_20 <= score_5 - 15 class TestContextStatus: """Tests for the main context_status function.""" def test_returns_score(self, tmp_project): """context_status should return a score in the report.""" from rdb_discovery.context_status import context_status report = context_status(tmp_project) assert "score" in report assert 0 <= report["score"] <= 110 # allows for discovery bonus def test_returns_summary(self, tmp_project): """Report should contain a summary dict.""" from rdb_discovery.context_status import context_status report = context_status(tmp_project) assert "summary" in report assert "total_expected" in report["summary"] assert "missing" in report["summary"] def test_returns_expected_files(self, tmp_project): """Report should list all expected files.""" from rdb_discovery.context_status import context_status report = context_status(tmp_project) assert len(report["expected_files"]) == 18 def test_returns_tbd_sections(self, tmp_project): """Report should include TBD section findings.""" from rdb_discovery.context_status import context_status report = context_status(tmp_project) assert "tbd_sections" in report assert len(report["tbd_sections"]) > 0 def test_returns_low_confidence(self, tmp_project): """Report should include low-confidence answers.""" from rdb_discovery.context_status import context_status report = context_status(tmp_project) assert "low_confidence" in report assert len(report["low_confidence"]) == 1 class TestCLICommand: """Tests that the CLI 'context-status' command is available and works.""" def test_context_status_command_exists(self): """The 'rdb context-status' command should be registered.""" from typer.testing import CliRunner from rdb_discovery.cli import app runner = CliRunner() result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 assert "context-status" in result.output.lower() def test_context_status_runs_successfully(self): """The CLI command should exit cleanly.""" from typer.testing import CliRunner from rdb_discovery.cli import app runner = CliRunner() result = runner.invoke(app, ["context-status"], catch_exceptions=False) assert result.exit_code == 0 def test_context_status_output_contains_score(self): """The output should contain the health score.""" from typer.testing import CliRunner from rdb_discovery.cli import app runner = CliRunner() result = runner.invoke(app, ["context-status"], catch_exceptions=False) assert result.exit_code == 0 # Score is in format like "92.5/100" assert "/100" in result.output def test_context_status_reports_missing_files(self): """Missing files should be detected by the core function.""" from pathlib import Path import tempfile with tempfile.TemporaryDirectory() as td: root = Path(td) / "proj" root.mkdir() (root / "context").mkdir() # No context files created — everything missing from rdb_discovery.context_status import _check_expected_files results = _check_expected_files(root) paths_by_status = {r["path"]: r["status"] for r in results} assert paths_by_status.get("TASKS.md") == "missing" def test_context_status_reports_tbd_sections(self): """TBD placeholders should be detected by the core function.""" from pathlib import Path import tempfile with tempfile.TemporaryDirectory() as td: root = Path(td) / "proj" root.mkdir() (root / "context").mkdir() # Write a file with TBD placeholder (root / "context" / "product-brief.md").write_text( "# Product Brief\n\n## Problem\n\nTBD\n", encoding="utf-8", ) from rdb_discovery.context_status import _check_tbd_sections findings = _check_tbd_sections(root) assert len(findings) > 0 paths = {f["file"] for f in findings} assert "product-brief.md" in paths def test_context_status_reports_low_confidence(self): """Low-confidence answers should be detected by the core function.""" from pathlib import Path import tempfile with tempfile.TemporaryDirectory() as td: root = Path(td) / "proj" root.mkdir() (root / "context").mkdir() # Write discovery-log.md with a Low-confidence answer (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-003 | What does success look like? | Not sure yet | Low | Yes | | | 2026-06-03 |\n', encoding="utf-8", ) from rdb_discovery.context_status import _check_low_confidence low = _check_low_confidence(root) assert len(low) == 1 assert low[0]["id"] == "Q-003" class TestHealthScoreColor: """Tests for health score color coding.""" def test_full_score_is_high(self): """All files present, no TBDs, no low-conf should produce a high score.""" from pathlib import Path import tempfile with tempfile.TemporaryDirectory() as td: root = Path(td) / "proj" root.mkdir() (root / "context").mkdir() for f in [ "discovery-log.md", "product-brief.md", "architecture.md", "decisions.md", "risks.md", "assumptions.md", "open-questions.md", ]: (root / "context" / f).write_text(f"# {f}\nDone\n", encoding="utf-8") for f in ["TASKS.md", "TEST_PLAN.md", "RUN_LOG.md", "PROJECT_STATE.md", "AGENT_HANDOFF.md"]: (root / f).write_text(f"# {f}\nDone\n", encoding="utf-8") from rdb_discovery.context_status import context_status report = context_status(root) assert report["score"] >= 70 def test_empty_project_score_is_below_full(self): """A completely empty project should score well below a full project.""" from pathlib import Path import tempfile with tempfile.TemporaryDirectory() as td: root = Path(td) / "proj" root.mkdir() # Don't create any files from rdb_discovery.context_status import context_status report = context_status(root) assert report["score"] < 70 def test_cli_score_appears_in_output(self): """The CLI should display the numeric score in output.""" from typer.testing import CliRunner from rdb_discovery.cli import app runner = CliRunner() result = runner.invoke(app, ["context-status"], catch_exceptions=False) assert result.exit_code == 0 # Score format is like "92.5/100" — digits followed by /100 assert "/100" in result.output