File size: 8,374 Bytes
8028640
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279522c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""Enrollment, the no-lookahead guarantee at the runtime layer, and tiering.

These cover the parts of the acceptance list that live between the adapters and
the UI: that enrollment is idempotent and refuses anything it cannot load
safely, that `load_context` cannot hand a model a bar from the future, and that
a CPU-tier model either meets its budget or is recorded as demoted.
"""

from __future__ import annotations

import numpy as np
import pandas as pd
import pytest

from src import config, runtime
from src.adapters import LookaheadError
from tests.fixture import synth


@pytest.fixture
def seeded_store(store):
    """A store with prices and one enrolled baseline."""
    bars = synth(600)
    for year, chunk in bars.groupby(bars["ts"].dt.year):
        store.write_parquet(config.prices_path("BTC-USD", "1h", int(year)), chunk)
    outcome = runtime.enroll(store, "baseline", "baseline/random-walk",
                             enrolled_by="tests")
    assert outcome.ok, outcome.message
    return store


# --------------------------------------------------------------------------
# 5. Enrollment is idempotent, and refuses what it cannot vouch for
# --------------------------------------------------------------------------


def test_enrolling_the_same_revision_twice_is_a_no_op(store):
    first = runtime.enroll(store, "baseline", "baseline/drift", enrolled_by="a")
    registry = store.get_registry()
    second = runtime.enroll(store, "baseline", "baseline/drift", enrolled_by="b",
                            registry=registry)

    assert first.ok and second.ok
    assert second.already is True
    assert len(registry["models"]) == 1
    # The second attempt must not have rewritten who enrolled it.
    assert registry["models"]["drift"]["enrolled_by"] == "a"


def test_unsupported_family_is_refused_with_a_clear_message(store):
    outcome = runtime.enroll(store, "llama", "meta-llama/Llama-3-8B")
    assert not outcome.ok
    assert "not a supported adapter family" in outcome.message
    assert "baseline" in outcome.message      # names what *is* supported


def test_a_malformed_model_id_is_refused(store):
    for bad in ("not-a-model-id", "../../etc/passwd", "a/b/c", ""):
        outcome = runtime.enroll(store, "baseline", bad)
        assert not outcome.ok, f"{bad!r} was accepted"


def test_enrolling_under_the_wrong_family_is_refused(store):
    """A known Chronos model must not be loaded through the Kronos loader."""
    outcome = runtime.enroll(store, "kronos", "amazon/chronos-bolt-tiny")
    assert not outcome.ok
    assert "chronos" in outcome.message


def test_enrollment_records_what_it_verified(store):
    runtime.enroll(store, "baseline", "baseline/bootstrap", enrolled_by="tests")
    entry = store.get_registry()["models"]["bootstrap"]

    assert entry["revision"]                       # pinned
    assert entry["smoke_test"]["ok"] is True       # actually ran
    assert entry["capabilities"]["output"] == "ohlcv_paths"
    assert entry["enrolled_by"] == "tests"
    assert entry["inference_version"]


# --------------------------------------------------------------------------
# 2. No lookahead, at the runtime layer
# --------------------------------------------------------------------------


def test_load_context_never_returns_a_bar_after_the_cut(seeded_store):
    prices = seeded_store.get_prices("BTC-USD", "1h")
    cut = pd.to_datetime(prices["ts"], utc=True).iloc[300]

    context = runtime.load_context(seeded_store, "BTC-USD", "1h", as_of=cut)
    assert pd.to_datetime(context["ts"], utc=True).max() <= cut


def test_an_as_of_forecast_is_issued_from_the_cut_not_the_clock(seeded_store):
    prices = seeded_store.get_prices("BTC-USD", "1h")
    cut = pd.to_datetime(prices["ts"], utc=True).iloc[400]

    run = runtime.run_forecast(seeded_store, "random-walk", "BTC-USD", "1h",
                               horizon=6, seed=1, as_of=cut, archive=False)
    assert run.issued_ts <= cut
    assert run.target_ts[0] > cut


def test_a_context_reaching_past_the_issue_moment_raises(seeded_store):
    """The adapter re-checks, so a caller cannot smuggle one past it."""
    from src.adapters import get_adapter

    context = runtime.load_context(seeded_store, "BTC-USD", "1h")
    stale = pd.to_datetime(context["ts"], utc=True).iloc[-10]
    adapter = get_adapter("baseline", "baseline/random-walk").load()

    with pytest.raises(LookaheadError):
        adapter.predict(context, horizon=4, seed=0, issued_ts=stale)


def test_forecast_targets_start_after_the_context_ends(seeded_store):
    run = runtime.run_forecast(seeded_store, "random-walk", "BTC-USD", "1h",
                               horizon=8, seed=3, archive=False)
    assert run.target_ts[0] > run.issued_ts
    assert len(run.target_ts) == 8


# --------------------------------------------------------------------------
# 7. Tiering
# --------------------------------------------------------------------------


def test_a_cpu_tier_model_either_meets_the_budget_or_is_recorded_as_demoted(store):
    """The rule the bootstrap script applies, checked directly.

    The registry on the Hub is written by a measurement run; this asserts the
    rule that run enforces, so a change to the thresholds cannot silently let a
    model sit in the CPU tier while missing its budget.
    """
    from scripts.bootstrap_registry import measure

    context = synth(config.DEFAULT_CONTEXT_BARS)
    latency = measure("baseline", "baseline/random-walk", context)

    assert latency["cold_s"] <= config.CPU_BUDGET_COLD_S
    assert latency["warm_s"] <= config.CPU_BUDGET_WARM_S
    assert latency["hardware"] == "cpu"
    assert latency["demoted"] is False
    # The measurement is meaningless without the machine it was taken on.
    assert latency["machine"]


def test_the_demotion_rule_fires_when_a_budget_is_missed(monkeypatch):
    """A CPU-declared model that is too slow must come back as gpu."""
    from scripts import bootstrap_registry

    context = synth(config.DEFAULT_CONTEXT_BARS)
    monkeypatch.setattr(config, "CPU_BUDGET_WARM_S", 0.0)
    latency = bootstrap_registry.measure("baseline", "baseline/drift", context)

    assert latency["declared_hardware"] == "cpu"
    assert latency["hardware"] == "gpu"
    assert latency["demoted"] is True


# --------------------------------------------------------------------------
# Anonymous session cap
# --------------------------------------------------------------------------


def test_the_session_cap_is_a_real_number():
    assert config.ANON_SESSION_CAP > 0


# --------------------------------------------------------------------------
# ZeroGPU: constructing an adapter must not touch CUDA
# --------------------------------------------------------------------------


def test_constructing_an_adapter_never_probes_the_device(monkeypatch):
    """Regression: on ZeroGPU an out-of-context CUDA probe is fatal.

    `torch.cuda.is_available()` raises unless it is called inside a
    `@spaces.GPU` function. Probing eagerly in `__init__` made merely building
    an adapter fail -- including for CPU-tier models that never wanted a GPU,
    which took the whole forecast endpoint down on ZeroGPU.
    """
    from src.adapters import base, get_adapter

    def explode():
        raise AssertionError("device was probed at construction time")

    monkeypatch.setattr(base, "default_device", explode)

    for family, model_id in (("baseline", "baseline/random-walk"),
                             ("chronos", "amazon/chronos-bolt-tiny"),
                             ("kronos", "NeoQuasar/Kronos-mini"),
                             ("timesfm", "google/timesfm-2.5-200m-pytorch")):
        adapter = get_adapter(family, model_id)
        # Capabilities must be readable without a device, because that is how
        # `run_forecast` decides whether a GPU is even needed.
        caps = adapter.capabilities()
        assert caps.hardware in ("cpu", "gpu")
        assert caps.output in ("quantile_line", "ohlcv_paths")


def test_capabilities_are_readable_without_loading_weights():
    from src.adapters import get_adapter

    adapter = get_adapter("kronos", "NeoQuasar/Kronos-base")
    caps = adapter.capabilities()
    assert adapter._model is None, "capabilities() loaded the model"
    assert caps.output == "ohlcv_paths"
    assert caps.hardware == "gpu"