"""Smoke test: Dockerfile.hf is well-formed and contains expected stages. We don't actually build the image (too slow for unit tests). We just verify the file exists, is non-empty, and has the load-bearing instructions. """ from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[2] DOCKERFILE = REPO_ROOT / "Dockerfile.hf" @pytest.fixture(scope="module") def dockerfile_text() -> str: if not DOCKERFILE.exists(): pytest.skip(f"{DOCKERFILE} does not exist yet (Day-8 T3 RED phase)") return DOCKERFILE.read_text() class TestDockerfileHF: """Day-8 T3: Hugging Face Spaces Dockerfile smoke.""" def test_dockerfile_exists_and_nonempty(self): assert DOCKERFILE.exists(), f"missing {DOCKERFILE}" assert DOCKERFILE.stat().st_size > 0, f"{DOCKERFILE} is empty" def test_dockerfile_contains_required_stages(self, dockerfile_text): """The HF Dockerfile must: - Start FROM a Python base - Install requirements.txt - Seed data/raw/bbbp.csv AND data/raw/eeg.fif from fixtures - Build the BBB model artifact at build time - Run all three pipelines (BBB / EEG / MRI) so mlruns/ has one run per modality available to /experiments/runs at startup - Expose port 7860 (HF Spaces convention) - Launch via supervisord """ text = dockerfile_text.lower() assert "from python" in text, "must FROM a Python base image" assert "requirements.txt" in text, "must reference requirements.txt" assert "src.models.bbb_model" in dockerfile_text, ( "must build the BBB model artifact at image-build time" ) assert "src.pipelines.bbb_pipeline" in dockerfile_text, ( "must run BBB pipeline at build so mlruns/ has a BBB run" ) assert "src.pipelines.eeg_pipeline" in dockerfile_text, ( "must run EEG pipeline at build so mlruns/ has an EEG run" ) assert "src.pipelines.mri_pipeline" in dockerfile_text, ( "must run MRI pipeline at build so mlruns/ has an MRI run" ) assert "tests/fixtures/eeg_sample.fif" in dockerfile_text, ( "must seed data/raw/eeg.fif from the bundled fixture so the " "Signal tab works without user file upload" ) assert "7860" in text, "must expose port 7860 (HF Spaces convention)" assert "supervisord" in text, ( "must launch FastAPI + Streamlit via supervisord" ) def test_dockerfile_does_not_disable_mlflow(self, dockerfile_text): """The MLflow kill-switch must be absent from the Dockerfile — file-store mlruns/ is built into the image and is safe to expose on the read-only demo. Re-introducing the kill-switch would silently kill the Experiments tab and the BBB provenance strip. Scan ENV lines only, not comment lines, so a future docstring or comment that cites the env name does not false-positive. """ import re env_lines = [ line for line in dockerfile_text.splitlines() if re.match( r"\s*(ENV\s+)?NEUROBRIDGE_DISABLE_MLFLOW\s*=\s*1", line, re.IGNORECASE, ) ] assert not env_lines, ( "Dockerfile must NOT set NEUROBRIDGE_DISABLE_MLFLOW=1 — that " "empties the Experiments tab and blanks the BBB provenance " "strip. If you need to disable MLflow at runtime, set the env " "manually on the Space, do not bake it into the image. " f"Offending lines: {env_lines}" ) def test_dockerfile_byte_identical_to_hf_alias(self): """`Dockerfile` (HF auto-discovers this name) and `Dockerfile.hf` (canonical/readable name) must stay byte-identical. Drift between the two means HF builds a different image than the one the test suite verifies — silent deploy regression. Catch it in CI.""" canonical = REPO_ROOT / "Dockerfile.hf" alias = REPO_ROOT / "Dockerfile" assert canonical.exists(), f"missing {canonical}" assert alias.exists(), f"missing {alias}" assert canonical.read_bytes() == alias.read_bytes(), ( "Dockerfile and Dockerfile.hf have diverged. Re-sync them: " "`cp Dockerfile.hf Dockerfile` (or vice versa)." )