File size: 7,684 Bytes
b5df635
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
963e554
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Phase 3 tests: tool registry permissions, audit completeness, allowlist."""

from __future__ import annotations

import json
import time

import pytest
from pydantic import BaseModel

from sensorflow.retro import store as retro_store
from sensorflow.retro.tools.registry import ToolPermissionError, ToolRegistry


class _EchoIn(BaseModel):
    msg: str


class _EchoOut(BaseModel):
    echoed: str


def test_read_only_is_the_default(registry):
    specs = {s.name: s for s in registry.specs()}
    assert specs["log_reader"].read_only is True
    assert specs["safety_standard_rag"].read_only is True
    assert specs["metric_calculator"].read_only is True
    assert specs["historical_failure_search"].read_only is True
    assert specs["distribution_analysis"].read_only is True
    # exactly one write tool exists
    write_tools = [s for s in specs.values() if not s.read_only]
    assert [t.name for t in write_tools] == ["create_evaluation_case"]


def test_write_tool_requires_explicit_authorization(registry, retro_root):
    args = {"title": "Rain-night pedestrian case",
            "description": "derived from retrospective EVAL-2026-0802-MP01",
            "scenario_tags": ["rain", "night", "pedestrian"],
            "source_evaluation_id": "EVAL-2026-0802-MP01"}
    with pytest.raises(ToolPermissionError):
        registry.call("create_evaluation_case", args)
    denied = [r for r in registry.audit_log if r.status == "denied"]
    assert denied and denied[0].tool == "create_evaluation_case"

    res = registry.call("create_evaluation_case", args, policy_authorization=True)
    assert res.ok
    case_path = res.result["path"]
    assert json.loads(open(case_path).read())["title"] == args["title"]
    authorized = [r for r in registry.audit_log
                  if r.tool == "create_evaluation_case" and r.status == "ok"]
    assert authorized and authorized[0].authorized_write is True


def test_audit_completeness(registry, retro_root):
    registry.call("safety_standard_rag", {"query": "phantom braking", "k": 2})
    with pytest.raises(Exception):
        registry.call("metric_calculator", {"operation": "nope", "params": {}})
    with pytest.raises(KeyError):
        registry.call("not_a_tool", {})

    # every call — ok, error, unknown — is audited with args + timestamp
    assert len(registry.audit_log) == 3
    for rec in registry.audit_log:
        assert rec.timestamp and rec.args is not None and rec.call_id
    ok = registry.audit_log[0]
    assert ok.status == "ok" and len(ok.result_hash) == 64
    assert registry.audit_log[1].status == "error"
    assert registry.audit_log[2].status == "error"

    # persisted to the isolated runs root as jsonl
    persisted = retro_store.read_audit("test-analysis")
    assert len(persisted) == 3


def test_result_hash_is_deterministic(registry, retro_root):
    r1 = registry.call("metric_calculator", {
        "operation": "stopping_distance", "params": {"velocity_mps": 10.0}})
    r2 = registry.call("metric_calculator", {
        "operation": "stopping_distance", "params": {"velocity_mps": 10.0}})
    hashes = [rec.result_hash for rec in registry.audit_log if rec.status == "ok"]
    assert hashes[0] == hashes[1]
    assert r1.result == r2.result


def test_path_allowlist_enforced(registry, tmp_path):
    secret = tmp_path / "secret.json"
    secret.write_text("{}")
    with pytest.raises(PermissionError, match="allowlist"):
        registry.call("log_reader", {"path": str(secret)})
    with pytest.raises(PermissionError):
        registry.call("log_reader", {"fixture_id": "../../../etc/passwd"})
    # uploads dir IS allowed
    up = retro_store.uploads_dir() / "ok.json"
    up.write_text(json.dumps({"evaluation_id": "X", "ground_truth": {"class": "car"}}))
    res = registry.call("log_reader", {"path": str(up)})
    assert res.ok and res.result["log"]["evaluation_id"] == "X"


def test_timeout_enforced(retro_root):
    reg = ToolRegistry(analysis_id="timeout-test")

    def slow(_inp: _EchoIn) -> _EchoOut:
        time.sleep(1.0)
        return _EchoOut(echoed="late")

    reg.register("slow_echo", slow, _EchoIn, _EchoOut, "slow", timeout_s=0.1)
    with pytest.raises(TimeoutError):
        reg.call("slow_echo", {"msg": "hi"})
    assert reg.audit_log[-1].status == "timeout"


def test_error_behavior_return_error(retro_root):
    reg = ToolRegistry(analysis_id="errbehavior-test")

    def boom(_inp: _EchoIn) -> _EchoOut:
        raise RuntimeError("kaput")

    reg.register("boom", boom, _EchoIn, _EchoOut, "explodes",
                 error_behavior="return_error")
    res = reg.call("boom", {"msg": "x"})
    assert not res.ok and "kaput" in res.error
    assert reg.audit_log[-1].status == "error"


def test_tool_schemas_declared(registry):
    for spec in registry.specs():
        assert spec.input_schema.get("properties") is not None
        assert spec.output_schema.get("properties") is not None
        assert spec.timeout_s > 0
        assert spec.description


# ---------------------------------------------- megaeval delegation (real data)

@pytest.fixture(scope="module")
def mega_run(tmp_path_factory):
    """A real, published megaeval run in an isolated store (public API only)."""
    from sensorflow.megaeval import population as pop_mod
    from sensorflow.megaeval.runs import get_mega_store, reset_mega_store

    root = tmp_path_factory.mktemp("retro-megaeval")
    pop_mod.set_mega_root(str(root))
    reset_mega_store()
    # 20k objects: large enough that skewed cohorts clear distribution_shift's
    # default min_eval_count=300 / rel_threshold=0.35 (8k leaves zero eligible)
    meta = pop_mod.generate_population("retro-delegation-pop",
                                       num_objects=20_000, seed=23)
    store = get_mega_store()
    run = store.create_run(population_id=meta["population_id"],
                           model_version="retro-delegation-model",
                           worker_delay_s=0.0)
    store.execute_sync(run)
    assert run.status == "published", run.error

    yield run

    pop_mod.set_mega_root("runs/megaeval")
    reset_mega_store()


def test_distribution_analysis_delegates_to_megaeval(registry, mega_run):
    """The SUCCESS path: import + call chain into megaeval actually works."""
    res = registry.call("distribution_analysis", {"run_id": mega_run.run_id})
    assert res.ok
    out = res.result
    # delegation succeeded — not the graceful-degradation branch
    assert out["source"] == "sensorflow.megaeval.analysis.distribution_shift"
    assert out["shift"]["run_id"] == mega_run.run_id
    assert "train mix vs eval mix" in out["shift"]["method"]
    # the synthetic training mix is deliberately skewed (night/rain cohorts),
    # so a real run surfaces genuine shifted cohorts
    shifts = out["shift"]["shifts"]
    assert len(shifts) > 0
    for s in shifts:
        assert {"cohort", "train_share", "eval_share",
                "relative_change"} <= set(s)
    assert any("cohort" in f for f in out["findings"])
    # the call is audited like any other tool call
    audited = [r for r in registry.audit_log
               if r.tool == "distribution_analysis" and r.status == "ok"]
    assert audited and len(audited[0].result_hash) == 64


def test_distribution_analysis_degrades_for_unknown_run(registry, mega_run):
    """Graceful degradation is reserved for genuinely missing data."""
    res = registry.call("distribution_analysis", {"run_id": "eval-nonexistent"})
    assert res.ok
    assert res.result["source"] == "megaeval-delegation-failed"
    assert res.result["shift"] is None
    assert "no distribution claim is made" in res.result["findings"][0]