File size: 6,524 Bytes
2e818da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
211
from __future__ import annotations

import typing

import pytest
from pydantic import ValidationError

from app.observability.contracts import (
    METRIC_ATTRIBUTE_KEYS,
    ExperimentIdentity,
    OperationObservation,
    RetrievalOutcome,
    TelemetryMode,
    metric_attributes,
)


# --- RetrievalOutcome -------------------------------------------------


def test_retrieval_outcome_distinguishes_empty_from_error():
    empty = RetrievalOutcome.success([])
    failed = RetrievalOutcome.failure("collection_unavailable")

    assert empty.status == "success_empty"
    assert empty.empty_result is True
    assert empty.retrieval_error is False

    assert failed.status == "error_fallback"
    assert failed.empty_result is False
    assert failed.retrieval_error is True


def test_retrieval_outcome_success_with_candidates_is_not_empty():
    outcome = RetrievalOutcome.success([{"chunk_id": "c1"}, {"chunk_id": "c2"}])

    assert outcome.status == "success"
    assert outcome.empty_result is False
    assert outcome.retrieval_error is False
    assert outcome.candidates == [{"chunk_id": "c1"}, {"chunk_id": "c2"}]


def test_retrieval_outcome_failure_carries_error_type_and_no_candidates():
    outcome = RetrievalOutcome.failure("timeout")

    assert outcome.error_type == "timeout"
    assert outcome.candidates == []


# --- metric_attributes / METRIC_ATTRIBUTE_KEYS -------------------------


def test_metric_attributes_remove_high_cardinality_values():
    safe = metric_attributes(
        {
            "pipeline.version": "rag-naive-v1",
            "query.category": "methodology",
            "query.id": "q-001",
            "project.id": "private-project",
        }
    )
    assert safe == {
        "pipeline.version": "rag-naive-v1",
        "query.category": "methodology",
    }


def test_metric_attributes_drops_non_scalar_values_even_for_safe_keys():
    safe = metric_attributes(
        {
            "pipeline.version": "rag-naive-v1",
            "operation": ["retrieval", "context_assembly"],
            "status": {"nested": "dict"},
        }
    )
    assert safe == {"pipeline.version": "rag-naive-v1"}


def test_metric_attributes_handles_empty_input():
    assert metric_attributes({}) == {}


def test_metric_attributes_never_mutates_input():
    original = {"pipeline.version": "v1", "query.id": "q-1"}
    snapshot = dict(original)
    metric_attributes(original)
    assert original == snapshot


# --- TelemetryMode -------------------------------------------------


def test_telemetry_mode_allows_only_the_three_canonical_values():
    assert typing.get_args(TelemetryMode) == ("disabled", "local", "full")


# --- ExperimentIdentity -------------------------------------------------


def test_experiment_identity_defaults_to_all_unset():
    identity = ExperimentIdentity()

    assert identity.experiment_id is None
    assert identity.run_id is None
    assert identity.pipeline_version is None
    assert identity.query_set_version is None
    assert identity.corpus_version is None
    assert identity.query_id is None
    assert identity.query_category is None
    assert identity.repetition is None
    assert identity.cache_state is None
    assert identity.git_commit is None
    assert identity.embedding_model is None
    assert identity.cerebras_model is None
    assert identity.chroma_version is None
    assert identity.cognee_version is None


def test_experiment_identity_accepts_all_canonical_fields():
    identity = ExperimentIdentity(
        experiment_id="exp-1",
        run_id="run-1",
        pipeline_version="rag-naive-v1",
        query_set_version="qs-1",
        corpus_version="corpus-1",
        query_id="q-001",
        query_category="methodology",
        repetition=3,
        cache_state="warm",
        git_commit="abc123",
        embedding_model="all-MiniLM-L6-v2",
        cerebras_model="gemma-4-31b",
        chroma_version="0.5.0",
        cognee_version="1.2.2",
    )

    assert identity.pipeline_version == "rag-naive-v1"
    assert identity.repetition == 3


def test_experiment_identity_rejects_unknown_fields():
    with pytest.raises(ValidationError):
        ExperimentIdentity(unknown_field="nope")


# --- OperationObservation -------------------------------------------------


def test_operation_observation_requires_operation_and_status():
    with pytest.raises(ValidationError):
        OperationObservation()


def test_operation_observation_minimal_construction_has_safe_defaults():
    obs = OperationObservation(operation="retrieval", status="success")

    assert obs.operation == "retrieval"
    assert obs.status == "success"
    assert obs.subsystem is None
    assert obs.consumer is None
    assert obs.duration_ms is None
    assert obs.stage_durations_ms == {}
    assert obs.counts == {}
    assert obs.trace_id is None
    assert obs.experiment is None
    assert obs.evaluation_run is False
    assert obs.attributes == {}


def test_operation_observation_carries_stage_durations_counts_and_trace_id():
    obs = OperationObservation(
        operation="retrieval",
        subsystem="chroma",
        consumer="chat",
        status="success",
        duration_ms=123.4,
        stage_durations_ms={"vector_search_ms": 88.0, "collection_lookup_ms": 12.0},
        counts={"raw_candidate_count": 20, "selected_candidate_count": 5},
        trace_id="trace-abc",
    )

    assert obs.stage_durations_ms["vector_search_ms"] == 88.0
    assert obs.counts["selected_candidate_count"] == 5
    assert obs.trace_id == "trace-abc"


def test_operation_observation_can_embed_experiment_identity_and_retrieval_outcome():
    identity = ExperimentIdentity(pipeline_version="rag-naive-v1", query_category="methodology")
    outcome = RetrievalOutcome.success([{"chunk_id": "c1"}])

    obs = OperationObservation(
        operation="retrieval",
        status=outcome.status,
        experiment=identity,
        retrieval=outcome,
        evaluation_run=True,
    )

    assert obs.experiment.pipeline_version == "rag-naive-v1"
    assert obs.retrieval.status == "success"
    assert obs.evaluation_run is True


def test_operation_observation_missing_data_uses_sentinel_not_zero():
    # Per the canonical contract: missing observations must never be silently
    # coerced to zero. A "not instrumented" duration must stay None, not 0.
    obs = OperationObservation(operation="memory", status="not_instrumented")

    assert obs.status == "not_instrumented"
    assert obs.duration_ms is None