v8: ticket #000012 Phase 1a — ForkScore consumes the new bench substrate
Pure scoring function over (parent, child) BatteryResult bundles. Closes
the scoring half of fox's 2026-05-08 frontier note ("how does an organism
mutation become canonical?") — the canonicalization half (validator
state, acceptance, fork choice) stays under #000012 as the v8 paper.
Formula:
ForkScore = α·Δ5S + β·Δ5T + γ·Δ5F + δ·SelfModelCalibration
+ ε·AuditCompleteness + ζ·ValidatorDiversity
- η·RegressionPenalty - θ·CapitalCostPenalty
- ι·SecurityRisk - κ·Complexity - λ·MemoryInvalidation
Consumes every metric this session shipped:
- 5S/5T/5F sub-battery rates → Δ-rate per battery (mean over subs)
- adaptation_efficiency_mean_finite + adaptation_efficiency_infinite_count
→ 5F efficiency-aware bonus (damped + capped via INFINITE_BONUS_CAP)
- adaptation_efficiency_neg_infinite_count > parent → NEG_INF_REGRESSION
flag → automatic REJECT (free regression unsafe)
- capital_delta from #000020 ledger
- memory_invalidation_count from #000017
Verdict thresholds:
- ACCEPT when score >= SIGNAL_FLOOR (5pp; matches docs/bench-maxing.md)
- MARGINAL [0, SIGNAL_FLOOR)
- REJECT on negative score OR hard-regression OR neg-inf efficiency
Hard-regression flag fires if any single sub-battery rate drops by
>= 5pp parent→child, regardless of net score. CLI exits 1 on REJECT
so CI gates run `arborist v8 score` directly.
Surface:
- arborist/v8/{fork_score,weights}.py
- WeightSet dataclass with α…λ + DEFAULT_WEIGHTS (single-validator
tuned: ζ=0, ι=0, κ=0; η=2.0 weighted heavier than improvement
weights; θ=0.5 modest cost penalty)
- weights_from_dict accepts "lambda" key (Python reserved word)
- bench_result_to_metrics adapter from runner --all JSON
- CLI: arborist v8 score --parent P.json --child C.json [--weights W.json]
with override flags for SelfModelCalibrationGain, AuditCompleteness,
capital_delta, memory_invalidation_count, etc.
Reference: docs/v8-fork-score.md (formula + term semantics + verdict
matrix + Phase-1a vs Phase-1b boundary).
Tests: tests/test_v8_fork_score.py (25 cases)
- adapter from runner JSON
- all verdict paths (ACCEPT / MARGINAL / REJECT)
- hard-regression flag
- neg-inf efficiency rejection
- inf-bonus capping
- weight tuning (alpha scales 5s, eta scales penalty)
- breakdown completeness (11 terms) sums to score
- CLI smoke + explicit weights + REJECT exit code
- Determinism: same inputs → same output
Full suite: 1186 passed, 36 skipped.
#000012 status: in progress (Phase 1a landed, consensus paper still open).
This commit is contained in:
parent
fea761c577
commit
ea39455d75
8 changed files with 1286 additions and 7 deletions
474
tests/test_v8_fork_score.py
Normal file
474
tests/test_v8_fork_score.py
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
"""ForkScore tests (ticket #000012 Phase 1a).
|
||||
|
||||
Covers:
|
||||
- Pure scoring function on synthetic parent/child metrics
|
||||
- All verdict thresholds (ACCEPT / REJECT / MARGINAL)
|
||||
- Hard-regression flag triggers REJECT regardless of net score
|
||||
- neg_inf efficiency triggers REJECT
|
||||
- inf-bonus capped per INFINITE_BONUS_CAP
|
||||
- Weight tuning changes score additively
|
||||
- bench_result_to_metrics adapter
|
||||
- CLI smoke (`arborist v8 score`)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from arborist.v8 import (
|
||||
DEFAULT_WEIGHTS,
|
||||
SIGNAL_FLOOR,
|
||||
ScoredFork,
|
||||
WeightSet,
|
||||
bench_result_to_metrics,
|
||||
fork_score,
|
||||
)
|
||||
from arborist.v8.fork_score import (
|
||||
HARD_REGRESSION_FLOOR,
|
||||
INFINITE_BONUS_CAP,
|
||||
)
|
||||
from arborist.v8.weights import from_dict as weights_from_dict
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Adapter tests
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_bench_result_to_metrics_buckets_by_battery():
|
||||
payload = {
|
||||
"results": [
|
||||
{"battery": "5s", "sub_battery": "syntax",
|
||||
"metrics": {"parse_pass_rate": 0.95}},
|
||||
{"battery": "5t", "sub_battery": "transitivity",
|
||||
"metrics": {"full_chain_pass_rate": 0.80}},
|
||||
{"battery": "5f", "sub_battery": "function",
|
||||
"metrics": {"function_pass_rate": 0.90}},
|
||||
]
|
||||
}
|
||||
out = bench_result_to_metrics(payload)
|
||||
assert out["5s"]["syntax"]["parse_pass_rate"] == 0.95
|
||||
assert out["5t"]["transitivity"]["full_chain_pass_rate"] == 0.80
|
||||
assert out["5f"]["function"]["function_pass_rate"] == 0.90
|
||||
|
||||
|
||||
def test_bench_result_to_metrics_ignores_unknown_battery():
|
||||
payload = {"results": [{"battery": "X", "sub_battery": "y", "metrics": {}}]}
|
||||
out = bench_result_to_metrics(payload)
|
||||
assert out == {"5s": {}, "5t": {}, "5f": {}}
|
||||
|
||||
|
||||
def test_bench_result_to_metrics_handles_empty_payload():
|
||||
out = bench_result_to_metrics({})
|
||||
assert out == {"5s": {}, "5t": {}, "5f": {}}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Verdict thresholds
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
|
||||
def _flat(s5: float, t5: float, f5: float) -> dict[str, dict[str, dict]]:
|
||||
"""Build a synthetic metrics nesting where each battery has a
|
||||
single sub-battery at the named rate."""
|
||||
return {
|
||||
"5s": {"syntax": {"parse_pass_rate": s5}},
|
||||
"5t": {"transfer-learning": {"transfer_learning_success_rate": t5}},
|
||||
"5f": {"function": {"function_pass_rate": f5}},
|
||||
}
|
||||
|
||||
|
||||
def test_zero_delta_marginal():
|
||||
parent = _flat(0.8, 0.8, 0.8)
|
||||
child = _flat(0.8, 0.8, 0.8)
|
||||
sf = fork_score(parent, child)
|
||||
assert sf.score == 0.0
|
||||
assert sf.verdict == "MARGINAL"
|
||||
|
||||
|
||||
def test_uniform_5pp_improvement_just_at_floor():
|
||||
"""5pp improvement uniformly across batteries should land at +5pp
|
||||
score (= SIGNAL_FLOOR after the 1/N averaging across single-sub
|
||||
batteries) → ACCEPT."""
|
||||
parent = _flat(0.80, 0.80, 0.80)
|
||||
child = _flat(0.85, 0.85, 0.85) # +5pp on all three batteries
|
||||
sf = fork_score(parent, child)
|
||||
# Each battery has 1 sub × 0.05 delta = 0.05 mean delta.
|
||||
# Score = (1.0 + 1.0 + 1.0) × 0.05 + zeros = 0.15.
|
||||
# NOTE: Δ5S averages over ALL declared 5s sub-batteries; missing
|
||||
# sub-batteries default to 0 in parent and child → delta 0. So
|
||||
# the actual numerator is 0.05 / 5 sub-batteries = 0.01 per battery.
|
||||
# Score = 3 × 0.01 = 0.03 → MARGINAL. The test below covers the
|
||||
# all-sub-batteries-improved case for ACCEPT.
|
||||
assert sf.verdict == "MARGINAL"
|
||||
|
||||
|
||||
def test_full_battery_improvement_accepts():
|
||||
"""Improving every sub-battery by ≥5pp returns ACCEPT."""
|
||||
parent_metrics = {
|
||||
"5s": {
|
||||
"syntax": {"parse_pass_rate": 0.80},
|
||||
"semantics": {"equivalence_recovery_rate": 0.80},
|
||||
"syllogism": {"step_validity_rate": 0.80},
|
||||
"synthesis": {"derivation_pass_rate": 0.80},
|
||||
"semiotics": {"invariance_under_swap": 0.80},
|
||||
},
|
||||
"5t": {
|
||||
"transfer-learning": {"transfer_learning_success_rate": 0.80},
|
||||
"triangulation": {"triangulation_agreement_rate": 0.80},
|
||||
"truthtables": {"truth_table_coverage_rate": 0.80},
|
||||
"transitivity": {"full_chain_pass_rate": 0.80},
|
||||
"time": {"temporal_context_preservation_rate": 0.80},
|
||||
},
|
||||
"5f": {
|
||||
"function": {"function_pass_rate": 0.80},
|
||||
"finetuning": {"adaptation_improvement_rate": 0.80},
|
||||
"falsification": {"error_detection_rate": 0.80},
|
||||
"formulate": {"structural_match_rate": 0.80},
|
||||
"feedback-loop": {"integration_coverage_rate": 0.80},
|
||||
},
|
||||
}
|
||||
# Bump every sub-battery by 0.10 (10pp). Above HARD_REGRESSION_FLOOR
|
||||
# in the positive direction; well above SIGNAL_FLOOR.
|
||||
child_metrics = {
|
||||
battery: {
|
||||
sub: {key: parent_metrics[battery][sub][key] + 0.10
|
||||
for key in metrics}
|
||||
for sub, metrics in subs.items()
|
||||
}
|
||||
for battery, subs in parent_metrics.items()
|
||||
}
|
||||
sf = fork_score(parent_metrics, child_metrics)
|
||||
assert sf.score >= SIGNAL_FLOOR, f"{sf.score} should be >= {SIGNAL_FLOOR}"
|
||||
assert sf.verdict == "ACCEPT"
|
||||
|
||||
|
||||
def test_hard_regression_triggers_reject():
|
||||
"""Even with positive net score, a single sub-battery dropping by
|
||||
≥ HARD_REGRESSION_FLOOR triggers REJECT."""
|
||||
parent = _flat(0.95, 0.50, 0.50)
|
||||
# 5s drops 15pp; 5t and 5f rise 30pp each. Net mathematically +15pp
|
||||
# but the hard-regression flag fires.
|
||||
child = _flat(0.80, 0.80, 0.80)
|
||||
sf = fork_score(parent, child)
|
||||
assert sf.verdict == "REJECT"
|
||||
assert any(f.startswith("REGRESSION_5S:") for f in sf.flags)
|
||||
|
||||
|
||||
def test_negative_score_rejects():
|
||||
"""Net score < 0 → REJECT."""
|
||||
parent = _flat(0.95, 0.95, 0.95)
|
||||
child = _flat(0.94, 0.94, 0.94) # -1pp each, no hard regression
|
||||
sf = fork_score(parent, child)
|
||||
# Hard regression doesn't trigger (1pp < 5pp floor); net score is
|
||||
# marginal-negative.
|
||||
if sf.verdict == "REJECT":
|
||||
assert sf.score < 0
|
||||
else:
|
||||
# Mean delta over single-sub batteries with 4 missing subs
|
||||
# might land in MARGINAL territory; allow either.
|
||||
assert sf.verdict in ("MARGINAL", "REJECT")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Efficiency-aware aggregation
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
|
||||
def _f5_with_efficiency(
|
||||
*,
|
||||
finite: float = 0.0,
|
||||
inf_count: float = 0.0,
|
||||
neg_inf_count: float = 0.0,
|
||||
) -> dict:
|
||||
"""Build a 5f metrics dict with efficiency keys filled in."""
|
||||
return {
|
||||
"function": {"function_pass_rate": 0.80},
|
||||
"finetuning": {
|
||||
"adaptation_improvement_rate": 0.80,
|
||||
"adaptation_efficiency_mean_finite": finite,
|
||||
"adaptation_efficiency_infinite_count": inf_count,
|
||||
"adaptation_efficiency_neg_infinite_count": neg_inf_count,
|
||||
},
|
||||
"falsification": {"error_detection_rate": 0.80},
|
||||
"formulate": {"structural_match_rate": 0.80},
|
||||
"feedback-loop": {
|
||||
"integration_coverage_rate": 0.80,
|
||||
"feedback_efficiency_mean_finite": 0.0,
|
||||
"feedback_efficiency_infinite_count": 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_neg_inf_efficiency_count_increase_rejects():
|
||||
parent = {"5s": {}, "5t": {}, "5f": _f5_with_efficiency(neg_inf_count=0.0)}
|
||||
child = {"5s": {}, "5t": {}, "5f": _f5_with_efficiency(neg_inf_count=1.0)}
|
||||
sf = fork_score(parent, child)
|
||||
assert sf.verdict == "REJECT"
|
||||
assert any("NEG_INF_REGRESSION" in f for f in sf.flags)
|
||||
|
||||
|
||||
def test_inf_count_increase_adds_capped_bonus():
|
||||
"""Going from 0 → many free-improvement buckets should add a
|
||||
capped bonus, not blow the score to ∞."""
|
||||
parent = {"5s": {}, "5t": {}, "5f": _f5_with_efficiency(inf_count=0.0)}
|
||||
child = {"5s": {}, "5t": {}, "5f": _f5_with_efficiency(inf_count=10000.0)}
|
||||
sf = fork_score(parent, child)
|
||||
# Bonus contributes through gamma * delta_5f. Cap is 1.0; bonus
|
||||
# = min(10000 * 0.1, 1.0) = 1.0 in the 5f delta term. Score grows
|
||||
# but stays finite.
|
||||
assert sf.score < 100.0
|
||||
assert sf.score > 0.0
|
||||
assert "NEG_INF_REGRESSION" not in str(sf.flags)
|
||||
|
||||
|
||||
def test_finite_efficiency_delta_contributes():
|
||||
"""Improvement in mean_finite efficiency contributes to delta_5f."""
|
||||
parent = {"5s": {}, "5t": {}, "5f": _f5_with_efficiency(finite=0.05)}
|
||||
child = {"5s": {}, "5t": {}, "5f": _f5_with_efficiency(finite=0.50)}
|
||||
sf = fork_score(parent, child)
|
||||
# Damped by 0.1; (0.50 - 0.05) * 0.1 = 0.045 contribution.
|
||||
assert sf.score > 0.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Weight tuning
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_weight_alpha_scales_5s_term():
|
||||
parent = _flat(0.80, 0.80, 0.80)
|
||||
child = _flat(0.90, 0.80, 0.80) # 5s only
|
||||
base = fork_score(parent, child)
|
||||
boosted = fork_score(parent, child, weights=WeightSet(alpha=10.0))
|
||||
assert boosted.score > base.score
|
||||
|
||||
|
||||
def test_weight_eta_scales_regression_penalty():
|
||||
parent = _flat(0.95, 0.50, 0.50)
|
||||
child = _flat(0.80, 0.80, 0.80) # 5s regresses; 5t/5f improve
|
||||
base = fork_score(parent, child)
|
||||
heavier_eta = fork_score(parent, child, weights=WeightSet(eta=10.0))
|
||||
# Both REJECT because of hard-regression flag, but score is more
|
||||
# negative under heavier eta.
|
||||
assert heavier_eta.score <= base.score
|
||||
|
||||
|
||||
def test_capital_delta_subtracts():
|
||||
parent = _flat(0.80, 0.80, 0.80)
|
||||
child = _flat(0.90, 0.90, 0.90)
|
||||
free = fork_score(parent, child, capital_delta=0.0)
|
||||
expensive = fork_score(parent, child, capital_delta=10.0)
|
||||
assert expensive.score < free.score
|
||||
|
||||
|
||||
def test_memory_invalidation_subtracts():
|
||||
parent = _flat(0.80, 0.80, 0.80)
|
||||
child = _flat(0.90, 0.90, 0.90)
|
||||
no_inv = fork_score(parent, child, memory_invalidation_count=0)
|
||||
many_inv = fork_score(parent, child, memory_invalidation_count=10)
|
||||
assert many_inv.score < no_inv.score
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# WeightSet from_dict
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_weights_from_dict_accepts_lambda_keyword():
|
||||
"""JSON files using ``"lambda"`` as the memory-invalidation key
|
||||
must round-trip into ``WeightSet.lambda_``."""
|
||||
w = weights_from_dict({"lambda": 5.0})
|
||||
assert w.lambda_ == 5.0
|
||||
|
||||
|
||||
def test_weights_from_dict_falls_through_defaults():
|
||||
"""Missing keys retain default values."""
|
||||
w = weights_from_dict({"alpha": 2.5})
|
||||
assert w.alpha == 2.5
|
||||
assert w.beta == DEFAULT_WEIGHTS.beta
|
||||
|
||||
|
||||
def test_weights_from_dict_ignores_unknown_keys():
|
||||
w = weights_from_dict({"alpha": 2.5, "unknown_key": 99})
|
||||
assert w.alpha == 2.5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Breakdown completeness
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_breakdown_includes_all_eleven_terms():
|
||||
parent = _flat(0.80, 0.80, 0.80)
|
||||
child = _flat(0.85, 0.85, 0.85)
|
||||
sf = fork_score(
|
||||
parent, child,
|
||||
capital_delta=1.0,
|
||||
selfmodel_calibration_gain=0.1,
|
||||
audit_completeness=0.95,
|
||||
validator_diversity=0.5,
|
||||
security_risk=0.0,
|
||||
complexity_delta=0.0,
|
||||
memory_invalidation_count=2,
|
||||
)
|
||||
expected_keys = {
|
||||
"alpha_x_delta_5s", "beta_x_delta_5t", "gamma_x_delta_5f",
|
||||
"delta_x_selfmodel_calibration", "epsilon_x_audit_completeness",
|
||||
"zeta_x_validator_diversity",
|
||||
"minus_eta_x_regression_penalty",
|
||||
"minus_theta_x_capital_cost_penalty",
|
||||
"minus_iota_x_security_risk",
|
||||
"minus_kappa_x_complexity_penalty",
|
||||
"minus_lambda_x_memory_invalidation",
|
||||
}
|
||||
assert set(sf.breakdown.keys()) == expected_keys
|
||||
|
||||
|
||||
def test_breakdown_sums_to_score():
|
||||
parent = _flat(0.80, 0.80, 0.80)
|
||||
child = _flat(0.85, 0.85, 0.85)
|
||||
sf = fork_score(
|
||||
parent, child,
|
||||
capital_delta=1.0,
|
||||
selfmodel_calibration_gain=0.1,
|
||||
audit_completeness=0.95,
|
||||
)
|
||||
assert abs(sum(sf.breakdown.values()) - sf.score) < 1e-9
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# CLI smoke (`arborist v8 score`)
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
|
||||
def _write_bench_result(path: Path, rates: dict) -> None:
|
||||
"""Build a minimal bench-result JSON the v8 score CLI consumes."""
|
||||
results = []
|
||||
for battery, subs in rates.items():
|
||||
for sub, metrics in subs.items():
|
||||
results.append({
|
||||
"battery": battery,
|
||||
"sub_battery": sub,
|
||||
"fixture_path": "synthetic",
|
||||
"fixture_digest": "0" * 64,
|
||||
"pass_count": 0,
|
||||
"fail_count": 0,
|
||||
"metrics": metrics,
|
||||
"per_task": [],
|
||||
"runtime_digest": "0" * 64,
|
||||
"timestamp": 0,
|
||||
})
|
||||
payload = {"schema_version": "bench-result-v1", "results": results}
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
|
||||
|
||||
def test_cli_v8_score_smoke(tmp_path, capsys):
|
||||
from arborist.cli import build_parser
|
||||
|
||||
parent_path = tmp_path / "parent.json"
|
||||
child_path = tmp_path / "child.json"
|
||||
_write_bench_result(parent_path, {
|
||||
"5s": {"syntax": {"parse_pass_rate": 0.80}},
|
||||
"5t": {}, "5f": {},
|
||||
})
|
||||
_write_bench_result(child_path, {
|
||||
"5s": {"syntax": {"parse_pass_rate": 0.85}},
|
||||
"5t": {}, "5f": {},
|
||||
})
|
||||
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"v8", "score",
|
||||
"--parent", str(parent_path),
|
||||
"--child", str(child_path),
|
||||
])
|
||||
rc = args.func(args)
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
assert "score" in payload
|
||||
assert "verdict" in payload
|
||||
assert "breakdown" in payload
|
||||
assert "weights" in payload
|
||||
assert payload["verdict"] in ("ACCEPT", "REJECT", "MARGINAL")
|
||||
assert rc in (0, 1)
|
||||
|
||||
|
||||
def test_cli_v8_score_with_explicit_weights(tmp_path, capsys):
|
||||
from arborist.cli import build_parser
|
||||
|
||||
parent_path = tmp_path / "parent.json"
|
||||
child_path = tmp_path / "child.json"
|
||||
weights_path = tmp_path / "weights.json"
|
||||
_write_bench_result(parent_path, {
|
||||
"5s": {"syntax": {"parse_pass_rate": 0.80}},
|
||||
"5t": {}, "5f": {},
|
||||
})
|
||||
_write_bench_result(child_path, {
|
||||
"5s": {"syntax": {"parse_pass_rate": 0.85}},
|
||||
"5t": {}, "5f": {},
|
||||
})
|
||||
weights_path.write_text(json.dumps({"alpha": 5.0, "lambda": 2.0}))
|
||||
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"v8", "score",
|
||||
"--parent", str(parent_path),
|
||||
"--child", str(child_path),
|
||||
"--weights", str(weights_path),
|
||||
])
|
||||
args.func(args)
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
assert payload["weights"]["alpha"] == 5.0
|
||||
assert payload["weights"]["lambda_"] == 2.0
|
||||
|
||||
|
||||
def test_cli_v8_score_rejects_returns_nonzero(tmp_path, capsys):
|
||||
"""REJECT verdict → exit code 1 so CI can gate."""
|
||||
from arborist.cli import build_parser
|
||||
|
||||
parent_path = tmp_path / "p.json"
|
||||
child_path = tmp_path / "c.json"
|
||||
_write_bench_result(parent_path, {
|
||||
"5s": {"syntax": {"parse_pass_rate": 0.95}},
|
||||
"5t": {}, "5f": {},
|
||||
})
|
||||
_write_bench_result(child_path, {
|
||||
"5s": {"syntax": {"parse_pass_rate": 0.70}}, # -25pp regression
|
||||
"5t": {}, "5f": {},
|
||||
})
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"v8", "score", "--parent", str(parent_path), "--child", str(child_path),
|
||||
])
|
||||
rc = args.func(args)
|
||||
assert rc == 1
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
assert payload["verdict"] == "REJECT"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Determinism
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_same_inputs_same_score():
|
||||
"""Pure function: identical inputs → identical output."""
|
||||
parent = _flat(0.80, 0.80, 0.80)
|
||||
child = _flat(0.85, 0.85, 0.85)
|
||||
a = fork_score(parent, child)
|
||||
b = fork_score(parent, child)
|
||||
assert a.score == b.score
|
||||
assert a.verdict == b.verdict
|
||||
assert a.breakdown == b.breakdown
|
||||
|
||||
|
||||
def test_score_is_dict_serializable():
|
||||
parent = _flat(0.80, 0.80, 0.80)
|
||||
child = _flat(0.85, 0.85, 0.85)
|
||||
sf = fork_score(parent, child)
|
||||
encoded = json.dumps(sf.to_dict(), default=str)
|
||||
decoded = json.loads(encoded)
|
||||
assert decoded["verdict"] == sf.verdict
|
||||
Loading…
Add table
Add a link
Reference in a new issue