""" Comprehensive coverage tests for the constraints package. Targets gaps not covered by existing test suites: - Constraint dataclass edge cases (equality, hashing, mutable defaults, exotic value types) - to_search_terms with non-string values for specialized branches - is_critical boundary across every non-NAME_PATTERN type - ConstraintAnalyzer parsing edge cases (colons in values, extra noise lines, think-tag wrapping around constraint blocks, weight edge values) - Package re-exports from __init__ """ import pytest from unittest.mock import MagicMock from local_deep_research.advanced_search_system.constraints.base_constraint import ( Constraint, ConstraintType, ) from local_deep_research.advanced_search_system.constraints.constraint_analyzer import ( ConstraintAnalyzer, ) from local_deep_research.advanced_search_system.constraints import ( Constraint as ReexportedConstraint, ConstraintType as ReexportedConstraintType, ConstraintAnalyzer as ReexportedConstraintAnalyzer, ) # --------------------------------------------------------------------------- # Helper # --------------------------------------------------------------------------- def _analyzer(): """Create a ConstraintAnalyzer with a mock model.""" a = ConstraintAnalyzer.__new__(ConstraintAnalyzer) a.model = MagicMock() return a def _mock_response(analyzer, content: str): analyzer.model.invoke.return_value = MagicMock(content=content) def _make(type_=ConstraintType.PROPERTY, value="v", weight=1.0, **kw): return Constraint( id=kw.pop("id", "c1"), type=type_, description=kw.pop("description", "desc"), value=value, weight=weight, **kw, ) # =================================================================== # Package __init__ re-exports # =================================================================== class TestPackageReexports: def test_constraint_reexported(self): assert ReexportedConstraint is Constraint def test_constraint_type_reexported(self): assert ReexportedConstraintType is ConstraintType def test_constraint_analyzer_reexported(self): assert ReexportedConstraintAnalyzer is ConstraintAnalyzer # =================================================================== # ConstraintType enum — identity and iteration # =================================================================== class TestConstraintTypeIteration: def test_all_values_are_strings(self): for ct in ConstraintType: assert isinstance(ct.value, str) def test_values_are_lowercase(self): for ct in ConstraintType: assert ct.value == ct.value.lower() def test_enum_members_are_unique(self): values = [ct.value for ct in ConstraintType] assert len(values) == len(set(values)) # =================================================================== # Constraint dataclass — edge cases # =================================================================== class TestConstraintDataclassEdgeCases: def test_equality_same_fields(self): a = _make() b = _make() assert a == b def test_inequality_different_id(self): a = _make(id="c1") b = _make(id="c2") assert a != b def test_metadata_default_not_shared_between_instances(self): """Each instance should get its own empty dict, not a shared one.""" a = _make() b = _make() a.metadata["x"] = 1 assert "x" not in b.metadata def test_value_can_be_list(self): c = _make(value=["a", "b"]) assert c.value == ["a", "b"] def test_value_can_be_dict(self): c = _make(value={"key": 42}) assert c.value == {"key": 42} def test_value_can_be_none(self): c = _make(value=None) assert c.value is None def test_value_can_be_bool(self): c = _make(value=True) assert c.value is True def test_weight_zero(self): c = _make(weight=0.0) assert c.weight == 0.0 def test_weight_negative(self): c = _make(weight=-0.5) assert c.weight == -0.5 def test_description_with_unicode(self): c = _make(description="Formed during Würm glaciation") assert "Würm" in c.description # =================================================================== # to_search_terms — non-string values in specialized branches # =================================================================== class TestToSearchTermsEdgeCases: def test_property_with_numeric_value(self): c = _make(type_=ConstraintType.PROPERTY, value=42) # PROPERTY branch returns self.value directly (an int) assert c.to_search_terms() == 42 def test_name_pattern_with_numeric_value(self): c = _make(type_=ConstraintType.NAME_PATTERN, value=123) result = c.to_search_terms() assert "123" in result assert "name trail mountain" in result def test_event_with_numeric_value(self): c = _make(type_=ConstraintType.EVENT, value=2020) result = c.to_search_terms() assert "2020" in result assert "accident incident" in result def test_statistic_with_numeric_value(self): c = _make(type_=ConstraintType.STATISTIC, value=3.14) result = c.to_search_terms() assert "3.14" in result assert "statistics data" in result def test_fallback_branch_for_comparison(self): c = _make(type_=ConstraintType.COMPARISON, value=99) assert c.to_search_terms() == "99" def test_fallback_branch_for_existence(self): c = _make(type_=ConstraintType.EXISTENCE, value="has feature") assert c.to_search_terms() == "has feature" def test_empty_string_value_property(self): c = _make(type_=ConstraintType.PROPERTY, value="") assert c.to_search_terms() == "" def test_empty_string_value_name_pattern(self): c = _make(type_=ConstraintType.NAME_PATTERN, value="") assert c.to_search_terms() == " name trail mountain" # =================================================================== # is_critical — boundary for every non-NAME_PATTERN type # =================================================================== class TestIsCriticalAllTypes: """Verify is_critical at the 0.8 boundary for every non-NAME_PATTERN type.""" @pytest.mark.parametrize( "ctype", [ ConstraintType.PROPERTY, ConstraintType.EVENT, ConstraintType.STATISTIC, ConstraintType.TEMPORAL, ConstraintType.LOCATION, ConstraintType.COMPARISON, ConstraintType.EXISTENCE, ], ) def test_weight_0_8_not_critical(self, ctype): c = _make(type_=ctype, weight=0.8) assert c.is_critical() is False @pytest.mark.parametrize( "ctype", [ ConstraintType.PROPERTY, ConstraintType.EVENT, ConstraintType.STATISTIC, ConstraintType.TEMPORAL, ConstraintType.LOCATION, ConstraintType.COMPARISON, ConstraintType.EXISTENCE, ], ) def test_weight_0_81_critical(self, ctype): c = _make(type_=ctype, weight=0.81) assert c.is_critical() is True def test_name_pattern_with_weight_zero_still_critical(self): c = _make(type_=ConstraintType.NAME_PATTERN, weight=0.0) assert c.is_critical() is True def test_default_weight_1_0_is_critical(self): c = _make(type_=ConstraintType.PROPERTY) assert c.is_critical() is True # =================================================================== # ConstraintAnalyzer._parse_constraint_type — extra edge cases # =================================================================== class TestParseConstraintTypeEdges: def test_whitespace_in_type_string(self): a = _analyzer() # The code does .lower() but NOT .strip() — spaces would cause fallback result = a._parse_constraint_type(" property ") assert ( result == ConstraintType.PROPERTY ) # Falls to default since " property " != "property" def test_all_caps_name_pattern(self): a = _analyzer() assert ( a._parse_constraint_type("NAME_PATTERN") == ConstraintType.NAME_PATTERN ) def test_numeric_string_defaults_to_property(self): a = _analyzer() assert a._parse_constraint_type("42") == ConstraintType.PROPERTY # =================================================================== # ConstraintAnalyzer._parse_weight — extra edge cases # =================================================================== class TestParseWeightEdges: def test_zero_int(self): a = _analyzer() assert a._parse_weight(0) == 0.0 def test_zero_float(self): a = _analyzer() assert a._parse_weight(0.0) == 0.0 def test_negative_float(self): a = _analyzer() assert a._parse_weight(-0.5) == -0.5 def test_large_float(self): a = _analyzer() assert a._parse_weight(100.0) == 100.0 def test_string_integer_no_decimal(self): a = _analyzer() assert a._parse_weight("1") == 1.0 def test_string_with_leading_text(self): a = _analyzer() assert a._parse_weight("Weight is 0.6 approximately") == 0.6 def test_string_with_multiple_numbers_takes_first(self): a = _analyzer() assert a._parse_weight("0.3 or maybe 0.9") == 0.3 def test_bool_true_treated_as_numeric(self): """bool is subclass of int, so True -> 1.0.""" a = _analyzer() assert a._parse_weight(True) == 1.0 def test_bool_false_treated_as_numeric(self): a = _analyzer() assert a._parse_weight(False) == 0.0 def test_list_returns_default(self): a = _analyzer() assert a._parse_weight([0.5]) == 1.0 def test_dict_returns_default(self): a = _analyzer() assert a._parse_weight({"w": 0.5}) == 1.0 # =================================================================== # ConstraintAnalyzer.extract_constraints — parsing edge cases # =================================================================== class TestExtractConstraintsEdgeCases: def test_value_containing_colon(self): """Values with colons should be preserved after the first split.""" a = _analyzer() _mock_response( a, ( "CONSTRAINT_1:\n" "Type: temporal\n" "Description: Time range\n" "Value: between 10:00 and 14:00\n" "Weight: 0.9\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 1 assert cs[0].value == "between 10:00 and 14:00" def test_noise_lines_are_ignored(self): """Lines that don't match key: value or CONSTRAINT_ should be skipped.""" a = _analyzer() _mock_response( a, ( "Here is my analysis:\n" "\n" "CONSTRAINT_1:\n" "Type: property\n" "Description: Must be red\n" "Value: red\n" "Weight: 0.7\n" "\n" "Let me also note something extra.\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 1 assert cs[0].value == "red" def test_think_tags_wrapping_entire_block(self): """Think tags around the whole response should be stripped.""" a = _analyzer() _mock_response( a, ( "I need to think about this carefully.\n" "Let me consider the constraints.\n" "CONSTRAINT_1:\n" "Type: location\n" "Description: Must be in Europe\n" "Value: Europe\n" "Weight: 0.8\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 1 assert cs[0].type == ConstraintType.LOCATION def test_orphaned_think_close_tag(self): a = _analyzer() _mock_response( a, ( "\n" "CONSTRAINT_1:\n" "Type: event\n" "Description: Happened recently\n" "Value: recent event\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 1 def test_only_last_constraint_present(self): """A single constraint without a following CONSTRAINT_ marker.""" a = _analyzer() _mock_response( a, ( "CONSTRAINT_1:\n" "Type: existence\n" "Description: Must exist\n" "Value: exists\n" "Weight: 1.0\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 1 assert cs[0].id == "c1" def test_missing_type_still_has_required_fields(self): """If type is missing, constraint is skipped because 'type' is not in required keys check. Wait — the code only checks for type/description/value. Let's verify type IS required.""" a = _analyzer() _mock_response(a, ("CONSTRAINT_1:\nDescription: No type\nValue: val\n")) cs = a.extract_constraints("query") # type is not in the required-keys check (only type, description, value) # Actually the check is: k in ["type", "description", "value"] # Without a Type: line, current_constraint won't have "type" key # So the constraint should be skipped assert len(cs) == 0 def test_irrelevant_key_value_lines_ignored(self): """Lines like 'Notes: something' should not corrupt the constraint.""" a = _analyzer() _mock_response( a, ( "CONSTRAINT_1:\n" "Type: property\n" "Description: Must be blue\n" "Value: blue\n" "Weight: 0.5\n" "Notes: This is just a note\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 1 assert cs[0].value == "blue" # "Notes" should not appear anywhere in the constraint assert cs[0].description == "Must be blue" def test_constraint_numbering_is_independent_of_model_numbering(self): """Even if model uses CONSTRAINT_5 and CONSTRAINT_10, IDs are c1, c2.""" a = _analyzer() _mock_response( a, ( "CONSTRAINT_5:\n" "Type: property\n" "Description: A\n" "Value: a\n" "\n" "CONSTRAINT_10:\n" "Type: event\n" "Description: B\n" "Value: b\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 2 assert cs[0].id == "c1" assert cs[1].id == "c2" def test_whitespace_only_response(self): a = _analyzer() _mock_response(a, " \n \n ") cs = a.extract_constraints("query") assert cs == [] def test_weight_with_annotation_parsed_in_full_flow(self): """End-to-end: model returns weight like '0.7 (moderate)'.""" a = _analyzer() _mock_response( a, ( "CONSTRAINT_1:\n" "Type: comparison\n" "Description: Greater than 100\n" "Value: >100\n" "Weight: 0.7 (moderate importance)\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 1 assert cs[0].weight == 0.7 def test_three_constraints_with_one_invalid_middle(self): """First valid, second missing value, third valid — should yield 2 constraints.""" a = _analyzer() _mock_response( a, ( "CONSTRAINT_1:\n" "Type: property\n" "Description: First\n" "Value: v1\n" "\n" "CONSTRAINT_2:\n" "Type: temporal\n" "Description: Missing value field\n" "\n" "CONSTRAINT_3:\n" "Type: location\n" "Description: Third\n" "Value: v3\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 2 assert cs[0].type == ConstraintType.PROPERTY assert cs[1].type == ConstraintType.LOCATION assert cs[0].id == "c1" assert cs[1].id == "c2" def test_unknown_type_defaults_to_property_in_full_flow(self): a = _analyzer() _mock_response( a, ( "CONSTRAINT_1:\n" "Type: magical\n" "Description: Unknown type\n" "Value: val\n" ), ) cs = a.extract_constraints("query") assert len(cs) == 1 assert cs[0].type == ConstraintType.PROPERTY def test_model_invoke_receives_query_in_prompt(self): """Verify the query text is passed to the model.""" a = _analyzer() _mock_response(a, "") a.extract_constraints("What is the capital of France?") call_args = a.model.invoke.call_args[0][0] assert "What is the capital of France?" in call_args # =================================================================== # Constraint list operations (filtering, sorting by criticality) # =================================================================== class TestConstraintListOperations: def test_filter_critical_constraints(self): constraints = [ _make(id="c1", type_=ConstraintType.PROPERTY, weight=0.5), _make(id="c2", type_=ConstraintType.NAME_PATTERN, weight=0.3), _make(id="c3", type_=ConstraintType.TEMPORAL, weight=0.9), _make(id="c4", type_=ConstraintType.LOCATION, weight=0.7), ] critical = [c for c in constraints if c.is_critical()] assert len(critical) == 2 assert {c.id for c in critical} == {"c2", "c3"} def test_sort_constraints_by_weight(self): constraints = [ _make(id="c1", weight=0.3), _make(id="c2", weight=0.9), _make(id="c3", weight=0.6), ] sorted_cs = sorted(constraints, key=lambda c: c.weight, reverse=True) assert [c.id for c in sorted_cs] == ["c2", "c3", "c1"] def test_to_search_terms_for_mixed_types(self): constraints = [ _make(type_=ConstraintType.PROPERTY, value="glacier"), _make(type_=ConstraintType.EVENT, value="landslide"), _make(type_=ConstraintType.TEMPORAL, value="2023"), ] terms = [c.to_search_terms() for c in constraints] assert terms[0] == "glacier" assert "accident incident" in terms[1] assert terms[2] == "2023"