Source: tests/test_v2_runner.py · Plain text

tests/test_v2_runner.py

"""V2 runner candidate isolation and complete-prediction lock contracts."""

from types import SimpleNamespace

import pytest
import torch

from mcss.vision_probe.adaptation import Readout
from mcss.vision_probe.v2_runner import (
    METHODS,
    TRAJECTORIES,
    _candidate_states,
    _prediction_checks,
    sha,
    write,
)


def test_off_isolation_and_reproducible_candidate_rollouts():
    torch.manual_seed(12)
    target = SimpleNamespace(features=torch.randn(2, 2, 64), rgb=torch.rand(2, 2, 3))
    readout = Readout(
        torch.zeros(64), torch.eye(64), torch.ones(64), torch.randn(64, 3), torch.zeros(3)
    )
    original = target.features.clone()
    _, first, cost = _candidate_states(target, readout)
    _, second, _ = _candidate_states(target, readout)
    assert torch.equal(first[0][1], original)
    assert not first[0][0].a.any() and not first[0][0].b.any()
    assert torch.equal(target.features, original)
    for (left, a), (right, b) in zip(first, second, strict=True):
        assert torch.equal(left.a, right.a) and torch.equal(left.b, right.b)
        assert torch.equal(a, b)
    first[1][0].a.fill_(99)
    assert not first[0][0].a.any()
    assert not torch.equal(first[1][0].a, first[2][0].a)
    assert cost["proposal_calls"] == 32 and cost["increment_calculations"] == 64


def setup_lock(tmp_path, monkeypatch):
    import mcss.vision_probe.v2_runner as module

    manifest = tmp_path / "manifest.json"
    write(
        manifest,
        {"sequences": [{"sequence": "new", "targets": [{"target_slot": 1}, {"target_slot": 2}]}]},
    )
    write(tmp_path / "selector_lock.json", {})
    write(tmp_path / "config.json", {})
    lock = {
        "selector_lock_sha256": sha(tmp_path / "selector_lock.json"),
        "config_sha256": sha(tmp_path / "config.json"),
        "split_sha256": sha(manifest),
        "source_hashes": {},
    }
    write(tmp_path / "prediction_lock.json", lock)
    monkeypatch.setattr(module, "_source_hashes", lambda: {})
    monkeypatch.setattr(module, "_verify_selectors", lambda p: {})
    document = {
        "complete": True,
        "prediction_lock_sha256": sha(tmp_path / "prediction_lock.json"),
        "predictions": [
            {
                "sequence": "new",
                "target_slot": i,
                "choices": {m: 0 for m in METHODS},
                "trajectories": [list(t) for t in TRAJECTORIES],
                "selector_lock_sha256": lock["selector_lock_sha256"],
            }
            for i in (1, 2)
        ],
    }
    return manifest, document


def test_prediction_gate_rejects_missing_pair(tmp_path, monkeypatch):
    manifest, document = setup_lock(tmp_path, monkeypatch)
    document["predictions"].pop()
    with pytest.raises(ValueError, match="pair coverage"):
        _prediction_checks(tmp_path, manifest, document)


def test_prediction_gate_rejects_missing_method_or_candidate(tmp_path, monkeypatch):
    manifest, document = setup_lock(tmp_path, monkeypatch)
    del document["predictions"][0]["choices"]["CycleGate"]
    with pytest.raises(ValueError, match="deployment decisions"):
        _prediction_checks(tmp_path, manifest, document)
    document["predictions"][0]["choices"]["CycleGate"] = 0
    document["predictions"][0]["trajectories"].pop()
    with pytest.raises(ValueError, match="candidate trajectories"):
        _prediction_checks(tmp_path, manifest, document)


def test_prediction_gate_rejects_changed_source(tmp_path, monkeypatch):
    import mcss.vision_probe.v2_runner as module

    manifest, document = setup_lock(tmp_path, monkeypatch)
    monkeypatch.setattr(module, "_source_hashes", lambda: {"changed.py": "different"})
    with pytest.raises(ValueError, match="changed"):
        _prediction_checks(tmp_path, manifest, document)


def test_complete_synthetic_prediction_artifacts_verify(tmp_path, monkeypatch):
    manifest, document = setup_lock(tmp_path, monkeypatch)
    for row in document["predictions"]:
        path = tmp_path / f"tensor_{row['target_slot']}.pt"
        torch.save(
            {
                "source_features": torch.zeros(1).expand(32, 32, 384),
                "adapted": torch.zeros(1).expand(16, 32, 32, 384),
                "choices": row["choices"],
                "trajectories": row["trajectories"],
            },
            path,
        )
        row["artifact_path"] = str(path)
        row["artifact_sha256"] = sha(path)
    assert _prediction_checks(tmp_path, manifest, document)["source_hashes"] == {}
    payload = torch.load(document["predictions"][0]["artifact_path"], weights_only=True)
    payload["adapted"] = payload["adapted"][:15]
    torch.save(payload, document["predictions"][0]["artifact_path"])
    document["predictions"][0]["artifact_sha256"] = sha(document["predictions"][0]["artifact_path"])
    with pytest.raises(ValueError, match="candidate schema"):
        _prediction_checks(tmp_path, manifest, document)