"""Unit tests for rule-based parser.""" SAMPLE_TEXT_POSITIVE = """ Item 2.02 Results of Operations Apple today announced record quarterly revenue of $124.3 billion, up 9 percent year over year. Guidance raised for Q2: The Company expects revenue to be between $125 billion and $131 billion. Demand remains strong across all product categories. Gross margin expansion driven by Services mix. Customer additions in enterprise continue to accelerate. Backlog increased significantly year over year. Adjusted EPS excludes certain non-GAAP items. """ SAMPLE_TEXT_NEGATIVE = """ Item 2.02 Results of Operations Company reported revenue of $5.2 billion, missing expectations. Guidance lowered for next quarter due to demand softness. Margin compression continues. Financing need announced with dilutive convertible note offering. """ SAMPLE_TEXT_CONTRACT = """ Item 1.01 Entry into Material Agreement The Company has entered into a definitive agreement with a major enterprise customer for a multi-year supply contract worth $500 million. The contract includes recurring revenue. """ def test_parse_earnings_positive(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() out = p.parse("DOC::test", "8-K", SAMPLE_TEXT_POSITIVE, {"filing_date": "2026-01-29"}) assert out.event_type == "earnings_release" assert out.guidance.status == "raised" assert out.signals.demand_strength == "strong" assert out.event_direction == "bullish" def test_parse_earnings_negative(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() out = p.parse("DOC::test", "8-K", SAMPLE_TEXT_NEGATIVE, {"filing_date": "2026-02-01"}) assert out.event_type == "earnings_release" assert out.guidance.status == "lowered" assert out.event_direction in ("bearish", "mixed") def test_parse_material_contract(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() out = p.parse("DOC::test", "8-K", SAMPLE_TEXT_CONTRACT, {"filing_date": "2026-02-15"}) assert out.event_type == "material_contract" def test_parse_non_gaap_flag(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() out = p.parse("DOC::test", "8-K", SAMPLE_TEXT_POSITIVE, {"filing_date": "2026-01-29"}) assert out.risk_flags.non_gaap_heavy is True def test_parse_margin_improving(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() out = p.parse("DOC::test", "8-K", SAMPLE_TEXT_POSITIVE, {"filing_date": "2026-01-29"}) assert out.signals.margin_quality == "improving" def test_parse_confidence_range(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() out = p.parse("DOC::test", "8-K", SAMPLE_TEXT_POSITIVE, {"filing_date": "2026-01-29"}) assert 0.0 <= out.confidence.overall <= 1.0 def test_parse_document_id_preserved(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() out = p.parse("DOC::my-id", "8-K", SAMPLE_TEXT_POSITIVE, {"filing_date": "2026-01-29"}) assert out.document_id == "DOC::my-id" def test_parse_schema_version(): from libs.parser.rule_parser import SCHEMA_VERSION, RuleBasedParser p = RuleBasedParser() out = p.parse("DOC::test", "8-K", SAMPLE_TEXT_POSITIVE, {"filing_date": "2026-01-29"}) assert out.schema_version == SCHEMA_VERSION def test_parse_unknown_event(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() out = p.parse("DOC::test", "8-K", "No items mentioned here.", {"filing_date": "2026-01-29"}) assert out.event_type == "unknown" assert len(out.warnings) > 0 def test_parse_filing_time_bucket_set(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() out = p.parse( "DOC::test", "8-K", SAMPLE_TEXT_POSITIVE, {"filing_date": "2026-01-29", "accepted_at_utc": "2026-01-29T21:05:00Z"}, ) assert out.filing_time_bucket == "post_market" def test_parse_html_earnings_release_without_item_numbers(): from libs.parser.rule_parser import RuleBasedParser from libs.parser.text_normalizer import normalize_text p = RuleBasedParser() html = """
FOR IMMEDIATE RELEASE
Ciena Reports Fiscal First Quarter 2026 Financial Results
Providing revenue guidance of $1.5 billion plus or minus $50 million.
Raising revenue guidance range for fiscal year 2026.
""" out = p.parse( "DOC::test", "8-K", normalize_text(html, is_html=True), {"filing_date": "2026-03-05"}, ) assert out.event_type == "earnings_release" assert out.guidance.status == "raised" assert out.event_direction == "bullish" def test_guidance_withdrawn_does_not_match_distant_words(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() text = ( "The company may withdraw from certain trade agreements. " "Management also discussed guidance measures for fiscal 2026." ) out = p.parse("DOC::test", "8-K", text, {"filing_date": "2026-03-05"}) assert out.guidance.status == "not_provided" def test_positive_backlog_and_customer_signals_outweigh_single_financing_flag(): from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() text = """ Company reports first quarter financial results. Raising full-year guidance. Backlog increased significantly year over year. Added new enterprise customers during the quarter. The release also references refinancing our term loan. """ out = p.parse("DOC::test", "8-K", text, {"filing_date": "2026-03-05"}) assert out.event_direction == "bullish" # --------------------------------------------------------------------------- # Item -> event_type classification (regression tests for unknown-class bug # where AMD/MNST 2.02 + 9.01 earnings filings were tagged "unknown"). # --------------------------------------------------------------------------- def test_classify_event_type_amd_style_earnings_release_with_9_01(): """AMD/MNST file 2.02 alongside 9.01; must classify as earnings_release.""" from libs.parser.rule_parser import _classify_event_type # Order in either direction — earnings still wins. assert _classify_event_type(["2.02", "9.01"]) == "earnings_release" assert _classify_event_type(["9.01", "2.02"]) == "earnings_release" def test_classify_event_type_tolerates_dirty_item_strings(): """Upstream may store entries like 'Item 2.02' or '2.02 - Results...'.""" from libs.parser.rule_parser import _classify_event_type assert _classify_event_type(["Item 2.02", "Item 9.01"]) == "earnings_release" assert ( _classify_event_type( ["Item 2.02 Results of Operations and Financial Condition"] ) == "earnings_release" ) assert _classify_event_type(["2.02 - Results of Operations"]) == "earnings_release" def test_classify_event_type_known_mappings_each_item(): from libs.parser.rule_parser import _classify_event_type assert _classify_event_type(["7.01"]) == "guidance_update" assert _classify_event_type(["1.01"]) == "material_contract" assert _classify_event_type(["8.01"]) == "other_material_event" assert _classify_event_type(["1.03"]) == "other_material_event" # 5.02 (departure of officers) -> management_change, even when co-filed # with 9.01 financial-statements-and-exhibits. assert _classify_event_type(["5.02", "9.01"]) == "management_change" def test_classify_event_type_negative_cases_remain_unknown(): """Items that aren't in the strategy vocabulary must remain 'unknown'. This guards against accidental over-eager relabeling. """ from libs.parser.rule_parser import _classify_event_type # 9.01 alone (financial statements & exhibits) is not its own event type assert _classify_event_type(["9.01"]) == "unknown" # 2.03 (financial obligation), 3.01 (delisting), 5.07 (shareholder vote) assert _classify_event_type(["2.03"]) == "unknown" assert _classify_event_type(["3.01"]) == "unknown" assert _classify_event_type(["5.07"]) == "unknown" # No items at all assert _classify_event_type([]) == "unknown" # Description-only with no extractable code assert ( _classify_event_type(["Results of Operations and Financial Condition"]) == "unknown" ) def test_parse_amd_style_earnings_with_only_9_01_in_item_numbers_uses_text_fallback(): """If the SGML loss left only 9.01 in item_numbers but the body says 'AMD Reports Q3 Financial Results', the body regex must catch it.""" from libs.parser.rule_parser import RuleBasedParser from libs.parser.text_normalizer import normalize_text p = RuleBasedParser() html = """
NEWS RELEASE
AMD Reports Third Quarter 2025 Financial Results
Revenue of $7.7 billion grew year over year.
""" out = p.parse( "DOC::test", "8-K", normalize_text(html, is_html=True), {"filing_date": "2025-10-29", "item_numbers": ["9.01"]}, ) assert out.event_type == "earnings_release" def test_parse_mnst_style_earnings_dirty_item_format_classifies_correctly(): """MNST-style: 'Item 2.02 Results of Operations and Financial Condition' plus 'Item 9.01 ...' as raw entries must classify earnings_release.""" from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() text = ( "Monster Beverage Corporation Reports 2025 Third Quarter Financial Results\n" "Net sales of $1.97 billion." ) out = p.parse( "DOC::test", "8-K", text, { "filing_date": "2025-11-06", "item_numbers": [ "Item 2.02 Results of Operations and Financial Condition", "Item 9.01 Financial Statements and Exhibits", ], }, ) assert out.event_type == "earnings_release" def test_normalize_item_codes_drops_non_string_and_dedupes(): from libs.parser.rule_parser import _normalize_item_codes # Order-preserving + dedup + non-string ignored assert _normalize_item_codes(["Item 2.02", "2.02", "9.01", None, 7]) == [ "2.02", "9.01", ]