Files
rdb-discovery/tests/test_context_status.py
T

432 lines
17 KiB
Python

"""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