feat: add us-cpa question engine
This commit is contained in:
@@ -53,13 +53,40 @@ class UsCpaCliSmokeTests(unittest.TestCase):
|
||||
self.assertIn(command, result.stdout)
|
||||
|
||||
def test_question_command_emits_json_by_default(self) -> None:
|
||||
result = self.run_cli("question", "--question", "What is the standard deduction?")
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = str(SRC_DIR)
|
||||
env["US_CPA_CACHE_DIR"] = temp_dir
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "us_cpa.cli", "fetch-year", "--tax-year", "2025"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
check=True,
|
||||
)
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"us_cpa.cli",
|
||||
"question",
|
||||
"--tax-year",
|
||||
"2025",
|
||||
"--question",
|
||||
"What is the standard deduction?",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
payload = json.loads(result.stdout)
|
||||
self.assertEqual(payload["command"], "question")
|
||||
self.assertEqual(payload["format"], "json")
|
||||
self.assertEqual(payload["question"], "What is the standard deduction?")
|
||||
self.assertEqual(payload["status"], "answered")
|
||||
self.assertIn("analysis", payload)
|
||||
|
||||
def test_prepare_requires_case_dir(self) -> None:
|
||||
result = self.run_cli("prepare", "--tax-year", "2025")
|
||||
@@ -128,6 +155,42 @@ class UsCpaCliSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(payload["status"], "needs_resolution")
|
||||
self.assertEqual(payload["issueType"], "fact_conflict")
|
||||
|
||||
def test_question_markdown_memo_mode_renders_tax_memo(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = str(SRC_DIR)
|
||||
env["US_CPA_CACHE_DIR"] = temp_dir
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "us_cpa.cli", "fetch-year", "--tax-year", "2025"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
check=True,
|
||||
)
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"us_cpa.cli",
|
||||
"question",
|
||||
"--tax-year",
|
||||
"2025",
|
||||
"--format",
|
||||
"markdown",
|
||||
"--style",
|
||||
"memo",
|
||||
"--question",
|
||||
"What is the standard deduction?",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("# Tax Memo", result.stdout)
|
||||
self.assertIn("## Conclusion", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
75
skills/us-cpa/tests/test_questions.py
Normal file
75
skills/us-cpa/tests/test_questions.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from us_cpa.questions import QuestionEngine, render_analysis, render_memo
|
||||
from us_cpa.sources import TaxYearCorpus, bootstrap_irs_catalog
|
||||
|
||||
|
||||
class QuestionEngineTests(unittest.TestCase):
|
||||
def build_engine(self, temp_dir: str) -> QuestionEngine:
|
||||
corpus = TaxYearCorpus(cache_root=Path(temp_dir))
|
||||
|
||||
def fake_fetch(url: str) -> bytes:
|
||||
return f"source for {url}".encode()
|
||||
|
||||
corpus.download_catalog(2025, bootstrap_irs_catalog(2025), fetcher=fake_fetch)
|
||||
return QuestionEngine(corpus=corpus)
|
||||
|
||||
def test_standard_deduction_question_returns_structured_analysis(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
engine = self.build_engine(temp_dir)
|
||||
|
||||
analysis = engine.answer(
|
||||
question="What is the standard deduction for single filers?",
|
||||
tax_year=2025,
|
||||
case_facts={"filingStatus": "single"},
|
||||
)
|
||||
|
||||
self.assertEqual(analysis["issue"], "standard_deduction")
|
||||
self.assertEqual(analysis["taxYear"], 2025)
|
||||
self.assertEqual(analysis["conclusion"]["answer"], "$15,000")
|
||||
self.assertEqual(analysis["confidence"], "high")
|
||||
self.assertTrue(analysis["authorities"])
|
||||
self.assertEqual(analysis["authorities"][0]["sourceClass"], "irs_instructions")
|
||||
|
||||
def test_complex_question_flags_primary_law_escalation(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
engine = self.build_engine(temp_dir)
|
||||
|
||||
analysis = engine.answer(
|
||||
question="Does section 469 passive activity loss limitation apply here?",
|
||||
tax_year=2025,
|
||||
case_facts={},
|
||||
)
|
||||
|
||||
self.assertEqual(analysis["confidence"], "low")
|
||||
self.assertTrue(analysis["primaryLawRequired"])
|
||||
self.assertIn("Internal Revenue Code", analysis["missingFacts"][0])
|
||||
|
||||
def test_renderers_produce_conversation_and_memo(self) -> None:
|
||||
analysis = {
|
||||
"issue": "standard_deduction",
|
||||
"taxYear": 2025,
|
||||
"factsUsed": [{"field": "filingStatus", "value": "single"}],
|
||||
"missingFacts": [],
|
||||
"authorities": [{"title": "Instructions for Form 1040 and Schedules 1-3"}],
|
||||
"conclusion": {"answer": "$15,000", "summary": "Single filers use a $15,000 standard deduction for tax year 2025."},
|
||||
"confidence": "high",
|
||||
"followUpQuestions": [],
|
||||
"primaryLawRequired": False,
|
||||
}
|
||||
|
||||
conversation = render_analysis(analysis)
|
||||
memo = render_memo(analysis)
|
||||
|
||||
self.assertIn("$15,000", conversation)
|
||||
self.assertIn("Issue", memo)
|
||||
self.assertIn("Authorities", memo)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user