omnibase_infra 0.2.8__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.
Files changed (79) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +4 -0
  3. omnibase_infra/enums/enum_declarative_node_violation.py +102 -0
  4. omnibase_infra/event_bus/adapters/__init__.py +31 -0
  5. omnibase_infra/event_bus/adapters/adapter_protocol_event_publisher_kafka.py +517 -0
  6. omnibase_infra/mixins/mixin_async_circuit_breaker.py +113 -1
  7. omnibase_infra/models/__init__.py +9 -0
  8. omnibase_infra/models/event_bus/__init__.py +22 -0
  9. omnibase_infra/models/event_bus/model_consumer_retry_config.py +367 -0
  10. omnibase_infra/models/event_bus/model_dlq_config.py +177 -0
  11. omnibase_infra/models/event_bus/model_idempotency_config.py +131 -0
  12. omnibase_infra/models/event_bus/model_offset_policy_config.py +107 -0
  13. omnibase_infra/models/resilience/model_circuit_breaker_config.py +15 -0
  14. omnibase_infra/models/validation/__init__.py +8 -0
  15. omnibase_infra/models/validation/model_declarative_node_validation_result.py +139 -0
  16. omnibase_infra/models/validation/model_declarative_node_violation.py +169 -0
  17. omnibase_infra/nodes/architecture_validator/__init__.py +28 -7
  18. omnibase_infra/nodes/architecture_validator/constants.py +36 -0
  19. omnibase_infra/nodes/architecture_validator/handlers/__init__.py +28 -0
  20. omnibase_infra/nodes/architecture_validator/handlers/contract.yaml +120 -0
  21. omnibase_infra/nodes/architecture_validator/handlers/handler_architecture_validation.py +359 -0
  22. omnibase_infra/nodes/architecture_validator/node.py +1 -0
  23. omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +48 -336
  24. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +16 -2
  25. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +14 -4
  26. omnibase_infra/nodes/node_ledger_projection_compute/handlers/__init__.py +18 -0
  27. omnibase_infra/nodes/node_ledger_projection_compute/handlers/contract.yaml +53 -0
  28. omnibase_infra/nodes/node_ledger_projection_compute/handlers/handler_ledger_projection.py +354 -0
  29. omnibase_infra/nodes/node_ledger_projection_compute/node.py +20 -256
  30. omnibase_infra/nodes/node_registry_effect/node.py +20 -73
  31. omnibase_infra/protocols/protocol_dispatch_engine.py +90 -0
  32. omnibase_infra/runtime/__init__.py +11 -0
  33. omnibase_infra/runtime/baseline_subscriptions.py +150 -0
  34. omnibase_infra/runtime/event_bus_subcontract_wiring.py +455 -24
  35. omnibase_infra/runtime/kafka_contract_source.py +13 -5
  36. omnibase_infra/runtime/service_message_dispatch_engine.py +112 -0
  37. omnibase_infra/runtime/service_runtime_host_process.py +6 -11
  38. omnibase_infra/services/__init__.py +36 -0
  39. omnibase_infra/services/contract_publisher/__init__.py +95 -0
  40. omnibase_infra/services/contract_publisher/config.py +199 -0
  41. omnibase_infra/services/contract_publisher/errors.py +243 -0
  42. omnibase_infra/services/contract_publisher/models/__init__.py +28 -0
  43. omnibase_infra/services/contract_publisher/models/model_contract_error.py +67 -0
  44. omnibase_infra/services/contract_publisher/models/model_infra_error.py +62 -0
  45. omnibase_infra/services/contract_publisher/models/model_publish_result.py +112 -0
  46. omnibase_infra/services/contract_publisher/models/model_publish_stats.py +79 -0
  47. omnibase_infra/services/contract_publisher/service.py +617 -0
  48. omnibase_infra/services/contract_publisher/sources/__init__.py +52 -0
  49. omnibase_infra/services/contract_publisher/sources/model_discovered.py +155 -0
  50. omnibase_infra/services/contract_publisher/sources/protocol.py +101 -0
  51. omnibase_infra/services/contract_publisher/sources/source_composite.py +309 -0
  52. omnibase_infra/services/contract_publisher/sources/source_filesystem.py +174 -0
  53. omnibase_infra/services/contract_publisher/sources/source_package.py +221 -0
  54. omnibase_infra/services/observability/__init__.py +40 -0
  55. omnibase_infra/services/observability/agent_actions/__init__.py +64 -0
  56. omnibase_infra/services/observability/agent_actions/config.py +209 -0
  57. omnibase_infra/services/observability/agent_actions/consumer.py +1320 -0
  58. omnibase_infra/services/observability/agent_actions/models/__init__.py +87 -0
  59. omnibase_infra/services/observability/agent_actions/models/model_agent_action.py +142 -0
  60. omnibase_infra/services/observability/agent_actions/models/model_detection_failure.py +125 -0
  61. omnibase_infra/services/observability/agent_actions/models/model_envelope.py +85 -0
  62. omnibase_infra/services/observability/agent_actions/models/model_execution_log.py +159 -0
  63. omnibase_infra/services/observability/agent_actions/models/model_performance_metric.py +130 -0
  64. omnibase_infra/services/observability/agent_actions/models/model_routing_decision.py +138 -0
  65. omnibase_infra/services/observability/agent_actions/models/model_transformation_event.py +124 -0
  66. omnibase_infra/services/observability/agent_actions/tests/__init__.py +20 -0
  67. omnibase_infra/services/observability/agent_actions/tests/test_consumer.py +1154 -0
  68. omnibase_infra/services/observability/agent_actions/tests/test_models.py +645 -0
  69. omnibase_infra/services/observability/agent_actions/tests/test_writer.py +709 -0
  70. omnibase_infra/services/observability/agent_actions/writer_postgres.py +926 -0
  71. omnibase_infra/validation/__init__.py +12 -0
  72. omnibase_infra/validation/contracts/declarative_node.validation.yaml +143 -0
  73. omnibase_infra/validation/validation_exemptions.yaml +93 -0
  74. omnibase_infra/validation/validator_declarative_node.py +850 -0
  75. {omnibase_infra-0.2.8.dist-info → omnibase_infra-0.2.9.dist-info}/METADATA +2 -2
  76. {omnibase_infra-0.2.8.dist-info → omnibase_infra-0.2.9.dist-info}/RECORD +79 -27
  77. {omnibase_infra-0.2.8.dist-info → omnibase_infra-0.2.9.dist-info}/WHEEL +0 -0
  78. {omnibase_infra-0.2.8.dist-info → omnibase_infra-0.2.9.dist-info}/entry_points.txt +0 -0
  79. {omnibase_infra-0.2.8.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
+ ]