Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

577 lines
19 KiB
Python

"""
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,
(
"<think>I need to think about this carefully.\n"
"Let me consider the constraints.</think>\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,
(
"</think>\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"