Source: tests/test_opportunity_runner.py · Plain text

tests/test_opportunity_runner.py

import inspect
import json

import pytest
import torch
from PIL import Image
from torch import nn

from mcss.vision_probe import opportunity_runner as runner
from mcss.vision_probe.adaptation import Readout
from mcss.vision_probe.experiment import FrameFeatures
from mcss.vision_probe.features import FrozenDinoExtractor
from mcss.vision_probe.opportunity_selectors import select_candidates


@pytest.mark.parametrize("size,grid", [(224, 16), (448, 32)])
def test_true_input_resolution_not_interpolation(tmp_path, monkeypatch, size, grid):
    class Backbone(nn.Module):
        embed_dim = 384
        patch_size = 14

        def load_state_dict(self, state, strict=True):
            assert strict

        def forward_features(self, x):
            assert x.shape == (1, 3, size, size)
            return {"x_norm_patchtokens": torch.ones(1, (x.shape[-1] // 14) ** 2, 384)}

    weights = tmp_path / "weights.pt"
    torch.save({}, weights)
    image = tmp_path / "image.png"
    Image.new("RGB", (51, 37)).save(image)
    monkeypatch.setattr(torch.hub, "load", lambda *a, **kw: Backbone())
    result = FrozenDinoExtractor(tmp_path, weights, "cpu", size).extract(image)
    assert result["features"].shape == (grid, grid, 384)
    assert result["rgb"].shape == (grid, grid, 3)


def test_candidate_isolation_off_and_rank():
    torch.manual_seed(2)
    readout = Readout(
        torch.zeros(64), torch.eye(64), torch.ones(64), torch.randn(64, 3) * 0.1, torch.zeros(3)
    )
    features = torch.randn(4, 4, 64)
    target = FrameFeatures(features, torch.rand(4, 4, 3), (56, 56), (56, 56), "fake")
    _, states = runner.candidate_states(target, readout, 5.0)
    state, adapted, audits = states[0]
    assert state.a.count_nonzero() == state.b.count_nonzero() == 0
    assert torch.equal(adapted, features)
    assert all(a["numerical_rank"] <= 3 for _, _, aa in states for a in aa)
    states[1][0].a.fill_(123)
    assert states[0][0].a.count_nonzero() == 0
    assert not torch.equal(states[1][0].a, states[2][0].a)


def test_guard_denies_official_val_reserve_and_unopened_internal_validation(tmp_path):
    splits = {
        "fit": ["fit"],
        "discovery": ["disc"],
        "validation": ["val"],
        "reserve": ["reserve"],
        "official_val_sealed": ["official"],
    }
    guard = runner.MediaGuard(tmp_path, splits)
    guard.allow("discovery")
    guard.check_path(tmp_path / "JPEGImages/480p/disc/00000.jpg")
    for sequence in ("official", "reserve", "val", "fit"):
        with pytest.raises(PermissionError):
            guard.check_path(tmp_path / "Annotations/480p" / sequence / "00000.png")
    with pytest.raises(ValueError):
        guard.allow("reserve")


def test_split_identity_overlap_rejected():
    path = runner.ROOT / "docs/experiments/EXP-2D-20260921-corrected/protocol.json"
    splits = json.loads(path.read_text())["splits"]
    runner.validate_splits(splits)
    splits["validation"][0] = splits["discovery"][0]
    with pytest.raises(ValueError):
        runner.validate_splits(splits)


def test_selector_signature_no_target_answers():
    assert set(inspect.signature(select_candidates).parameters) == {
        "features",
        "trajectories",
        "artifact",
    }


def test_validation_lock_detects_selector_changes(tmp_path, monkeypatch):
    report = tmp_path / "report"
    work = tmp_path / "work"
    report.mkdir()
    work.mkdir()
    monkeypatch.setattr(runner, "REPORT", report)
    monkeypatch.setattr(runner, "WORK", work)
    weights = tmp_path / "weights.pt"
    weights.write_bytes(b"self-contained weight artifact for lock hashing")
    monkeypatch.setattr(runner, "WEIGHTS", weights)
    monkeypatch.setattr(runner, "source_hashes", lambda: {"runner": "sourcehash"})
    for name in ("config.json", "split_manifest.json", "selectors.json", "discovery_raw.jsonl"):
        (report / name).write_text("{}")
    for size in runner.CONFIG["resolutions"]:
        (work / f"readout_{size}.pt").write_text("readout")
    lock = {
        "source_hashes": runner.source_hashes(),
        "config_sha256": runner.sha(report / "config.json"),
        "split_sha256": runner.sha(report / "split_manifest.json"),
        "selectors_sha256": runner.sha(report / "selectors.json"),
        "weights_sha256": runner.sha(runner.WEIGHTS),
        "discovery_sha256": runner.sha(report / "discovery_raw.jsonl"),
        "readout_sha256": {
            str(s): runner.sha(work / f"readout_{s}.pt") for s in runner.CONFIG["resolutions"]
        },
        "fit_split": "discovery",
    }
    runner.write_json(report / "validation_lock.json", lock)
    runner.verify_lock()
    (report / "selectors.json").write_text('{"threshold":999}')
    with pytest.raises(RuntimeError, match="selectors_sha256"):
        runner.verify_lock()