| """CPU-only expected-red contract for autonomous Flow-encoder rounds. |
| |
| Expected public API (specified before implementation):: |
| |
| from music3lab.autonomous import ( |
| AutonomousController, Candidate, EvaluationSuite, PromotionPolicy, |
| Registry, |
| ) |
| |
| This contract deliberately covers only continuous Flow-encoder waveform |
| reconstruction. It neither asserts nor implies Music 3 token extraction, |
| composition, style transfer, or native-token generation. |
| """ |
| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| import pytest |
|
|
| from music3lab.autonomous import ( |
| AutonomousController, |
| Candidate, |
| EvaluationSuite, |
| PromotionPolicy, |
| Registry, |
| ) |
|
|
|
|
| USER22 = tuple(f"user22-{index:02d}" for index in range(22)) |
|
|
|
|
| def _suite() -> EvaluationSuite: |
| return EvaluationSuite.freeze( |
| train=tuple(f"train-{index:02d}" for index in range(8)), |
| validation=tuple(f"validation-{index:02d}" for index in range(6)), |
| public=tuple(f"public-{index:02d}" for index in range(6)), |
| sealed=tuple(f"sealed-{index:02d}" for index in range(6)), |
| excluded_sources=USER22, |
| ) |
|
|
|
|
| def _controller(tmp_path: Path) -> tuple[AutonomousController, Registry]: |
| registry = Registry(tmp_path / "registry") |
| registry.register_champion( |
| Candidate(name="base", artifact_sha256="a" * 64, kind="flow_encoder") |
| ) |
| policy = PromotionPolicy( |
| bootstrap_samples=200, |
| seed=41, |
| protected_regression_limit=0.0, |
| teacher_regression_limit=0.05, |
| ) |
| return AutonomousController(registry=registry, policy=policy), registry |
|
|
|
|
| def _metrics(*, train: float, validation: float, public: float, sealed: float, teacher: float = 1.0, technical: float = 1.0, protected: float = 1.0) -> dict[str, tuple[float, ...]]: |
| return { |
| "train": (train,) * 8, |
| "validation": (validation,) * 6, |
| "public": (public,) * 6, |
| "sealed": (sealed,) * 6, |
| "teacher": (teacher,) * 6, |
| "technical": (technical,) * 6, |
| "protected": (protected,) * 6, |
| } |
|
|
|
|
| def test_frozen_source_exclusive_suite_excludes_user22_and_is_immutable() -> None: |
| suite = _suite() |
|
|
| assert suite.splits == ("train", "validation", "public", "sealed") |
| assert not set(USER22) & set().union(*suite.source_ids_by_split.values()) |
| assert len(set().union(*suite.source_ids_by_split.values())) == 26 |
| with pytest.raises((AttributeError, TypeError, ValueError)): |
| suite.source_ids_by_split["train"] += ("leak",) |
|
|
|
|
| def test_challenger_selection_uses_validation_then_independent_public_and_sealed_metrics(tmp_path: Path) -> None: |
| controller, registry = _controller(tmp_path) |
| suite = _suite() |
| baseline = _metrics(train=1.0, validation=1.0, public=1.0, sealed=1.0) |
| winner = Candidate(name="real-latent-calibration", artifact_sha256="b" * 64, kind="flow_encoder") |
| weaker = Candidate(name="weaker-validation", artifact_sha256="c" * 64, kind="flow_encoder") |
|
|
| report = controller.run_round( |
| suite=suite, |
| baseline=baseline, |
| challengers={ |
| winner: _metrics(train=0.80, validation=0.60, public=0.70, sealed=0.70), |
| weaker: _metrics(train=0.10, validation=0.70, public=0.01, sealed=0.01), |
| }, |
| ) |
|
|
| assert report.validation_winner == winner.artifact_sha256 |
| assert report.decision == "PROMOTE" |
| assert registry.champion().artifact_sha256 == winner.artifact_sha256 |
| assert report.selection_split == "validation" |
| assert report.independent_splits == ("public", "sealed") |
|
|
|
|
| def test_zero_latent_challenger_is_automatically_rejected(tmp_path: Path) -> None: |
| controller, registry = _controller(tmp_path) |
| baseline_hash = registry.champion().artifact_sha256 |
| zero = Candidate( |
| name="zero-latent", |
| artifact_sha256="d" * 64, |
| kind="flow_encoder", |
| metadata={"latent_strategy": "zero"}, |
| ) |
|
|
| report = controller.run_round( |
| suite=_suite(), |
| baseline=_metrics(train=1.0, validation=1.0, public=1.0, sealed=1.0), |
| challengers={zero: _metrics(train=1.2, validation=1.4, public=1.5, sealed=1.5)}, |
| ) |
|
|
| assert report.decision == "REJECT" |
| assert "zero-latent" in report.rejected[zero.artifact_sha256].reasons |
| assert registry.champion().artifact_sha256 == baseline_hash |
|
|
|
|
| def test_train_lookup_overfit_rolls_back_without_champion_pointer_or_hash_change(tmp_path: Path) -> None: |
| controller, registry = _controller(tmp_path) |
| baseline = registry.champion() |
| lookup = Candidate(name="train-lookup", artifact_sha256="e" * 64, kind="flow_encoder") |
|
|
| report = controller.run_round( |
| suite=_suite(), |
| baseline=_metrics(train=1.0, validation=1.0, public=1.0, sealed=1.0), |
| challengers={lookup: _metrics(train=0.0, validation=0.60, public=1.20, sealed=1.30)}, |
| ) |
|
|
| assert report.decision == "ROLLBACK" |
| assert report.rejected[lookup.artifact_sha256].rollback is True |
| assert registry.champion().name == baseline.name |
| assert registry.champion().artifact_sha256 == baseline.artifact_sha256 |
|
|
|
|
| def test_promotion_requires_positive_bootstrap_ci_and_all_non_regression_gates(tmp_path: Path) -> None: |
| controller, registry = _controller(tmp_path) |
| candidate = Candidate(name="candidate", artifact_sha256="f" * 64, kind="flow_encoder") |
| baseline = _metrics(train=1.0, validation=1.0, public=1.0, sealed=1.0) |
|
|
| rejected = controller.run_round( |
| suite=_suite(), baseline=baseline, |
| challengers={candidate: _metrics(train=0.7, validation=0.7, public=0.7, sealed=0.7, teacher=1.2)}, |
| ) |
| assert rejected.decision == "REJECT" |
| assert "teacher" in rejected.rejected[candidate.artifact_sha256].reasons |
|
|
| accepted = controller.run_round( |
| suite=_suite(), baseline=baseline, |
| challengers={candidate: _metrics(train=0.7, validation=0.7, public=0.7, sealed=0.7, teacher=1.01, technical=0.99, protected=1.0)}, |
| ) |
| assert accepted.decision == "PROMOTE" |
| assert all(interval[0] > 0 for interval in accepted.bootstrap_ci.values()) |
| assert registry.champion().artifact_sha256 == candidate.artifact_sha256 |
|
|
|
|
| def test_registry_contains_complete_base_champion_challenger_and_rejected_audit_rows(tmp_path: Path) -> None: |
| controller, registry = _controller(tmp_path) |
| good = Candidate(name="good", artifact_sha256="1" * 64, kind="flow_encoder") |
| bad = Candidate(name="bad", artifact_sha256="2" * 64, kind="flow_encoder") |
| report = controller.run_round( |
| suite=_suite(), baseline=_metrics(train=1.0, validation=1.0, public=1.0, sealed=1.0), |
| challengers={ |
| good: _metrics(train=0.7, validation=0.7, public=0.7, sealed=0.7), |
| bad: _metrics(train=0.1, validation=0.8, public=1.2, sealed=1.2), |
| }, |
| ) |
|
|
| rows = registry.audit_rows(round_id=report.round_id) |
| assert {row.role for row in rows} == {"base", "champion", "challenger", "rejected"} |
| assert {row.artifact_sha256 for row in rows} >= {"a" * 64, good.artifact_sha256, bad.artifact_sha256} |
| assert all(row.suite_sha256 == _suite().sha256 for row in rows) |
|
|