from __future__ import annotations import json import tempfile import unittest from io import BytesIO from pathlib import Path from reportlab.pdfgen import canvas from us_cpa.cases import CaseManager from us_cpa.prepare import PrepareEngine from us_cpa.review import ReviewEngine, render_review_memo, render_review_summary from us_cpa.sources import TaxYearCorpus, bootstrap_irs_catalog class ReviewEngineTests(unittest.TestCase): def build_prepared_case(self, temp_dir: str) -> tuple[Path, TaxYearCorpus]: case_dir = Path(temp_dir) / "2025-jane-doe" manager = CaseManager(case_dir) manager.create_case(case_label="Jane Doe", tax_year=2025) manager.intake( tax_year=2025, user_facts={ "taxpayer.fullName": "Jane Doe", "filingStatus": "single", "wages": 50000, "taxableInterest": 100, "federalWithholding": 6000, }, document_paths=[], ) corpus = TaxYearCorpus(cache_root=Path(temp_dir) / "cache") def fake_fetch(url: str) -> bytes: buffer = BytesIO() pdf = canvas.Canvas(buffer) pdf.drawString(72, 720, f"Template for {url}") pdf.save() return buffer.getvalue() corpus.download_catalog(2025, bootstrap_irs_catalog(2025), fetcher=fake_fetch) PrepareEngine(corpus=corpus).prepare_case(case_dir) return case_dir, corpus def test_review_detects_mismatched_return_and_missing_artifacts(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: case_dir, corpus = self.build_prepared_case(temp_dir) normalized_path = case_dir / "return" / "normalized-return.json" normalized = json.loads(normalized_path.read_text()) normalized["totals"]["adjustedGrossIncome"] = 99999.0 normalized_path.write_text(json.dumps(normalized, indent=2)) artifacts_path = case_dir / "output" / "artifacts.json" artifacts = json.loads(artifacts_path.read_text()) artifacts["artifacts"] = [] artifacts["artifactCount"] = 0 artifacts_path.write_text(json.dumps(artifacts, indent=2)) review = ReviewEngine(corpus=corpus).review_case(case_dir) self.assertEqual(review["status"], "reviewed") self.assertEqual(review["findings"][0]["severity"], "high") self.assertIn("adjusted gross income", review["findings"][0]["title"].lower()) self.assertTrue(any("missing rendered artifact" in item["title"].lower() for item in review["findings"])) def test_review_renderers_produce_summary_and_memo(self) -> None: review = { "status": "reviewed", "findings": [ { "severity": "high", "title": "Adjusted gross income mismatch", "explanation": "Stored AGI does not match recomputed AGI.", "suggestedAction": "Update Form 1040 line 11.", "authorities": [{"title": "Instructions for Form 1040 and Schedules 1-3"}], } ], } summary = render_review_summary(review) memo = render_review_memo(review) self.assertIn("Adjusted gross income mismatch", summary) self.assertIn("# Review Memo", memo) self.assertIn("Suggested correction", memo) if __name__ == "__main__": unittest.main()