arborist/substrate/weights.py was zero-coverage. 73 LOC of
dataclass + adapter. Tests pin:
- documented defaults from the module docstring (each weight
has a comment block explaining its design intent; tests
pin the values so a PR that flips alpha=1.0 → 0.5 fires the
test and forces an explicit docstring update)
- documented invariants: alpha=beta=gamma (3 batteries equal),
eta > alpha (regression heavier than improvement),
reserved-zero defaults (zeta/iota/kappa)
- as_dict() returns all 11 fields; lambda_ key (not "lambda" —
keyword)
- from_dict() greek-letter keys, "lambda" → "lambda_" translation
(JSON/YAML friendly), missing-keys-default fall-through,
extra-keys silent drop, str → float coercion, int → float
coercion
- frozen-dataclass invariant (mutation raises FrozenInstanceError)
- dataclass equality
Full suite: 1881 passed, 54 skipped. tests/ count growing
roughly 1655 → 1881 (+226) across today's autonomous quality
session.
180 lines
6 KiB
Python
180 lines
6 KiB
Python
"""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_eleven_fields():
|
||
d = DEFAULT_WEIGHTS.as_dict()
|
||
expected_keys = {
|
||
"alpha", "beta", "gamma", "delta", "epsilon", "zeta",
|
||
"eta", "theta", "iota", "kappa", "lambda_",
|
||
}
|
||
assert set(d.keys()) == expected_keys
|
||
|
||
|
||
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
|