openclinical-ai / tests /test_substrate.py
bclermo's picture
Upload folder using huggingface_hub
00c1e8f verified
Raw
History Blame Contribute Delete
12.1 kB
"""Tests for the openclinical-ai substrate MVP.
Run with:
python3 -m pytest tests/
"""
from __future__ import annotations
import asyncio
import json
import sys
import tempfile
from pathlib import Path
import pytest
# Add parent dir to path so we can import runtime modules
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from runtime.config import Settings
from runtime.audit import AuditLogger
from runtime.consent import ConsentEngine, ConsentDenied
from runtime.models import (
LoadedModel,
ModelCard,
ModelRegistry,
ModelSignatureError,
PSWShiftHandoffAdapter,
_categorize_notes,
_detect_concerns,
_structure_observations,
)
@pytest.fixture
def tmp_paths():
"""Temp directories for registry/audit/consent."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
yield {
"registry": tmp / "registry",
"audit": tmp / "audit",
"consent": tmp / "consent",
}
# -- audit --------------------------------------------------------------------
@pytest.mark.asyncio
async def test_audit_log_and_query(tmp_paths):
audit = AuditLogger(audit_path=tmp_paths["audit"])
eid = await audit.log(
event_type="inference",
patient_id="p1",
model_id="m1",
model_version="1.0",
consent_token="tok",
)
assert eid
events = await audit.query(patient_id="p1")
assert len(events) == 1
assert events[0]["event_type"] == "inference"
@pytest.mark.asyncio
async def test_audit_query_filtered(tmp_paths):
audit = AuditLogger(audit_path=tmp_paths["audit"])
await audit.log(event_type="inference", patient_id="p1", model_id="m1")
await audit.log(event_type="inference", patient_id="p2", model_id="m1")
p1_events = await audit.query(patient_id="p1")
assert len(p1_events) == 1
assert p1_events[0]["patient_id"] == "p1"
# -- consent ------------------------------------------------------------------
@pytest.mark.asyncio
async def test_consent_default_denied(tmp_paths):
engine = ConsentEngine(consent_path=tmp_paths["consent"])
with pytest.raises(ConsentDenied):
await engine.check("p1", "m1")
@pytest.mark.asyncio
async def test_consent_granted(tmp_paths):
engine = ConsentEngine(consent_path=tmp_paths["consent"])
token = await engine.grant_consent("p1", ["*"], "psw-brian")
assert token
await engine.check("p1", "m1", consent_token=token) # should not raise
@pytest.mark.asyncio
async def test_consent_revoked(tmp_paths):
engine = ConsentEngine(consent_path=tmp_paths["consent"])
await engine.grant_consent("p1", ["*"], "psw-brian")
await engine.revoke_consent("p1", "psw-brian")
with pytest.raises(ConsentDenied):
await engine.check("p1", "m1")
@pytest.mark.asyncio
async def test_consent_scope_limited(tmp_paths):
engine = ConsentEngine(consent_path=tmp_paths["consent"])
token = await engine.grant_consent("p1", ["psw-shift-handoff"], "psw-brian")
await engine.check("p1", "psw-shift-handoff", consent_token=token) # ok
with pytest.raises(ConsentDenied):
await engine.check("p1", "biology-protein-fold", consent_token=token)
# -- PSW adapter --------------------------------------------------------------
@pytest.mark.asyncio
async def test_psw_adapter_basic():
adapter = PSWShiftHandoffAdapter()
out = await adapter.run({
"notes": "BP 145/90, walked 20m, calm mood, daughter visited, refused meds",
"observations": {"bp": "145/90", "hr": "78", "ambulation": "walked 20m", "mood": "calm"},
"resident_id": "r1",
"psw_id": "psw-brian",
})
h = out["shift_handoff"]
assert h["psw_id"] == "psw-brian"
assert h["resident_id"] == "r1"
assert "Vitals" in h["summary"]
assert h["structured_observations"]["vitals"]["bp"] == "145/90"
@pytest.mark.asyncio
async def test_psw_adapter_high_bp_concern():
out = await PSWShiftHandoffAdapter().run({
"notes": "BP 195/110",
"observations": {"bp": "195/110"},
"resident_id": "r1",
"psw_id": "psw-brian",
})
concerns = out["shift_handoff"]["concerns"]
assert len(concerns) >= 1
assert any(c["type"] == "vitals" and c["severity"] in ("medium", "high") for c in concerns)
@pytest.mark.asyncio
async def test_psw_adapter_low_spo2_concern():
out = await PSWShiftHandoffAdapter().run({
"notes": "SpO2 85% on room air",
"observations": {"spo2": "85%"},
"resident_id": "r1",
"psw_id": "psw-brian",
})
concerns = out["shift_handoff"]["concerns"]
assert any(c["type"] == "vitals" and c["severity"] == "high" for c in concerns)
@pytest.mark.asyncio
async def test_psw_adapter_incident_concern():
out = await PSWShiftHandoffAdapter().run({
"notes": "Resident had a fall in the bathroom",
"observations": {},
"resident_id": "r1",
"psw_id": "psw-brian",
})
concerns = out["shift_handoff"]["concerns"]
assert any(c["type"] == "incident" for c in concerns)
assert out["shift_handoff"]["followup_required"] is True
# -- heuristic helpers --------------------------------------------------------
def test_categorize_vitals():
cats = _categorize_notes("BP 145/90 and HR 78")
assert "vitals" in cats
def test_categorize_mobility():
cats = _categorize_notes("Walked 20m with walker")
assert "mobility" in cats
def test_categorize_mood():
cats = _categorize_notes("Resident was agitated and confused")
assert "mood" in cats
def test_categorize_general_fallback():
cats = _categorize_notes("Random notes that match nothing specific")
assert "general" in cats
def test_structure_observations():
obs = _structure_observations({"bp": "120/80", "hr": "72", "ambulation": "walked 10m"})
assert obs["vitals"]["bp"] == "120/80"
assert obs["vitals"]["hr"] == "72"
assert obs["mobility"]["ambulation"] == "walked 10m"
def test_detect_concerns_temp_high():
concerns = _detect_concerns(_structure_observations({"temp_c": "39.5"}), {})
assert any("Temp" in c["detail"] for c in concerns)
# -- model registry -----------------------------------------------------------
@pytest.mark.asyncio
async def test_model_registry_signature_required(tmp_paths):
"""Unsigned manifest must be rejected."""
registry_dir = tmp_paths["registry"]
registry_dir.mkdir(parents=True, exist_ok=True)
(registry_dir / "bad.manifest.json").write_text(json.dumps({
"id": "bad-model",
"version": "1.0.0",
"model_type": "test",
"description": "no signature",
# no signature field!
}))
registry = ModelRegistry(registry_path=registry_dir)
# load_all should silently skip — but no model loaded
count = await registry.load_all()
assert count == 0
assert len(registry.loaded_models) == 0
@pytest.mark.asyncio
async def test_model_registry_signature_invalid(tmp_paths):
"""Tampered manifest must be rejected."""
registry_dir = tmp_paths["registry"]
keys_dir = registry_dir / "keys"
keys_dir.mkdir(parents=True, exist_ok=True)
from nacl.signing import SigningKey
sk = SigningKey.generate()
keys_dir.joinpath("default.pub").write_text(bytes(sk.verify_key).hex())
# Manifest with bogus signature
manifest = {
"id": "tampered",
"version": "1.0.0",
"model_type": "test",
"description": "tampered",
"signature": {
"key_id": "default",
"algorithm": "ed25519",
"value": "00" * 64, # bogus
},
}
(registry_dir / "tampered.manifest.json").write_text(json.dumps(manifest))
registry = ModelRegistry(registry_path=registry_dir)
count = await registry.load_all()
assert count == 0
@pytest.mark.asyncio
async def test_model_registry_signed_manifest_loads(tmp_paths):
"""A correctly-signed manifest should load successfully."""
from nacl.signing import SigningKey
registry_dir = tmp_paths["registry"]
keys_dir = registry_dir / "keys"
keys_dir.mkdir(parents=True, exist_ok=True)
sk = SigningKey.generate()
keys_dir.joinpath("default.pub").write_text(bytes(sk.verify_key).hex())
manifest = {
"id": "psw-shift-handoff",
"version": "1.0.0",
"model_type": "clinical-heuristic",
"description": "test",
"card": {
"id": "psw-shift-handoff",
"version": "1.0.0",
"model_type": "clinical-heuristic",
"description": "test",
"intended_use": "test",
"training_data": "test",
"evaluation_metrics": {},
"ethical_considerations": "",
"clinical_validation": "",
"phipa_compliance": "",
"license": "Apache-2.0",
},
"artifact_sha256": "00" * 32,
"artifact_size_bytes": 0,
"created_at": "2026-06-29T00:00:00Z",
}
canonical = json.dumps(manifest, sort_keys=True).encode()
sig = sk.sign(canonical).signature
manifest["signature"] = {"key_id": "default", "algorithm": "ed25519", "value": sig.hex()}
(registry_dir / "psw-shift-handoff.v1.0.0.manifest.json").write_text(json.dumps(manifest))
registry = ModelRegistry(registry_path=registry_dir)
count = await registry.load_all()
assert count == 1
model = registry.get("psw-shift-handoff")
assert model is not None
assert model.version == "1.0.0"
# -- end-to-end through server -------------------------------------------------
@pytest.mark.asyncio
async def test_full_inference_flow(tmp_paths):
"""End-to-end: signed manifest → consent → inference → audit."""
from nacl.signing import SigningKey
# Set up registry with signed manifest
registry_dir = tmp_paths["registry"]
keys_dir = registry_dir / "keys"
keys_dir.mkdir(parents=True, exist_ok=True)
sk = SigningKey.generate()
keys_dir.joinpath("default.pub").write_text(bytes(sk.verify_key).hex())
manifest = {
"id": "psw-shift-handoff",
"version": "1.0.0",
"model_type": "clinical-heuristic",
"description": "test",
"card": {
"id": "psw-shift-handoff", "version": "1.0.0",
"model_type": "clinical-heuristic", "description": "test",
"intended_use": "test", "training_data": "test",
"evaluation_metrics": {}, "ethical_considerations": "",
"clinical_validation": "", "phipa_compliance": "",
"license": "Apache-2.0",
},
"artifact_sha256": "00" * 32,
"artifact_size_bytes": 0,
"created_at": "2026-06-29T00:00:00Z",
}
canonical = json.dumps(manifest, sort_keys=True).encode()
sig = sk.sign(canonical).signature
manifest["signature"] = {"key_id": "default", "algorithm": "ed25519", "value": sig.hex()}
(registry_dir / "psw-shift-handoff.v1.0.0.manifest.json").write_text(json.dumps(manifest))
# Use the registry + audit + consent
registry = ModelRegistry(registry_path=registry_dir)
await registry.load_all()
audit = AuditLogger(audit_path=tmp_paths["audit"])
consent = ConsentEngine(consent_path=tmp_paths["consent"])
# Grant consent
token = await consent.grant_consent("resident-001", ["*"], "psw-brian")
# Inference
model = registry.get("psw-shift-handoff")
await consent.check("resident-001", model.id, consent_token=token)
out = await model.run({
"notes": "BP 120/80, walked 10m",
"observations": {"bp": "120/80", "ambulation": "walked 10m"},
"resident_id": "r1",
"psw_id": "psw-brian",
})
eid = await audit.log(event_type="inference", patient_id="resident-001", model_id=model.id, model_version=model.version)
# Verify
assert "Vitals" in out["shift_handoff"]["summary"]
events = await audit.query(patient_id="resident-001")
assert len(events) == 1
assert events[0]["event_type"] == "inference"