File size: 7,124 Bytes
dfb775d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Coach chronos_client + the three /coach/api/diagnostics/* endpoints
it powers. Tests stay independent of a running mindX backend by
patching httpx.AsyncClient via MockTransport — same approach used by
test_ollama_backend.py.
"""

from __future__ import annotations

import httpx
import pytest
from fastapi.testclient import TestClient

from mindxtrain.operator import app as operator_app
from mindxtrain.operator.coach import chronos_client

client = TestClient(operator_app.app)


# ---- chronos_client direct ----------------------------------------------


@pytest.mark.asyncio
async def test_now_returns_promised_time_shape(monkeypatch):
    body = {
        "unix_18dp": "1779169999.123456789",
        "utc": "2026-05-19T05:53:19.123456789+00:00",
        "consensus": "correlated",
        "confidence_ms": 42.0,
        "sources": {"cpu": {"unix": 1779169999.0}},
        "anchor_count_24h": 12,
        "promised_by": "chronos.agent",
    }
    transport = httpx.MockTransport(lambda req: httpx.Response(200, json=body))
    orig = httpx.AsyncClient
    monkeypatch.setattr(
        httpx, "AsyncClient",
        lambda *a, **kw: orig(*a, **{**kw, "transport": transport}),
    )
    pt = await chronos_client.now()
    assert pt["consensus"] == "correlated"
    assert pt["confidence_ms"] == 42.0
    assert pt["anchor_count_24h"] == 12
    assert pt["promised_by"] == "chronos.agent"


@pytest.mark.asyncio
async def test_now_degrades_on_connection_error(monkeypatch):
    def _raise(*_a, **_kw):
        raise httpx.ConnectError("mindX down")
    transport = httpx.MockTransport(_raise)
    orig = httpx.AsyncClient
    monkeypatch.setattr(
        httpx, "AsyncClient",
        lambda *a, **kw: orig(*a, **{**kw, "transport": transport}),
    )
    pt = await chronos_client.now()
    # The contract: same keys present, consensus flips to "unavailable".
    assert pt["consensus"] == "unavailable"
    assert pt["confidence_ms"] == 0.0
    assert pt["anchor_count_24h"] == 0
    assert pt["promised_by"] == "chronos.agent"
    assert "error" in pt["sources"]


@pytest.mark.asyncio
async def test_now_fills_missing_keys_from_partial_response(monkeypatch):
    """Defensive against upstream shape drift — caller never KeyErrors."""
    transport = httpx.MockTransport(
        lambda req: httpx.Response(200, json={"unix_18dp": "1.0"}),
    )
    orig = httpx.AsyncClient
    monkeypatch.setattr(
        httpx, "AsyncClient",
        lambda *a, **kw: orig(*a, **{**kw, "transport": transport}),
    )
    pt = await chronos_client.now()
    assert pt["unix_18dp"] == "1.0"
    assert pt["consensus"] == "unavailable"  # default fill
    assert pt["anchor_count_24h"] == 0
    assert pt["sources"] == {}


@pytest.mark.asyncio
async def test_anchors_returns_list_and_n(monkeypatch):
    body = {"anchors": [{"id": 1, "chain": "algorand"}], "n": 1}
    transport = httpx.MockTransport(lambda req: httpx.Response(200, json=body))
    orig = httpx.AsyncClient
    monkeypatch.setattr(
        httpx, "AsyncClient",
        lambda *a, **kw: orig(*a, **{**kw, "transport": transport}),
    )
    out = await chronos_client.anchors(limit=10)
    assert out["n"] == 1
    assert out["anchors"][0]["chain"] == "algorand"


@pytest.mark.asyncio
async def test_anchors_returns_empty_on_failure(monkeypatch):
    def _raise(*_a, **_kw):
        raise httpx.ConnectError("nope")
    transport = httpx.MockTransport(_raise)
    orig = httpx.AsyncClient
    monkeypatch.setattr(
        httpx, "AsyncClient",
        lambda *a, **kw: orig(*a, **{**kw, "transport": transport}),
    )
    out = await chronos_client.anchors(limit=10)
    assert out["n"] == 0
    assert out["anchors"] == []
    assert "error" in out


@pytest.mark.asyncio
async def test_drift_returns_history_shape(monkeypatch):
    body = {
        "hours": 24, "bucket_count": 2,
        "buckets": [{"ts_unix": 1000, "n": 3, "drift_mean_ms": 5.0, "drift_std_ms": 1.0}],
        "drift_std_ms": 1.5, "drift_max_abs_ms": 8.0, "anchor_count": 5,
    }
    transport = httpx.MockTransport(lambda req: httpx.Response(200, json=body))
    orig = httpx.AsyncClient
    monkeypatch.setattr(
        httpx, "AsyncClient",
        lambda *a, **kw: orig(*a, **{**kw, "transport": transport}),
    )
    out = await chronos_client.drift(hours=24)
    assert out["bucket_count"] == 2
    assert out["anchor_count"] == 5


@pytest.mark.asyncio
async def test_base_url_env_override(monkeypatch):
    """MINDX_BASE_URL must shape the request URL."""
    monkeypatch.setenv("MINDX_BASE_URL", "http://example.test:9999")
    seen: dict[str, str] = {}

    def _handler(req):
        seen["url"] = str(req.url)
        return httpx.Response(200, json={"hours": 24, "buckets": []})

    transport = httpx.MockTransport(_handler)
    orig = httpx.AsyncClient
    monkeypatch.setattr(
        httpx, "AsyncClient",
        lambda *a, **kw: orig(*a, **{**kw, "transport": transport}),
    )
    await chronos_client.drift()
    assert "example.test:9999" in seen["url"]


# ---- Coach endpoint integration -----------------------------------------


def test_coach_chronos_endpoint_merges_three_calls(monkeypatch):
    """/coach/api/diagnostics/chronos aggregates time + anchors + drift."""

    async def _fake_now(**_kw):
        return {"consensus": "correlated", "confidence_ms": 1.0, "promised_by": "x"}

    async def _fake_anchors(**_kw):
        return {"anchors": [{"id": 1}], "n": 1}

    async def _fake_drift(**_kw):
        return {"hours": 24, "anchor_count": 1, "buckets": []}

    monkeypatch.setattr(chronos_client, "now", _fake_now)
    monkeypatch.setattr(chronos_client, "anchors", _fake_anchors)
    monkeypatch.setattr(chronos_client, "drift", _fake_drift)

    r = client.get("/coach/api/diagnostics/chronos")
    assert r.status_code == 200
    body = r.json()
    assert body["promised_time"]["consensus"] == "correlated"
    assert body["anchor_count"] == 1
    assert body["drift_history"]["anchor_count"] == 1


def test_measurement_confidence_endpoint_returns_band():
    """psutil vs ps cross-check endpoint — should classify on a live host."""
    r = client.get("/coach/api/diagnostics/measurement-confidence")
    assert r.status_code == 200
    body = r.json()
    # Band is one of the four known categories.
    assert body["confidence_band"] in {"tight", "loose", "divergent", "unknown"}
    if body["ok"]:
        # Deltas are non-negative absolute values.
        assert body["cpu_delta_pp"] >= 0
        assert body["rss_delta_mb"] >= 0


def test_cli_samplers_endpoint_returns_sampler_records():
    """When mindX cli_time_samplers.py is reachable, all 6 records come through."""
    r = client.get("/coach/api/diagnostics/cli-samplers")
    assert r.status_code == 200
    body = r.json()
    # Either ok=True (mindX checkout present) or ok=False with a reason.
    assert "ok" in body and "samplers" in body
    if body["ok"]:
        # Six samplers expected.
        assert set(body["samplers"]) == {
            "nano_wall", "ntp_status", "uptime_load",
            "proc_snapshot", "cpu_jiffies", "monotonic_boot",
        }