import copy import json import pytest import torch from mcss.dynamic.policy import ( FEATURE_NAMES, FEATURE_SCHEMA_VERSION, POLICY_FEATURE_DIM, LearnedActionPolicy, ) from mcss.dynamic.policy_checkpoint import load_policy_checkpoint from mcss.training.policy_fit import ( ACTION_TEACHER_SCHEMA, fit_action_policy, load_teacher_dataset, main, parse_teacher_row, split_scene_ids, ) _BINDING = { "model_content_hash": "carrier-abc", "feature_schema_version": FEATURE_SCHEMA_VERSION, "render_protocol": "render-v1", "budget_protocol": "budget-v1", "utility_protocol": "utility-v1", "teacher_dataset_hash": "dataset-abc", "teacher_source_hashes": {"teacher": "hash-abc"}, } def _row(row_id: str, scene_id: str, value: float, *, binding=None) -> dict: features = [0.0] * POLICY_FEATURE_DIM features[0] = value features[1] = value * 0.5 return { "schema_version": ACTION_TEACHER_SCHEMA, "row_id": row_id, "episode_id": f"episode-{scene_id}", "scene_id": scene_id, "split_id": "train", "prefix_step": 0, "prefix_frame_id": 1, "control_input": {}, "features": features, "actions": ["OFF", "FUSE", "COMPLETE", "ALL"], "feasible_mask": [True, True, False, True], "losses": [1.0 - value, 1.0 + value, None, 1.0 - 0.5 * value], "advantages": [value, -value, None, 0.5 * value], "costs": [0.0, 2.0, None, 4.0], "horizon_used": 2, "provenance": dict(_BINDING if binding is None else binding), } def test_scene_split_is_deterministic_and_has_no_row_scene_leak() -> None: scenes = ["scene-a", "scene-b", "scene-c", "scene-d", "scene-e"] train_one, valid_one = split_scene_ids(scenes, holdout_fraction=0.4, seed=17) train_two, valid_two = split_scene_ids(scenes, holdout_fraction=0.4, seed=17) assert (train_one, valid_one) == (train_two, valid_two) assert set(train_one).isdisjoint(valid_one) assert set(train_one) | set(valid_one) == set(scenes) def test_masked_fit_is_finite_and_learns_continuous_advantages() -> None: rows = [_row(f"row-{index}", "scene-a", -1.0 + index * 0.25) for index in range(9)] initial = LearnedActionPolicy(hidden_dim=8, seed=19) initial_state = {name: value.detach().clone() for name, value in initial.state_dict().items()} result = fit_action_policy( rows, hidden_dim=8, epochs=80, learning_rate=0.02, holdout_fraction=0.0, seed=19, init_policy=initial, ) assert torch.isfinite(result.policy.feature_mean).all() assert torch.isfinite(result.policy.feature_scale).all() assert torch.isfinite(torch.tensor(float(result.metrics["training_loss"]))) assert result.metrics["heldout_row_count"] == 0 changed = [ not torch.equal(initial_state[name], value.detach().cpu()) for name, value in result.policy.state_dict().items() if name.startswith("model.") ] assert any(changed) # The masked COMPLETE label is null and cannot create a nonfinite loss. assert result.target_scale >= 1e-6 def test_feature_normalization_uses_training_scenes_once() -> None: train_rows = [_row("train-a", "scene-train", 2.0), _row("train-b", "scene-train", 4.0)] heldout = _row("valid-a", "scene-valid", 100.0) result = fit_action_policy( [*train_rows, heldout], hidden_dim=6, epochs=1, holdout_fraction=0.0, validation_scene_ids=["scene-valid"], seed=3, ) assert result.validation_scene_ids == ("scene-valid",) assert result.feature_mean[0].item() == pytest.approx(3.0) assert result.feature_scale[0].item() == pytest.approx(1.0) assert result.policy.target_mean == pytest.approx(result.target_mean) assert result.policy.target_scale == pytest.approx(result.target_scale) def test_categorical_and_near_constant_features_use_identity_guards() -> None: action_indices = [ FEATURE_NAMES.index(f"previous_action_{action.lower()}") for action in ("off", "fuse", "complete", "all") ] varying_fast_index = FEATURE_NAMES.index("fast_total_norm") near_constant_fast_index = FEATURE_NAMES.index("fast_max_abs") rows = [] for index in range(4): row = _row(f"row-{index}", "scene-a", 0.1 + index * 0.1) features = row["features"] for action_index in action_indices: features[action_index] = 0.0 features[action_indices[index]] = 1.0 features[varying_fast_index] = float(index) features[near_constant_fast_index] = index * 5e-7 rows.append(row) result = fit_action_policy(rows, hidden_dim=6, epochs=1, holdout_fraction=0) torch.testing.assert_close( result.feature_mean[action_indices], torch.zeros(len(action_indices)) ) torch.testing.assert_close( result.feature_scale[action_indices], torch.ones(len(action_indices)) ) assert result.feature_scale[near_constant_fast_index].item() == pytest.approx(1.0) assert result.feature_scale[varying_fast_index].item() > 1e-6 changed = torch.stack([torch.tensor(row["features"]) for row in rows]) changed[:, near_constant_fast_index] = 0.75 normalized = (changed - result.feature_mean) / result.feature_scale assert normalized[:, near_constant_fast_index].abs().max().item() < 1.0 assert normalized[:, action_indices].abs().max().item() <= 1.0 def test_conflicting_binding_is_rejected() -> None: first = _row("row-a", "scene-a", 0.1) second_binding = dict(_BINDING) second_binding["model_content_hash"] = "carrier-other" second = _row("row-b", "scene-b", 0.2, binding=second_binding) with pytest.raises(ValueError, match="binding mismatch"): fit_action_policy([first, second], holdout_fraction=0.0) def test_conflicting_duplicate_row_id_is_rejected() -> None: first = _row("same-row", "scene-a", 0.1) second = copy.deepcopy(first) second["advantages"][0] = 0.2 with pytest.raises(ValueError, match="conflicting duplicate"): fit_action_policy([first, second], holdout_fraction=0.0) def test_future_top_level_fields_are_rejected_by_row_whitelist() -> None: row = _row("row-a", "scene-a", 0.1) row["future_label"] = 1.0 with pytest.raises(ValueError, match="fields mismatch"): parse_teacher_row(row) def test_row_json_round_trip_keeps_strict_schema() -> None: row = _row("row-a", "scene-a", 0.1) parsed = parse_teacher_row(json.loads(json.dumps(row))) assert parsed.row_id == "row-a" assert parsed.features.shape == (POLICY_FEATURE_DIM,) assert parsed.feasible_mask.tolist() == [True, True, False, True] def test_sidecar_binding_is_carried_into_loaded_rows(tmp_path) -> None: row = _row("row-sidecar", "scene-a", 0.1) row["provenance"] = {} source = tmp_path / "teacher.jsonl" source.write_text(json.dumps(row) + "\n", encoding="utf-8") source.with_name("teacher.metadata.json").write_text( json.dumps( { "schema_version": "mcss.dynamic.action_teacher.v1", "row_schema_version": ACTION_TEACHER_SCHEMA, "binding": _BINDING, } ), encoding="utf-8", ) dataset = load_teacher_dataset([source]) assert dataset.rows[0].binding == _BINDING result = fit_action_policy(dataset.rows, epochs=1, holdout_fraction=0) assert result.binding["feature_schema_version"] == FEATURE_SCHEMA_VERSION def test_loaded_rows_drop_expanded_provenance_after_fingerprint(tmp_path) -> None: row = _row("row-compact", "scene-a", 0.1) row["provenance"] = { **_BINDING, "source_data": { "manifest_sha256": [ {"path": f"manifest-{index}.json", "sha256": f"hash-{index}"} for index in range(8) ] }, "episode": {"episode_id": "episode-scene-a", "scene_id": "scene-a"}, } expected = parse_teacher_row(row) source = tmp_path / "compact.jsonl" source.write_text(json.dumps(row) + "\n", encoding="utf-8") source.with_name("compact.metadata.json").write_text( json.dumps( { "schema_version": "mcss.dynamic.action_teacher.v1", "row_schema_version": ACTION_TEACHER_SCHEMA, "row_provenance_mode": "episode_compact_v1", "binding": _BINDING, } ), encoding="utf-8", ) dataset = load_teacher_dataset([source]) loaded = dataset.rows[0] assert loaded.fingerprint == expected.fingerprint assert loaded.binding == _BINDING assert loaded.provenance == _BINDING def test_cli_writes_bound_checkpoint_and_refuses_second_output(tmp_path) -> None: source = tmp_path / "teacher.jsonl" source.write_text( "\n".join( json.dumps(_row(f"row-{index}", "scene-a", 0.1 + index * 0.01)) for index in range(3) ) + "\n", encoding="utf-8", ) output = tmp_path / "policy.pt" assert main( [ "--teacher", str(source), "--output", str(output), "--epochs", "3", "--holdout-fraction", "0", "--seed", "7", ] ) == 0 policy, metadata = load_policy_checkpoint(output) assert policy.target_scale >= 1e-6 assert metadata["binding"]["feature_schema_version"] == FEATURE_SCHEMA_VERSION with pytest.raises(FileExistsError, match="overwrite"): main( [ "--teacher", str(source), "--output", str(output), "--epochs", "1", "--holdout-fraction", "0", ] )