"""Tests for ``arborist.substrate.weights`` — WeightSet defaults + ``from_dict`` adapter (#000012 Phase 1a). Pure-stdlib dataclass + classmethod-style adapter; small surface. This pins: - documented defaults (lifted from the module docstring) - greek-letter and Python-safe key aliases ("lambda" ↔ "lambda_") - missing-keys fall-through to defaults - extra-keys silently ignored - string values coerce to float - frozen-dataclass invariant (mutation raises) """ from __future__ import annotations import dataclasses import pytest from arborist.substrate.weights import ( DEFAULT_WEIGHTS, WeightSet, from_dict, ) # --- defaults pinned to the docstring ------------------------------ def test_default_weights_match_documented_values(): """Each weight in the dataclass docstring is pinned. If a future PR changes a default, this test fires and forces an explicit update of both the docstring AND the test.""" assert DEFAULT_WEIGHTS.alpha == 1.0 assert DEFAULT_WEIGHTS.beta == 1.0 assert DEFAULT_WEIGHTS.gamma == 1.0 assert DEFAULT_WEIGHTS.delta == 0.5 assert DEFAULT_WEIGHTS.epsilon == 0.3 assert DEFAULT_WEIGHTS.zeta == 0.0 # validator-diversity off in single-validator assert DEFAULT_WEIGHTS.eta == 2.0 # regression > improvement weight assert DEFAULT_WEIGHTS.theta == 0.5 assert DEFAULT_WEIGHTS.iota == 0.0 # reserved assert DEFAULT_WEIGHTS.kappa == 0.0 # reserved assert DEFAULT_WEIGHTS.lambda_ == 0.5 def test_eta_is_heavier_than_alpha(): """Documented invariant from the docstring: 'η (RegressionPenalty) heavier than improvement weights so the scorer is biased toward refusing regressions.'""" assert DEFAULT_WEIGHTS.eta > DEFAULT_WEIGHTS.alpha assert DEFAULT_WEIGHTS.eta > DEFAULT_WEIGHTS.beta assert DEFAULT_WEIGHTS.eta > DEFAULT_WEIGHTS.gamma def test_three_batteries_equal_by_default(): """Documented invariant: 'α = β = γ (3 batteries weighted equally by default).'""" assert DEFAULT_WEIGHTS.alpha == DEFAULT_WEIGHTS.beta == DEFAULT_WEIGHTS.gamma def test_reserved_weights_are_zero_phase_1a(): """Phase 1a discipline: validator-diversity / security / complexity are zero by default. v8 multi-validator deployments must opt in.""" assert DEFAULT_WEIGHTS.zeta == 0.0 assert DEFAULT_WEIGHTS.iota == 0.0 assert DEFAULT_WEIGHTS.kappa == 0.0 # --- as_dict -------------------------------------------------------- def test_as_dict_returns_all_fields(): d = DEFAULT_WEIGHTS.as_dict() expected_keys = { "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda_", "delta_aggregator", # #000047 — categorical, not a numeric weight } assert set(d.keys()) == expected_keys assert d["delta_aggregator"] == "mean" def test_as_dict_uses_lambda_underscore_key(): """``lambda`` is a Python keyword → dataclass uses ``lambda_``; as_dict() carries it forward unchanged. Callers serializing to JSON / YAML rename to bare ``lambda`` if needed.""" d = DEFAULT_WEIGHTS.as_dict() assert "lambda_" in d assert "lambda" not in d # --- from_dict adapter --------------------------------------------- def test_from_dict_uses_greek_letter_keys(): ws = from_dict({"alpha": 0.7, "beta": 0.8, "gamma": 0.9}) assert ws.alpha == 0.7 assert ws.beta == 0.8 assert ws.gamma == 0.9 # Unspecified keys fall through to defaults. assert ws.delta == DEFAULT_WEIGHTS.delta def test_from_dict_translates_lambda_to_lambda_underscore(): """JSON/YAML configs use bare ``lambda`` (the Greek letter, not the keyword). from_dict translates to the Python-safe field name ``lambda_``.""" ws = from_dict({"lambda": 0.99}) assert ws.lambda_ == 0.99 # Bare 'lambda' did not survive into a hidden field. assert "lambda" not in ws.as_dict() def test_from_dict_lambda_underscore_key_also_accepted(): """For Python callers writing config dicts directly, ``lambda_`` is the natural key name; from_dict accepts it too.""" ws = from_dict({"lambda_": 0.42}) assert ws.lambda_ == 0.42 def test_from_dict_missing_keys_fall_through_to_defaults(): """Empty dict → identical to DEFAULT_WEIGHTS.""" ws = from_dict({}) assert ws == DEFAULT_WEIGHTS def test_from_dict_partial_override_preserves_other_defaults(): ws = from_dict({"alpha": 0.5}) assert ws.alpha == 0.5 # Every other field is unchanged. assert ws.beta == DEFAULT_WEIGHTS.beta assert ws.eta == DEFAULT_WEIGHTS.eta assert ws.lambda_ == DEFAULT_WEIGHTS.lambda_ def test_from_dict_ignores_unknown_keys(): """Defensive: extra keys (e.g. "mu" — never defined) are silently dropped rather than raising. Forward-compat with config files that include vendor-specific tags.""" ws = from_dict({"alpha": 0.6, "mu": 1.234, "extra_metadata": "ignore"}) assert ws.alpha == 0.6 # mu / extra_metadata silently dropped. assert not hasattr(ws, "mu") def test_from_dict_coerces_string_to_float(): """JSON/YAML may pass numeric weights as strings. from_dict coerces via float().""" ws = from_dict({"alpha": "0.7", "lambda": "0.3"}) assert ws.alpha == 0.7 assert ws.lambda_ == 0.3 def test_from_dict_int_input_becomes_float(): ws = from_dict({"alpha": 1}) assert ws.alpha == 1.0 assert isinstance(ws.alpha, float) # --- frozen invariant ---------------------------------------------- def test_weight_set_is_frozen(): """WeightSet is dataclass(frozen=True) — mutation raises FrozenInstanceError. Pinned so a future @dataclass change forces this test to be updated explicitly.""" with pytest.raises(dataclasses.FrozenInstanceError): DEFAULT_WEIGHTS.alpha = 99.0 def test_weight_sets_equal_by_value(): """Two WeightSet instances with identical fields compare equal — standard dataclass equality. Used by tests + audit-trail serialization.""" a = WeightSet(alpha=0.5, beta=0.6) b = WeightSet(alpha=0.5, beta=0.6) assert a == b # Different values → not equal. c = WeightSet(alpha=0.5, beta=0.7) assert a != c