omnibase_infra 0.2.7__py3-none-any.whl → 0.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/enums/__init__.py +4 -0
- omnibase_infra/enums/enum_declarative_node_violation.py +102 -0
- omnibase_infra/event_bus/adapters/__init__.py +31 -0
- omnibase_infra/event_bus/adapters/adapter_protocol_event_publisher_kafka.py +517 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +113 -1
- omnibase_infra/models/__init__.py +9 -0
- omnibase_infra/models/event_bus/__init__.py +22 -0
- omnibase_infra/models/event_bus/model_consumer_retry_config.py +367 -0
- omnibase_infra/models/event_bus/model_dlq_config.py +177 -0
- omnibase_infra/models/event_bus/model_idempotency_config.py +131 -0
- omnibase_infra/models/event_bus/model_offset_policy_config.py +107 -0
- omnibase_infra/models/resilience/model_circuit_breaker_config.py +15 -0
- omnibase_infra/models/validation/__init__.py +8 -0
- omnibase_infra/models/validation/model_declarative_node_validation_result.py +139 -0
- omnibase_infra/models/validation/model_declarative_node_violation.py +169 -0
- omnibase_infra/nodes/architecture_validator/__init__.py +28 -7
- omnibase_infra/nodes/architecture_validator/constants.py +36 -0
- omnibase_infra/nodes/architecture_validator/handlers/__init__.py +28 -0
- omnibase_infra/nodes/architecture_validator/handlers/contract.yaml +120 -0
- omnibase_infra/nodes/architecture_validator/handlers/handler_architecture_validation.py +359 -0
- omnibase_infra/nodes/architecture_validator/node.py +1 -0
- omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +48 -336
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +16 -2
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +14 -4
- omnibase_infra/nodes/node_ledger_projection_compute/handlers/__init__.py +18 -0
- omnibase_infra/nodes/node_ledger_projection_compute/handlers/contract.yaml +53 -0
- omnibase_infra/nodes/node_ledger_projection_compute/handlers/handler_ledger_projection.py +354 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +20 -256
- omnibase_infra/nodes/node_registry_effect/node.py +20 -73
- omnibase_infra/protocols/protocol_dispatch_engine.py +90 -0
- omnibase_infra/runtime/__init__.py +11 -0
- omnibase_infra/runtime/baseline_subscriptions.py +150 -0
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +455 -24
- omnibase_infra/runtime/kafka_contract_source.py +13 -5
- omnibase_infra/runtime/service_message_dispatch_engine.py +112 -0
- omnibase_infra/runtime/service_runtime_host_process.py +6 -11
- omnibase_infra/services/__init__.py +36 -0
- omnibase_infra/services/contract_publisher/__init__.py +95 -0
- omnibase_infra/services/contract_publisher/config.py +199 -0
- omnibase_infra/services/contract_publisher/errors.py +243 -0
- omnibase_infra/services/contract_publisher/models/__init__.py +28 -0
- omnibase_infra/services/contract_publisher/models/model_contract_error.py +67 -0
- omnibase_infra/services/contract_publisher/models/model_infra_error.py +62 -0
- omnibase_infra/services/contract_publisher/models/model_publish_result.py +112 -0
- omnibase_infra/services/contract_publisher/models/model_publish_stats.py +79 -0
- omnibase_infra/services/contract_publisher/service.py +617 -0
- omnibase_infra/services/contract_publisher/sources/__init__.py +52 -0
- omnibase_infra/services/contract_publisher/sources/model_discovered.py +155 -0
- omnibase_infra/services/contract_publisher/sources/protocol.py +101 -0
- omnibase_infra/services/contract_publisher/sources/source_composite.py +309 -0
- omnibase_infra/services/contract_publisher/sources/source_filesystem.py +174 -0
- omnibase_infra/services/contract_publisher/sources/source_package.py +221 -0
- omnibase_infra/services/observability/__init__.py +40 -0
- omnibase_infra/services/observability/agent_actions/__init__.py +64 -0
- omnibase_infra/services/observability/agent_actions/config.py +209 -0
- omnibase_infra/services/observability/agent_actions/consumer.py +1320 -0
- omnibase_infra/services/observability/agent_actions/models/__init__.py +87 -0
- omnibase_infra/services/observability/agent_actions/models/model_agent_action.py +142 -0
- omnibase_infra/services/observability/agent_actions/models/model_detection_failure.py +125 -0
- omnibase_infra/services/observability/agent_actions/models/model_envelope.py +85 -0
- omnibase_infra/services/observability/agent_actions/models/model_execution_log.py +159 -0
- omnibase_infra/services/observability/agent_actions/models/model_performance_metric.py +130 -0
- omnibase_infra/services/observability/agent_actions/models/model_routing_decision.py +138 -0
- omnibase_infra/services/observability/agent_actions/models/model_transformation_event.py +124 -0
- omnibase_infra/services/observability/agent_actions/tests/__init__.py +20 -0
- omnibase_infra/services/observability/agent_actions/tests/test_consumer.py +1154 -0
- omnibase_infra/services/observability/agent_actions/tests/test_models.py +645 -0
- omnibase_infra/services/observability/agent_actions/tests/test_writer.py +709 -0
- omnibase_infra/services/observability/agent_actions/writer_postgres.py +926 -0
- omnibase_infra/validation/__init__.py +12 -0
- omnibase_infra/validation/contracts/declarative_node.validation.yaml +143 -0
- omnibase_infra/validation/validation_exemptions.yaml +93 -0
- omnibase_infra/validation/validator_declarative_node.py +850 -0
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/RECORD +79 -27
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Unit tests for agent_actions observability models.
|
|
4
|
+
|
|
5
|
+
This module tests model validation behavior:
|
|
6
|
+
- Envelope strict validation (extra="forbid")
|
|
7
|
+
- Payload models allow extras (extra="allow")
|
|
8
|
+
- Type validation (UUID, datetime, dict[str, object])
|
|
9
|
+
- Required vs optional field enforcement
|
|
10
|
+
|
|
11
|
+
Related Tickets:
|
|
12
|
+
- OMN-1743: Migrate agent_actions_consumer to omnibase_infra
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
from pydantic import ValidationError
|
|
22
|
+
|
|
23
|
+
from omnibase_infra.services.observability.agent_actions.models import (
|
|
24
|
+
ModelAgentAction,
|
|
25
|
+
ModelDetectionFailure,
|
|
26
|
+
ModelExecutionLog,
|
|
27
|
+
ModelObservabilityEnvelope,
|
|
28
|
+
ModelPerformanceMetric,
|
|
29
|
+
ModelRoutingDecision,
|
|
30
|
+
ModelTransformationEvent,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# Envelope Strict Validation Tests
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestModelObservabilityEnvelopeStrict:
|
|
39
|
+
"""Test that ModelObservabilityEnvelope has strict validation (extra='forbid')."""
|
|
40
|
+
|
|
41
|
+
def test_envelope_rejects_extra_fields(self) -> None:
|
|
42
|
+
"""Envelope should reject unknown fields with ValidationError."""
|
|
43
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
44
|
+
ModelObservabilityEnvelope(
|
|
45
|
+
event_id=uuid4(),
|
|
46
|
+
event_time=datetime.now(UTC),
|
|
47
|
+
producer_id="test-producer",
|
|
48
|
+
schema_version="1.0.0",
|
|
49
|
+
unknown_field="should_fail", # type: ignore[call-arg]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
errors = exc_info.value.errors()
|
|
53
|
+
assert len(errors) == 1
|
|
54
|
+
assert errors[0]["type"] == "extra_forbidden"
|
|
55
|
+
assert "unknown_field" in str(errors[0]["loc"])
|
|
56
|
+
|
|
57
|
+
def test_envelope_rejects_multiple_extra_fields(self) -> None:
|
|
58
|
+
"""Envelope should reject all unknown fields."""
|
|
59
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
60
|
+
ModelObservabilityEnvelope(
|
|
61
|
+
event_id=uuid4(),
|
|
62
|
+
event_time=datetime.now(UTC),
|
|
63
|
+
producer_id="test-producer",
|
|
64
|
+
schema_version="1.0.0",
|
|
65
|
+
extra1="value1", # type: ignore[call-arg]
|
|
66
|
+
extra2="value2", # type: ignore[call-arg]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
errors = exc_info.value.errors()
|
|
70
|
+
# Multiple extra fields should each produce an error
|
|
71
|
+
assert len(errors) >= 1
|
|
72
|
+
error_types = {e["type"] for e in errors}
|
|
73
|
+
assert "extra_forbidden" in error_types
|
|
74
|
+
|
|
75
|
+
def test_envelope_required_fields_enforced(self) -> None:
|
|
76
|
+
"""Envelope should require all mandatory fields."""
|
|
77
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
78
|
+
ModelObservabilityEnvelope() # type: ignore[call-arg]
|
|
79
|
+
|
|
80
|
+
errors = exc_info.value.errors()
|
|
81
|
+
error_locs = {e["loc"][0] for e in errors}
|
|
82
|
+
assert "event_id" in error_locs
|
|
83
|
+
assert "event_time" in error_locs
|
|
84
|
+
assert "producer_id" in error_locs
|
|
85
|
+
assert "schema_version" in error_locs
|
|
86
|
+
|
|
87
|
+
def test_envelope_optional_correlation_id(self) -> None:
|
|
88
|
+
"""Envelope should allow correlation_id to be omitted."""
|
|
89
|
+
envelope = ModelObservabilityEnvelope(
|
|
90
|
+
event_id=uuid4(),
|
|
91
|
+
event_time=datetime.now(UTC),
|
|
92
|
+
producer_id="test-producer",
|
|
93
|
+
schema_version="1.0.0",
|
|
94
|
+
)
|
|
95
|
+
assert envelope.correlation_id is None
|
|
96
|
+
|
|
97
|
+
def test_envelope_accepts_valid_correlation_id(self) -> None:
|
|
98
|
+
"""Envelope should accept a valid UUID correlation_id."""
|
|
99
|
+
cid = uuid4()
|
|
100
|
+
envelope = ModelObservabilityEnvelope(
|
|
101
|
+
event_id=uuid4(),
|
|
102
|
+
event_time=datetime.now(UTC),
|
|
103
|
+
producer_id="test-producer",
|
|
104
|
+
schema_version="1.0.0",
|
|
105
|
+
correlation_id=cid,
|
|
106
|
+
)
|
|
107
|
+
assert envelope.correlation_id == cid
|
|
108
|
+
|
|
109
|
+
def test_envelope_is_frozen(self) -> None:
|
|
110
|
+
"""Envelope should be immutable after creation."""
|
|
111
|
+
envelope = ModelObservabilityEnvelope(
|
|
112
|
+
event_id=uuid4(),
|
|
113
|
+
event_time=datetime.now(UTC),
|
|
114
|
+
producer_id="test-producer",
|
|
115
|
+
schema_version="1.0.0",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
with pytest.raises(ValidationError):
|
|
119
|
+
envelope.producer_id = "new-producer" # type: ignore[misc]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# =============================================================================
|
|
123
|
+
# Payload Models Allow Extras Tests
|
|
124
|
+
# =============================================================================
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestModelAgentActionExtrasAllowed:
|
|
128
|
+
"""Test that ModelAgentAction allows extra fields (extra='allow')."""
|
|
129
|
+
|
|
130
|
+
def test_agent_action_accepts_extra_fields(self) -> None:
|
|
131
|
+
"""Agent action should accept and preserve extra fields."""
|
|
132
|
+
action = ModelAgentAction( # type: ignore[call-arg]
|
|
133
|
+
id=uuid4(),
|
|
134
|
+
correlation_id=uuid4(),
|
|
135
|
+
agent_name="test-agent",
|
|
136
|
+
action_type="tool_call",
|
|
137
|
+
action_name="Read",
|
|
138
|
+
created_at=datetime.now(UTC),
|
|
139
|
+
custom_field="extra_value", # Extra field - should be allowed
|
|
140
|
+
another_extra=123, # Another extra field
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Extra fields should be accessible via model_extra
|
|
144
|
+
assert action.model_extra is not None
|
|
145
|
+
assert action.model_extra.get("custom_field") == "extra_value"
|
|
146
|
+
assert action.model_extra.get("another_extra") == 123
|
|
147
|
+
|
|
148
|
+
def test_agent_action_required_fields_enforced(self) -> None:
|
|
149
|
+
"""Agent action should still enforce required fields."""
|
|
150
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
151
|
+
ModelAgentAction() # type: ignore[call-arg]
|
|
152
|
+
|
|
153
|
+
errors = exc_info.value.errors()
|
|
154
|
+
error_locs = {e["loc"][0] for e in errors}
|
|
155
|
+
assert "id" in error_locs
|
|
156
|
+
assert "correlation_id" in error_locs
|
|
157
|
+
assert "agent_name" in error_locs
|
|
158
|
+
assert "action_type" in error_locs
|
|
159
|
+
assert "action_name" in error_locs
|
|
160
|
+
assert "created_at" in error_locs
|
|
161
|
+
|
|
162
|
+
def test_agent_action_optional_fields_work(self) -> None:
|
|
163
|
+
"""Agent action optional fields should default to None."""
|
|
164
|
+
action = ModelAgentAction(
|
|
165
|
+
id=uuid4(),
|
|
166
|
+
correlation_id=uuid4(),
|
|
167
|
+
agent_name="test-agent",
|
|
168
|
+
action_type="tool_call",
|
|
169
|
+
action_name="Read",
|
|
170
|
+
created_at=datetime.now(UTC),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
assert action.status is None
|
|
174
|
+
assert action.duration_ms is None
|
|
175
|
+
assert action.result is None
|
|
176
|
+
assert action.error_message is None
|
|
177
|
+
assert action.metadata is None
|
|
178
|
+
assert action.raw_payload is None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TestModelRoutingDecisionExtrasAllowed:
|
|
182
|
+
"""Test that ModelRoutingDecision allows extra fields."""
|
|
183
|
+
|
|
184
|
+
def test_routing_decision_accepts_extra_fields(self) -> None:
|
|
185
|
+
"""Routing decision should accept and preserve extra fields."""
|
|
186
|
+
decision = ModelRoutingDecision( # type: ignore[call-arg]
|
|
187
|
+
id=uuid4(),
|
|
188
|
+
correlation_id=uuid4(),
|
|
189
|
+
selected_agent="api-architect",
|
|
190
|
+
confidence_score=0.95,
|
|
191
|
+
created_at=datetime.now(UTC),
|
|
192
|
+
custom_routing_field="allowed",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
assert decision.model_extra is not None
|
|
196
|
+
assert decision.model_extra.get("custom_routing_field") == "allowed"
|
|
197
|
+
|
|
198
|
+
def test_routing_decision_required_fields_enforced(self) -> None:
|
|
199
|
+
"""Routing decision should enforce required fields."""
|
|
200
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
201
|
+
ModelRoutingDecision() # type: ignore[call-arg]
|
|
202
|
+
|
|
203
|
+
errors = exc_info.value.errors()
|
|
204
|
+
error_locs = {e["loc"][0] for e in errors}
|
|
205
|
+
assert "id" in error_locs
|
|
206
|
+
assert "correlation_id" in error_locs
|
|
207
|
+
assert "selected_agent" in error_locs
|
|
208
|
+
assert "confidence_score" in error_locs
|
|
209
|
+
assert "created_at" in error_locs
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestModelTransformationEventExtrasAllowed:
|
|
213
|
+
"""Test that ModelTransformationEvent allows extra fields."""
|
|
214
|
+
|
|
215
|
+
def test_transformation_event_accepts_extra_fields(self) -> None:
|
|
216
|
+
"""Transformation event should accept and preserve extra fields."""
|
|
217
|
+
event = ModelTransformationEvent( # type: ignore[call-arg]
|
|
218
|
+
id=uuid4(),
|
|
219
|
+
correlation_id=uuid4(),
|
|
220
|
+
source_agent="polymorphic-agent",
|
|
221
|
+
target_agent="api-architect",
|
|
222
|
+
created_at=datetime.now(UTC),
|
|
223
|
+
extra_transform_data={"key": "value"},
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
assert event.model_extra is not None
|
|
227
|
+
assert event.model_extra.get("extra_transform_data") == {"key": "value"}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TestModelPerformanceMetricExtrasAllowed:
|
|
231
|
+
"""Test that ModelPerformanceMetric allows extra fields."""
|
|
232
|
+
|
|
233
|
+
def test_performance_metric_accepts_extra_fields(self) -> None:
|
|
234
|
+
"""Performance metric should accept and preserve extra fields."""
|
|
235
|
+
metric = ModelPerformanceMetric( # type: ignore[call-arg]
|
|
236
|
+
id=uuid4(),
|
|
237
|
+
metric_name="routing_latency_ms",
|
|
238
|
+
metric_value=45.2,
|
|
239
|
+
created_at=datetime.now(UTC),
|
|
240
|
+
extra_metric_tag="custom_tag",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
assert metric.model_extra is not None
|
|
244
|
+
assert metric.model_extra.get("extra_metric_tag") == "custom_tag"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class TestModelDetectionFailureExtrasAllowed:
|
|
248
|
+
"""Test that ModelDetectionFailure allows extra fields."""
|
|
249
|
+
|
|
250
|
+
def test_detection_failure_accepts_extra_fields(self) -> None:
|
|
251
|
+
"""Detection failure should accept and preserve extra fields."""
|
|
252
|
+
failure = ModelDetectionFailure( # type: ignore[call-arg]
|
|
253
|
+
correlation_id=uuid4(),
|
|
254
|
+
failure_reason="No matching pattern",
|
|
255
|
+
created_at=datetime.now(UTC),
|
|
256
|
+
debug_info="extra debugging data",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
assert failure.model_extra is not None
|
|
260
|
+
assert failure.model_extra.get("debug_info") == "extra debugging data"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class TestModelExecutionLogExtrasAllowed:
|
|
264
|
+
"""Test that ModelExecutionLog allows extra fields."""
|
|
265
|
+
|
|
266
|
+
def test_execution_log_accepts_extra_fields(self) -> None:
|
|
267
|
+
"""Execution log should accept and preserve extra fields."""
|
|
268
|
+
log = ModelExecutionLog( # type: ignore[call-arg]
|
|
269
|
+
execution_id=uuid4(),
|
|
270
|
+
correlation_id=uuid4(),
|
|
271
|
+
agent_name="testing",
|
|
272
|
+
status="completed",
|
|
273
|
+
created_at=datetime.now(UTC),
|
|
274
|
+
updated_at=datetime.now(UTC),
|
|
275
|
+
custom_log_field=42,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
assert log.model_extra is not None
|
|
279
|
+
assert log.model_extra.get("custom_log_field") == 42
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# =============================================================================
|
|
283
|
+
# Type Validation Tests
|
|
284
|
+
# =============================================================================
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestUUIDValidation:
|
|
288
|
+
"""Test UUID field validation across models."""
|
|
289
|
+
|
|
290
|
+
def test_uuid_accepts_valid_uuid(self) -> None:
|
|
291
|
+
"""UUID fields should accept valid UUID objects."""
|
|
292
|
+
uid = uuid4()
|
|
293
|
+
action = ModelAgentAction(
|
|
294
|
+
id=uid,
|
|
295
|
+
correlation_id=uuid4(),
|
|
296
|
+
agent_name="test-agent",
|
|
297
|
+
action_type="tool_call",
|
|
298
|
+
action_name="Read",
|
|
299
|
+
created_at=datetime.now(UTC),
|
|
300
|
+
)
|
|
301
|
+
assert action.id == uid
|
|
302
|
+
|
|
303
|
+
def test_uuid_accepts_string_uuid(self) -> None:
|
|
304
|
+
"""UUID fields should accept valid UUID strings."""
|
|
305
|
+
uid_str = str(uuid4())
|
|
306
|
+
action = ModelAgentAction(
|
|
307
|
+
id=uid_str, # type: ignore[arg-type]
|
|
308
|
+
correlation_id=uuid4(),
|
|
309
|
+
agent_name="test-agent",
|
|
310
|
+
action_type="tool_call",
|
|
311
|
+
action_name="Read",
|
|
312
|
+
created_at=datetime.now(UTC),
|
|
313
|
+
)
|
|
314
|
+
assert str(action.id) == uid_str
|
|
315
|
+
|
|
316
|
+
def test_uuid_rejects_invalid_string(self) -> None:
|
|
317
|
+
"""UUID fields should reject invalid UUID strings."""
|
|
318
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
319
|
+
ModelAgentAction(
|
|
320
|
+
id="not-a-uuid", # type: ignore[arg-type]
|
|
321
|
+
correlation_id=uuid4(),
|
|
322
|
+
agent_name="test-agent",
|
|
323
|
+
action_type="tool_call",
|
|
324
|
+
action_name="Read",
|
|
325
|
+
created_at=datetime.now(UTC),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
errors = exc_info.value.errors()
|
|
329
|
+
assert any(e["loc"] == ("id",) for e in errors)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class TestDatetimeValidation:
|
|
333
|
+
"""Test datetime field validation across models."""
|
|
334
|
+
|
|
335
|
+
def test_datetime_accepts_utc_datetime(self) -> None:
|
|
336
|
+
"""Datetime fields should accept UTC datetime objects."""
|
|
337
|
+
now = datetime.now(UTC)
|
|
338
|
+
action = ModelAgentAction(
|
|
339
|
+
id=uuid4(),
|
|
340
|
+
correlation_id=uuid4(),
|
|
341
|
+
agent_name="test-agent",
|
|
342
|
+
action_type="tool_call",
|
|
343
|
+
action_name="Read",
|
|
344
|
+
created_at=now,
|
|
345
|
+
)
|
|
346
|
+
assert action.created_at == now
|
|
347
|
+
|
|
348
|
+
def test_datetime_accepts_iso_string(self) -> None:
|
|
349
|
+
"""Datetime fields should accept valid ISO format strings."""
|
|
350
|
+
now = datetime.now(UTC)
|
|
351
|
+
iso_str = now.isoformat()
|
|
352
|
+
action = ModelAgentAction(
|
|
353
|
+
id=uuid4(),
|
|
354
|
+
correlation_id=uuid4(),
|
|
355
|
+
agent_name="test-agent",
|
|
356
|
+
action_type="tool_call",
|
|
357
|
+
action_name="Read",
|
|
358
|
+
created_at=iso_str, # type: ignore[arg-type]
|
|
359
|
+
)
|
|
360
|
+
assert action.created_at is not None
|
|
361
|
+
|
|
362
|
+
def test_datetime_rejects_invalid_string(self) -> None:
|
|
363
|
+
"""Datetime fields should reject invalid datetime strings."""
|
|
364
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
365
|
+
ModelAgentAction(
|
|
366
|
+
id=uuid4(),
|
|
367
|
+
correlation_id=uuid4(),
|
|
368
|
+
agent_name="test-agent",
|
|
369
|
+
action_type="tool_call",
|
|
370
|
+
action_name="Read",
|
|
371
|
+
created_at="not-a-datetime", # type: ignore[arg-type]
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
errors = exc_info.value.errors()
|
|
375
|
+
assert any(e["loc"] == ("created_at",) for e in errors)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class TestRawPayloadValidation:
|
|
379
|
+
"""Test raw_payload field validation (dict[str, object])."""
|
|
380
|
+
|
|
381
|
+
def test_raw_payload_accepts_dict(self) -> None:
|
|
382
|
+
"""raw_payload should accept dict[str, object]."""
|
|
383
|
+
payload = {"key": "value", "number": 123, "nested": {"a": 1}}
|
|
384
|
+
action = ModelAgentAction(
|
|
385
|
+
id=uuid4(),
|
|
386
|
+
correlation_id=uuid4(),
|
|
387
|
+
agent_name="test-agent",
|
|
388
|
+
action_type="tool_call",
|
|
389
|
+
action_name="Read",
|
|
390
|
+
created_at=datetime.now(UTC),
|
|
391
|
+
raw_payload=payload,
|
|
392
|
+
)
|
|
393
|
+
assert action.raw_payload == payload
|
|
394
|
+
|
|
395
|
+
def test_raw_payload_accepts_none(self) -> None:
|
|
396
|
+
"""raw_payload should accept None."""
|
|
397
|
+
action = ModelAgentAction(
|
|
398
|
+
id=uuid4(),
|
|
399
|
+
correlation_id=uuid4(),
|
|
400
|
+
agent_name="test-agent",
|
|
401
|
+
action_type="tool_call",
|
|
402
|
+
action_name="Read",
|
|
403
|
+
created_at=datetime.now(UTC),
|
|
404
|
+
raw_payload=None,
|
|
405
|
+
)
|
|
406
|
+
assert action.raw_payload is None
|
|
407
|
+
|
|
408
|
+
def test_raw_payload_accepts_complex_nested_dict(self) -> None:
|
|
409
|
+
"""raw_payload should accept deeply nested structures."""
|
|
410
|
+
payload = {
|
|
411
|
+
"level1": {
|
|
412
|
+
"level2": {
|
|
413
|
+
"level3": [1, 2, {"deep": "value"}],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
"array": [1, "two", 3.0, True, None],
|
|
417
|
+
}
|
|
418
|
+
action = ModelAgentAction(
|
|
419
|
+
id=uuid4(),
|
|
420
|
+
correlation_id=uuid4(),
|
|
421
|
+
agent_name="test-agent",
|
|
422
|
+
action_type="tool_call",
|
|
423
|
+
action_name="Read",
|
|
424
|
+
created_at=datetime.now(UTC),
|
|
425
|
+
raw_payload=payload,
|
|
426
|
+
)
|
|
427
|
+
assert action.raw_payload == payload
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class TestMetadataValidation:
|
|
431
|
+
"""Test metadata field validation (dict[str, object])."""
|
|
432
|
+
|
|
433
|
+
def test_metadata_accepts_dict(self) -> None:
|
|
434
|
+
"""metadata should accept dict[str, object]."""
|
|
435
|
+
metadata = {"tool": "Read", "file": "/path/to/file.py"}
|
|
436
|
+
action = ModelAgentAction(
|
|
437
|
+
id=uuid4(),
|
|
438
|
+
correlation_id=uuid4(),
|
|
439
|
+
agent_name="test-agent",
|
|
440
|
+
action_type="tool_call",
|
|
441
|
+
action_name="Read",
|
|
442
|
+
created_at=datetime.now(UTC),
|
|
443
|
+
metadata=metadata,
|
|
444
|
+
)
|
|
445
|
+
assert action.metadata == metadata
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# =============================================================================
|
|
449
|
+
# Model-Specific Validation Tests
|
|
450
|
+
# =============================================================================
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class TestModelAgentActionSpecific:
|
|
454
|
+
"""Model-specific tests for ModelAgentAction."""
|
|
455
|
+
|
|
456
|
+
def test_agent_action_with_all_optional_fields(self) -> None:
|
|
457
|
+
"""Agent action should work with all optional fields populated."""
|
|
458
|
+
now = datetime.now(UTC)
|
|
459
|
+
action = ModelAgentAction(
|
|
460
|
+
id=uuid4(),
|
|
461
|
+
correlation_id=uuid4(),
|
|
462
|
+
agent_name="test-agent",
|
|
463
|
+
action_type="tool_call",
|
|
464
|
+
action_name="Bash",
|
|
465
|
+
created_at=now,
|
|
466
|
+
status="completed",
|
|
467
|
+
duration_ms=1500,
|
|
468
|
+
result="Success",
|
|
469
|
+
error_message=None,
|
|
470
|
+
metadata={"command": "ls -la"},
|
|
471
|
+
raw_payload={"full": "payload"},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
assert action.status == "completed"
|
|
475
|
+
assert action.duration_ms == 1500
|
|
476
|
+
assert action.result == "Success"
|
|
477
|
+
assert action.metadata == {"command": "ls -la"}
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class TestModelRoutingDecisionSpecific:
|
|
481
|
+
"""Model-specific tests for ModelRoutingDecision."""
|
|
482
|
+
|
|
483
|
+
def test_routing_decision_confidence_score_float(self) -> None:
|
|
484
|
+
"""Confidence score should accept float values."""
|
|
485
|
+
decision = ModelRoutingDecision(
|
|
486
|
+
id=uuid4(),
|
|
487
|
+
correlation_id=uuid4(),
|
|
488
|
+
selected_agent="api-architect",
|
|
489
|
+
confidence_score=0.875,
|
|
490
|
+
created_at=datetime.now(UTC),
|
|
491
|
+
)
|
|
492
|
+
assert decision.confidence_score == 0.875
|
|
493
|
+
|
|
494
|
+
def test_routing_decision_alternatives_list(self) -> None:
|
|
495
|
+
"""Alternatives should accept list of strings."""
|
|
496
|
+
decision = ModelRoutingDecision(
|
|
497
|
+
id=uuid4(),
|
|
498
|
+
correlation_id=uuid4(),
|
|
499
|
+
selected_agent="api-architect",
|
|
500
|
+
confidence_score=0.95,
|
|
501
|
+
created_at=datetime.now(UTC),
|
|
502
|
+
alternatives=["testing", "debug", "code-reviewer"],
|
|
503
|
+
)
|
|
504
|
+
assert decision.alternatives == ["testing", "debug", "code-reviewer"]
|
|
505
|
+
|
|
506
|
+
def test_routing_decision_rejects_confidence_score_above_one(self) -> None:
|
|
507
|
+
"""Confidence score above 1.0 should be rejected."""
|
|
508
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
509
|
+
ModelRoutingDecision(
|
|
510
|
+
id=uuid4(),
|
|
511
|
+
correlation_id=uuid4(),
|
|
512
|
+
selected_agent="test-agent",
|
|
513
|
+
confidence_score=1.5, # Invalid - above 1.0
|
|
514
|
+
created_at=datetime.now(UTC),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
errors = exc_info.value.errors()
|
|
518
|
+
assert any(e["loc"] == ("confidence_score",) for e in errors)
|
|
519
|
+
assert any(e["type"] == "less_than_equal" for e in errors)
|
|
520
|
+
|
|
521
|
+
def test_routing_decision_rejects_confidence_score_below_zero(self) -> None:
|
|
522
|
+
"""Confidence score below 0.0 should be rejected."""
|
|
523
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
524
|
+
ModelRoutingDecision(
|
|
525
|
+
id=uuid4(),
|
|
526
|
+
correlation_id=uuid4(),
|
|
527
|
+
selected_agent="test-agent",
|
|
528
|
+
confidence_score=-0.1, # Invalid - below 0.0
|
|
529
|
+
created_at=datetime.now(UTC),
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
errors = exc_info.value.errors()
|
|
533
|
+
assert any(e["loc"] == ("confidence_score",) for e in errors)
|
|
534
|
+
assert any(e["type"] == "greater_than_equal" for e in errors)
|
|
535
|
+
|
|
536
|
+
def test_routing_decision_accepts_boundary_confidence_scores(self) -> None:
|
|
537
|
+
"""Confidence score at boundaries (0.0 and 1.0) should be accepted."""
|
|
538
|
+
# Test lower boundary
|
|
539
|
+
decision_zero = ModelRoutingDecision(
|
|
540
|
+
id=uuid4(),
|
|
541
|
+
correlation_id=uuid4(),
|
|
542
|
+
selected_agent="test-agent",
|
|
543
|
+
confidence_score=0.0, # Valid - exactly 0.0
|
|
544
|
+
created_at=datetime.now(UTC),
|
|
545
|
+
)
|
|
546
|
+
assert decision_zero.confidence_score == 0.0
|
|
547
|
+
|
|
548
|
+
# Test upper boundary
|
|
549
|
+
decision_one = ModelRoutingDecision(
|
|
550
|
+
id=uuid4(),
|
|
551
|
+
correlation_id=uuid4(),
|
|
552
|
+
selected_agent="test-agent",
|
|
553
|
+
confidence_score=1.0, # Valid - exactly 1.0
|
|
554
|
+
created_at=datetime.now(UTC),
|
|
555
|
+
)
|
|
556
|
+
assert decision_one.confidence_score == 1.0
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class TestModelExecutionLogSpecific:
|
|
560
|
+
"""Model-specific tests for ModelExecutionLog."""
|
|
561
|
+
|
|
562
|
+
def test_execution_log_requires_both_timestamps(self) -> None:
|
|
563
|
+
"""Execution log should require both created_at and updated_at."""
|
|
564
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
565
|
+
ModelExecutionLog(
|
|
566
|
+
execution_id=uuid4(),
|
|
567
|
+
correlation_id=uuid4(),
|
|
568
|
+
agent_name="testing",
|
|
569
|
+
status="running",
|
|
570
|
+
created_at=datetime.now(UTC),
|
|
571
|
+
# missing updated_at
|
|
572
|
+
) # type: ignore[call-arg]
|
|
573
|
+
|
|
574
|
+
errors = exc_info.value.errors()
|
|
575
|
+
error_locs = {e["loc"][0] for e in errors}
|
|
576
|
+
assert "updated_at" in error_locs
|
|
577
|
+
|
|
578
|
+
def test_execution_log_lifecycle_tracking_fields(self) -> None:
|
|
579
|
+
"""Execution log should support lifecycle tracking fields."""
|
|
580
|
+
now = datetime.now(UTC)
|
|
581
|
+
log = ModelExecutionLog(
|
|
582
|
+
execution_id=uuid4(),
|
|
583
|
+
correlation_id=uuid4(),
|
|
584
|
+
agent_name="testing",
|
|
585
|
+
status="completed",
|
|
586
|
+
created_at=now,
|
|
587
|
+
updated_at=now,
|
|
588
|
+
started_at=now,
|
|
589
|
+
completed_at=now,
|
|
590
|
+
duration_ms=5000,
|
|
591
|
+
exit_code=0,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
assert log.started_at == now
|
|
595
|
+
assert log.completed_at == now
|
|
596
|
+
assert log.duration_ms == 5000
|
|
597
|
+
assert log.exit_code == 0
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class TestModelDetectionFailureSpecific:
|
|
601
|
+
"""Model-specific tests for ModelDetectionFailure."""
|
|
602
|
+
|
|
603
|
+
def test_detection_failure_correlation_as_idempotency_key(self) -> None:
|
|
604
|
+
"""Detection failure uses correlation_id as idempotency key (not separate id)."""
|
|
605
|
+
cid = uuid4()
|
|
606
|
+
failure = ModelDetectionFailure(
|
|
607
|
+
correlation_id=cid,
|
|
608
|
+
failure_reason="No pattern matched",
|
|
609
|
+
created_at=datetime.now(UTC),
|
|
610
|
+
)
|
|
611
|
+
# No 'id' field - correlation_id serves as the key
|
|
612
|
+
assert failure.correlation_id == cid
|
|
613
|
+
|
|
614
|
+
def test_detection_failure_attempted_patterns(self) -> None:
|
|
615
|
+
"""Detection failure should accept list of attempted patterns."""
|
|
616
|
+
failure = ModelDetectionFailure(
|
|
617
|
+
correlation_id=uuid4(),
|
|
618
|
+
failure_reason="Low confidence scores",
|
|
619
|
+
created_at=datetime.now(UTC),
|
|
620
|
+
attempted_patterns=["code-review", "testing", "infrastructure"],
|
|
621
|
+
)
|
|
622
|
+
assert failure.attempted_patterns == [
|
|
623
|
+
"code-review",
|
|
624
|
+
"testing",
|
|
625
|
+
"infrastructure",
|
|
626
|
+
]
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
__all__ = [
|
|
630
|
+
"TestModelObservabilityEnvelopeStrict",
|
|
631
|
+
"TestModelAgentActionExtrasAllowed",
|
|
632
|
+
"TestModelRoutingDecisionExtrasAllowed",
|
|
633
|
+
"TestModelTransformationEventExtrasAllowed",
|
|
634
|
+
"TestModelPerformanceMetricExtrasAllowed",
|
|
635
|
+
"TestModelDetectionFailureExtrasAllowed",
|
|
636
|
+
"TestModelExecutionLogExtrasAllowed",
|
|
637
|
+
"TestUUIDValidation",
|
|
638
|
+
"TestDatetimeValidation",
|
|
639
|
+
"TestRawPayloadValidation",
|
|
640
|
+
"TestMetadataValidation",
|
|
641
|
+
"TestModelAgentActionSpecific",
|
|
642
|
+
"TestModelRoutingDecisionSpecific",
|
|
643
|
+
"TestModelExecutionLogSpecific",
|
|
644
|
+
"TestModelDetectionFailureSpecific",
|
|
645
|
+
]
|