"""Shared pytest fixtures for the Obliteratus test suite.""" from __future__ import annotations import socket from unittest.mock import MagicMock import pytest _EXTERNAL_MARKERS = ("network", "download", "remote") _EVIDENCE_MARKERS = ( "cpu", "integration", "slow", "gpu", "mps", "mlx", "network", "download", "remote", "operator_ui", ) def pytest_collection_modifyitems(items): """Attach stable test-layer markers to JUnit timing evidence.""" for item in items: markers = sorted( name for name in _EVIDENCE_MARKERS if item.get_closest_marker(name) is not None ) item.user_properties.append(("duration_markers", ",".join(markers) or "unmarked")) @pytest.fixture(autouse=True) def offline_test_environment(request, monkeypatch): """Fail accidental network access and force offline Hugging Face behavior.""" if any(request.node.get_closest_marker(name) for name in _EXTERNAL_MARKERS): return monkeypatch.setenv("HF_DATASETS_OFFLINE", "1") monkeypatch.setenv("HF_HUB_DISABLE_TELEMETRY", "1") monkeypatch.setenv("HF_HUB_OFFLINE", "1") monkeypatch.setenv("TRANSFORMERS_OFFLINE", "1") def reject_network(*_args, **_kwargs): raise RuntimeError( "unmarked tests may not access the network; add an explicit " "network, download, or remote marker", ) monkeypatch.setattr(socket, "create_connection", reject_network) monkeypatch.setattr(socket.socket, "connect", reject_network) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def mock_model(): """A minimal mock transformer model. Provides: - model.config with config.num_hidden_layers = 4 - model.named_parameters() returning fake weight tensors """ import torch model = MagicMock() # Config with num_hidden_layers config = MagicMock() config.num_hidden_layers = 4 model.config = config # named_parameters returns fake weight tensors across 4 layers fake_params = [] for layer_idx in range(4): weight = torch.randn(768, 768) fake_params.append((f"model.layers.{layer_idx}.self_attn.q_proj.weight", weight)) fake_params.append((f"model.layers.{layer_idx}.self_attn.v_proj.weight", weight)) fake_params.append((f"model.layers.{layer_idx}.mlp.gate_proj.weight", weight)) model.named_parameters.return_value = fake_params return model @pytest.fixture def mock_tokenizer(): """A minimal mock tokenizer with encode, decode, and apply_chat_template.""" tokenizer = MagicMock() tokenizer.encode.return_value = [1, 2, 3, 4, 5] tokenizer.decode.return_value = "Hello, this is a decoded string." tokenizer.apply_chat_template.return_value = [1, 2, 3, 4, 5, 6, 7] tokenizer.pad_token = "" tokenizer.eos_token = "" return tokenizer @pytest.fixture def refusal_direction(): """A normalized random torch tensor of shape (768,).""" import torch t = torch.randn(768) return t / t.norm() @pytest.fixture def activation_pair(): """A tuple of (harmful_activations, harmless_activations) as random tensors of shape (10, 768).""" import torch harmful_activations = torch.randn(10, 768) harmless_activations = torch.randn(10, 768) return (harmful_activations, harmless_activations) @pytest.fixture def tmp_output_dir(tmp_path): """A clean temporary output directory for test artifacts.""" output_dir = tmp_path / "test_output" output_dir.mkdir() return output_dir