Python · 3450 bytes Raw Blame History
1 """DlmLock schema — construction, round-trip, field constraints."""
2
3 from __future__ import annotations
4
5 from datetime import UTC, datetime
6
7 import pytest
8 from pydantic import ValidationError
9
10 from dlm.lock.schema import CURRENT_LOCK_VERSION, DlmLock
11
12
13 def _minimal_lock(**overrides: object) -> DlmLock:
14 """Build a valid DlmLock with every required field populated."""
15 base = {
16 "created_at": datetime(2026, 4, 19, 12, 0, 0, tzinfo=UTC),
17 "dlm_id": "01HZXYAAAAAAAAAAAAAAAAAA00",
18 "dlm_sha256": "0" * 64,
19 "base_model_revision": "abc123",
20 "hardware_tier": "cpu",
21 "seed": 42,
22 "determinism_class": "best-effort",
23 "last_run_id": 1,
24 }
25 base.update(overrides)
26 return DlmLock(**base) # type: ignore[arg-type]
27
28
29 class TestDlmLockShape:
30 def test_default_lock_version_is_current(self) -> None:
31 assert _minimal_lock().lock_version == CURRENT_LOCK_VERSION
32
33 def test_frozen(self) -> None:
34 """`model_config.frozen=True` — mutation must raise."""
35 lock = _minimal_lock()
36 with pytest.raises(ValidationError):
37 lock.seed = 99 # type: ignore[misc]
38
39 def test_extra_keys_forbidden(self) -> None:
40 with pytest.raises(ValidationError, match="extra"):
41 DlmLock( # type: ignore[call-arg]
42 created_at=datetime.now(UTC),
43 dlm_id="01HZXY",
44 dlm_sha256="0" * 64,
45 base_model_revision="r",
46 hardware_tier="cpu",
47 seed=0,
48 determinism_class="advisory",
49 last_run_id=1,
50 who_let_this_in="drift",
51 )
52
53 def test_dlm_sha256_must_be_hex_64(self) -> None:
54 with pytest.raises(ValidationError):
55 _minimal_lock(dlm_sha256="deadbeef") # too short
56
57 def test_hardware_tier_literal_is_enforced(self) -> None:
58 with pytest.raises(ValidationError):
59 _minimal_lock(hardware_tier="tpu") # type: ignore[arg-type]
60
61 def test_determinism_class_literal_is_enforced(self) -> None:
62 with pytest.raises(ValidationError):
63 _minimal_lock(determinism_class="perfect") # type: ignore[arg-type]
64
65 def test_last_run_id_ge_1(self) -> None:
66 with pytest.raises(ValidationError):
67 _minimal_lock(last_run_id=0)
68
69 def test_pinned_versions_defaults_empty(self) -> None:
70 assert _minimal_lock().pinned_versions == {}
71
72
73 class TestRoundTrip:
74 def test_json_round_trip_preserves_fields(self) -> None:
75 original = _minimal_lock(
76 pinned_versions={"torch": "2.5.1", "transformers": "4.45.2"},
77 determinism_flags={"cublas_workspace": ":4096:8", "use_det_algos": True},
78 cuda_version="12.1",
79 )
80 payload = original.model_dump(mode="json")
81 restored = DlmLock.model_validate(payload)
82 assert restored == original
83
84
85 class TestLockErrors:
86 def test_lock_validation_error_joins_reasons(self) -> None:
87 from pathlib import Path
88
89 from dlm.lock.errors import LockValidationError
90
91 exc = LockValidationError(
92 path=Path("/tmp/dlm.lock"),
93 reasons=["torch drift", "base revision changed"],
94 )
95 assert exc.path == Path("/tmp/dlm.lock")
96 assert exc.reasons == ["torch drift", "base revision changed"]
97 assert "torch drift" in str(exc)
98 assert "base revision changed" in str(exc)