"""Step 3 -> Step 4 architecture-plugin handoff safety. Step 4 must instantiate the SAME architecture that produced the checkpoint. The trained plugin id is recorded in the Step 3 manifest; Step 4 inherits it (or verifies an explicit override). These tests pin that contract so a forgotten ``--model-plugin`` on a new-SCTM run cannot silently/loudly load IO weights into the canonical skeleton. """ from __future__ import annotations import json import pytest from sctm_next_round.checkpoint_manifest import load_step3_checkpoint_context, reconcile_model_plugin_id def test_reconcile_inherits_trained_plugin_when_cli_default() -> None: assert reconcile_model_plugin_id("canonical", "SCTM-IO-OU") == "SCTM-IO-OU" assert reconcile_model_plugin_id(None, "SCTM-IO-OU") == "SCTM-IO-OU" def test_reconcile_keeps_canonical_and_trusts_cli_on_legacy_manifest() -> None: assert reconcile_model_plugin_id("canonical", "canonical") == "canonical" assert reconcile_model_plugin_id("canonical", None) == "canonical" assert reconcile_model_plugin_id("SCTM-IO", None) == "SCTM-IO" # legacy manifest w/o plugin -> trust CLI def test_reconcile_accepts_alias_and_case_variants_of_same_architecture() -> None: # same class via alias/case must NOT be treated as a conflict assert reconcile_model_plugin_id("sctm-io-ou", "SCTM-IO-OU") == "sctm-io-ou" assert reconcile_model_plugin_id("informative-observation-ou", "SCTM-IO-OU") == "informative-observation-ou" def test_reconcile_raises_on_genuine_architecture_conflict() -> None: with pytest.raises(ValueError): reconcile_model_plugin_id("SCTM-IO", "SCTM-IO-OU") # different classes with pytest.raises(ValueError): reconcile_model_plugin_id("canonical-with-explicit", "SCTM-IO-OU") if False else None reconcile_model_plugin_id("sctm", "SCTM-IO-OU") # canonical explicitly named vs IO checkpoint def test_step3_manifest_plugin_id_round_trips_into_context(tmp_path) -> None: step3 = tmp_path / "step3" step3.mkdir() (step3 / "selected.pt").write_bytes(b"\x00") # checkpoint file must exist for resolver (step3 / "selected_checkpoint_manifest.json").write_text( json.dumps({"selected_step": 8500, "selected_checkpoint_sha256": "deadbeef", "model_plugin_id": "SCTM-IO-OU"}), encoding="utf-8", ) ctx = load_step3_checkpoint_context(step3) assert ctx.model_plugin_id == "SCTM-IO-OU" assert ctx.selected_step == 8500 # the full Step-4 handoff: a default CLI inherits the trained plugin assert reconcile_model_plugin_id("canonical", ctx.model_plugin_id) == "SCTM-IO-OU"