S-Dreamer commited on
Commit
302b72d
·
verified ·
1 Parent(s): 9cb16b8

Upload 5 files

Browse files
tests/test_drift.py CHANGED
@@ -19,6 +19,7 @@ from __future__ import annotations
19
 
20
  import copy
21
  from dataclasses import asdict
 
22
 
23
  import pytest
24
 
@@ -36,8 +37,8 @@ from osint_core.drift import (
36
  )
37
 
38
 
39
- def make_telemetry(**overrides):
40
- data = {
41
  "run_id": "run_test_001",
42
  "manifest_hash": "manifest_good",
43
  "dependency_hash": "deps_good",
@@ -61,8 +62,8 @@ def make_telemetry(**overrides):
61
  return TelemetrySnapshot(**data)
62
 
63
 
64
- def make_baseline(**overrides):
65
- data = {
66
  "runtime_p95_ms": 500,
67
  "error_rate_threshold": 2,
68
  "timeout_threshold": 1,
@@ -85,8 +86,8 @@ def make_baseline(**overrides):
85
  return data
86
 
87
 
88
- def make_policy_result(**overrides):
89
- data = {
90
  "decision": "allow",
91
  "allowed_modules": ["resource_links"],
92
  "blocked_modules": [],
@@ -96,7 +97,22 @@ def make_policy_result(**overrides):
96
  return data
97
 
98
 
99
- def test_drift_vector_defaults_to_zero():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  vector = DriftVector()
101
 
102
  assert vector.statistical == 0.0
@@ -107,7 +123,11 @@ def test_drift_vector_defaults_to_zero():
107
  assert vector.policy == 0.0
108
 
109
 
110
- def test_aggregate_signals_uses_max_score_per_type():
 
 
 
 
111
  signals = [
112
  DriftSignal(
113
  name="weak_adversarial_signal",
@@ -142,7 +162,8 @@ def test_aggregate_signals_uses_max_score_per_type():
142
  assert vector.policy == 0.0
143
 
144
 
145
- def test_dominant_type_respects_priority_not_raw_score():
 
146
  vector = DriftVector(
147
  statistical=0.9,
148
  adversarial=0.4,
@@ -151,6 +172,8 @@ def test_dominant_type_respects_priority_not_raw_score():
151
 
152
  assert choose_dominant_drift_type(vector) == DriftType.ADVERSARIAL
153
 
 
 
154
  vector = DriftVector(
155
  statistical=0.9,
156
  adversarial=0.4,
@@ -160,45 +183,41 @@ def test_dominant_type_respects_priority_not_raw_score():
160
  assert choose_dominant_drift_type(vector) == DriftType.POLICY
161
 
162
 
163
- def test_recommend_correction_policy_drift_reverts():
164
- vector = DriftVector(policy=0.6, statistical=1.0, adversarial=0.2)
165
-
166
- assert recommend_correction(vector) == "REVERT"
167
-
168
-
169
- def test_recommend_correction_structural_drift_reverts():
170
- vector = DriftVector(structural=0.5)
171
-
172
- assert recommend_correction(vector) == "REVERT"
173
-
174
-
175
- def test_recommend_correction_behavioral_drift_reverts():
176
- vector = DriftVector(behavioral=0.7)
177
-
178
- assert recommend_correction(vector) == "REVERT"
179
-
180
-
181
- def test_recommend_correction_adversarial_drift_constrains():
182
- vector = DriftVector(adversarial=0.3, statistical=0.9)
183
-
184
- assert recommend_correction(vector) == "CONSTRAIN"
185
-
186
-
187
- def test_recommend_correction_statistical_drift_adapts_only_when_clean():
188
- vector = DriftVector(statistical=0.5)
189
-
190
- assert recommend_correction(vector) == "ADAPT"
191
-
192
-
193
- def test_recommend_correction_defaults_to_observe():
194
- vector = DriftVector(statistical=0.1, operational=0.1)
195
-
196
- assert recommend_correction(vector) == "OBSERVE"
197
 
198
 
199
- def test_policy_violation_creates_policy_signal_and_revert_recommendation():
200
- telemetry = make_telemetry()
201
- baseline = make_baseline()
 
202
  policy_result = make_policy_result(
203
  decision="constrain",
204
  blocked_modules=["port_scan"],
@@ -224,13 +243,14 @@ def test_policy_violation_creates_policy_signal_and_revert_recommendation():
224
  assert any(signal.drift_type == DriftType.POLICY for signal in assessment.signals)
225
 
226
 
227
- def test_authorization_gate_trigger_creates_policy_signal():
 
 
228
  telemetry = make_telemetry(
229
  modules_requested=["http_headers"],
230
  modules_blocked=["http_headers"],
231
  authorized_target=False,
232
  )
233
- baseline = make_baseline()
234
  policy_result = make_policy_result(
235
  decision="constrain",
236
  blocked_modules=["http_headers"],
@@ -253,14 +273,15 @@ def test_authorization_gate_trigger_creates_policy_signal():
253
  assert assessment.recommended_correction == "REVERT"
254
 
255
 
256
- def test_adversarial_patterns_create_constrain_recommendation():
 
 
 
257
  telemetry = make_telemetry(
258
  input_rejected=True,
259
  rejection_reason="Input contains a blocked pattern.",
260
  sanitized_input_trace="https://example.com/?next=http://169.254.169.254/latest",
261
  )
262
- baseline = make_baseline()
263
- policy_result = make_policy_result()
264
 
265
  assessment = assess_drift(
266
  telemetry=telemetry,
@@ -273,10 +294,31 @@ def test_adversarial_patterns_create_constrain_recommendation():
273
  assert assessment.recommended_correction == "CONSTRAIN"
274
 
275
 
276
- def test_operational_runtime_drift_detected():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  telemetry = make_telemetry(duration_ms=1200)
278
- baseline = make_baseline(runtime_p95_ms=500)
279
- policy_result = make_policy_result()
280
 
281
  assessment = assess_drift(
282
  telemetry=telemetry,
@@ -288,10 +330,11 @@ def test_operational_runtime_drift_detected():
288
  assert any(signal.name == "runtime_boundary_exceeded" for signal in assessment.signals)
289
 
290
 
291
- def test_operational_error_drift_detected():
 
 
 
292
  telemetry = make_telemetry(error_count=3)
293
- baseline = make_baseline(error_rate_threshold=2)
294
- policy_result = make_policy_result()
295
 
296
  assessment = assess_drift(
297
  telemetry=telemetry,
@@ -303,10 +346,28 @@ def test_operational_error_drift_detected():
303
  assert any(signal.name == "error_threshold_exceeded" for signal in assessment.signals)
304
 
305
 
306
- def test_structural_manifest_mismatch_reverts():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  telemetry = make_telemetry(manifest_hash="manifest_changed")
308
- baseline = make_baseline(expected_manifest_hash="manifest_good")
309
- policy_result = make_policy_result()
310
 
311
  assessment = assess_drift(
312
  telemetry=telemetry,
@@ -319,10 +380,11 @@ def test_structural_manifest_mismatch_reverts():
319
  assert assessment.recommended_correction == "REVERT"
320
 
321
 
322
- def test_structural_dependency_mismatch_reverts():
 
 
 
323
  telemetry = make_telemetry(dependency_hash="deps_changed")
324
- baseline = make_baseline(expected_dependency_hash="deps_good")
325
- policy_result = make_policy_result()
326
 
327
  assessment = assess_drift(
328
  telemetry=telemetry,
@@ -334,7 +396,27 @@ def test_structural_dependency_mismatch_reverts():
334
  assert assessment.recommended_correction == "REVERT"
335
 
336
 
337
- def test_behavioral_same_input_different_output_reverts():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  telemetry = make_telemetry(
339
  indicator_hash="hmac_abc123",
340
  output_hash="output_changed",
@@ -342,7 +424,6 @@ def test_behavioral_same_input_different_output_reverts():
342
  baseline = make_baseline(
343
  known_output_hashes={"hmac_abc123": "output_good"},
344
  )
345
- policy_result = make_policy_result()
346
 
347
  assessment = assess_drift(
348
  telemetry=telemetry,
@@ -355,10 +436,11 @@ def test_behavioral_same_input_different_output_reverts():
355
  assert assessment.recommended_correction == "REVERT"
356
 
357
 
358
- def test_behavioral_invalid_schema_reverts():
 
 
 
359
  telemetry = make_telemetry(output_schema_valid=False)
360
- baseline = make_baseline()
361
- policy_result = make_policy_result()
362
 
363
  assessment = assess_drift(
364
  telemetry=telemetry,
@@ -370,12 +452,14 @@ def test_behavioral_invalid_schema_reverts():
370
  assert assessment.recommended_correction == "REVERT"
371
 
372
 
373
- def test_statistical_shift_can_adapt_when_no_higher_priority_signal():
 
 
 
374
  telemetry = make_telemetry(indicator_type="ip")
375
  baseline = make_baseline(
376
  input_type_distribution={"domain": 0.9, "username": 0.1},
377
  )
378
- policy_result = make_policy_result()
379
 
380
  assessment = assess_drift(
381
  telemetry=telemetry,
@@ -388,7 +472,30 @@ def test_statistical_shift_can_adapt_when_no_higher_priority_signal():
388
  assert assessment.recommended_correction == "ADAPT"
389
 
390
 
391
- def test_policy_drift_overrides_statistical_adaptation():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  telemetry = make_telemetry(indicator_type="ip")
393
  baseline = make_baseline(
394
  input_type_distribution={"domain": 0.9, "username": 0.1},
@@ -417,7 +524,10 @@ def test_policy_drift_overrides_statistical_adaptation():
417
  assert assessment.recommended_correction == "REVERT"
418
 
419
 
420
- def test_adversarial_drift_overrides_statistical_adaptation():
 
 
 
421
  telemetry = make_telemetry(
422
  indicator_type="ip",
423
  sanitized_input_trace="http://169.254.169.254/latest",
@@ -425,7 +535,6 @@ def test_adversarial_drift_overrides_statistical_adaptation():
425
  baseline = make_baseline(
426
  input_type_distribution={"domain": 0.9, "username": 0.1},
427
  )
428
- policy_result = make_policy_result()
429
 
430
  assessment = assess_drift(
431
  telemetry=telemetry,
@@ -439,7 +548,7 @@ def test_adversarial_drift_overrides_statistical_adaptation():
439
  assert assessment.recommended_correction == "CONSTRAIN"
440
 
441
 
442
- def test_estimate_confidence_increases_with_signal_count_and_tier():
443
  low_signal = DriftSignal(
444
  name="weak",
445
  drift_type=DriftType.STATISTICAL,
@@ -459,14 +568,16 @@ def test_estimate_confidence_increases_with_signal_count_and_tier():
459
 
460
  assert estimate_confidence([]) == 0.0
461
  assert estimate_confidence([high_signal]) > estimate_confidence([low_signal])
462
- assert estimate_confidence([low_signal, high_signal]) >= estimate_confidence([high_signal])
463
 
 
 
464
 
465
- def test_assess_drift_is_pure_and_does_not_mutate_inputs():
466
- telemetry = make_telemetry()
467
- baseline = make_baseline()
468
- policy_result = make_policy_result()
469
 
 
 
 
 
 
470
  telemetry_before = copy.deepcopy(asdict(telemetry))
471
  baseline_before = copy.deepcopy(baseline)
472
  policy_before = copy.deepcopy(policy_result)
@@ -482,11 +593,11 @@ def test_assess_drift_is_pure_and_does_not_mutate_inputs():
482
  assert policy_result == policy_before
483
 
484
 
485
- def test_clean_execution_observes_without_significant_drift():
486
- telemetry = make_telemetry()
487
- baseline = make_baseline()
488
- policy_result = make_policy_result()
489
-
490
  assessment = assess_drift(
491
  telemetry=telemetry,
492
  baseline=baseline,
@@ -497,4 +608,4 @@ def test_clean_execution_observes_without_significant_drift():
497
  assert assessment.signals == []
498
  assert assessment.dominant_type is None
499
  assert assessment.recommended_correction == "OBSERVE"
500
- assert assessment.confidence == 0.0
 
19
 
20
  import copy
21
  from dataclasses import asdict
22
+ from typing import Any
23
 
24
  import pytest
25
 
 
37
  )
38
 
39
 
40
+ def make_telemetry(**overrides: Any) -> TelemetrySnapshot:
41
+ data: dict[str, Any] = {
42
  "run_id": "run_test_001",
43
  "manifest_hash": "manifest_good",
44
  "dependency_hash": "deps_good",
 
62
  return TelemetrySnapshot(**data)
63
 
64
 
65
+ def make_baseline(**overrides: Any) -> dict[str, Any]:
66
+ data: dict[str, Any] = {
67
  "runtime_p95_ms": 500,
68
  "error_rate_threshold": 2,
69
  "timeout_threshold": 1,
 
86
  return data
87
 
88
 
89
+ def make_policy_result(**overrides: Any) -> dict[str, Any]:
90
+ data: dict[str, Any] = {
91
  "decision": "allow",
92
  "allowed_modules": ["resource_links"],
93
  "blocked_modules": [],
 
97
  return data
98
 
99
 
100
+ @pytest.fixture
101
+ def telemetry() -> TelemetrySnapshot:
102
+ return make_telemetry()
103
+
104
+
105
+ @pytest.fixture
106
+ def baseline() -> dict[str, Any]:
107
+ return make_baseline()
108
+
109
+
110
+ @pytest.fixture
111
+ def policy_result() -> dict[str, Any]:
112
+ return make_policy_result()
113
+
114
+
115
+ def test_drift_vector_defaults_to_zero() -> None:
116
  vector = DriftVector()
117
 
118
  assert vector.statistical == 0.0
 
123
  assert vector.policy == 0.0
124
 
125
 
126
+ def test_aggregate_signals_empty_returns_zero_vector() -> None:
127
+ assert aggregate_signals([]) == DriftVector()
128
+
129
+
130
+ def test_aggregate_signals_uses_max_score_per_type() -> None:
131
  signals = [
132
  DriftSignal(
133
  name="weak_adversarial_signal",
 
162
  assert vector.policy == 0.0
163
 
164
 
165
+ def test_dominant_type_prefers_adversarial_over_statistical() -> None:
166
+ # Adversarial outranks statistical even if statistical has a higher raw score.
167
  vector = DriftVector(
168
  statistical=0.9,
169
  adversarial=0.4,
 
172
 
173
  assert choose_dominant_drift_type(vector) == DriftType.ADVERSARIAL
174
 
175
+
176
+ def test_dominant_type_prefers_policy_over_all() -> None:
177
  vector = DriftVector(
178
  statistical=0.9,
179
  adversarial=0.4,
 
183
  assert choose_dominant_drift_type(vector) == DriftType.POLICY
184
 
185
 
186
+ def test_dominant_type_respects_structural_over_behavioral_over_operational() -> None:
187
+ vector = DriftVector(structural=0.1, behavioral=0.9, operational=1.0)
188
+ assert choose_dominant_drift_type(vector) == DriftType.STRUCTURAL
189
+
190
+ vector = DriftVector(behavioral=0.2, adversarial=0.9, operational=1.0)
191
+ assert choose_dominant_drift_type(vector) == DriftType.BEHAVIORAL
192
+
193
+
194
+ @pytest.mark.parametrize(
195
+ ("vector", "expected"),
196
+ [
197
+ (DriftVector(policy=0.6, statistical=1.0, adversarial=0.2), "REVERT"),
198
+ (DriftVector(structural=0.5), "REVERT"),
199
+ (DriftVector(behavioral=0.7), "REVERT"),
200
+ (DriftVector(adversarial=0.3, statistical=0.9), "CONSTRAIN"),
201
+ (DriftVector(statistical=0.5), "ADAPT"),
202
+ (DriftVector(statistical=0.1, operational=0.1), "OBSERVE"),
203
+ ],
204
+ ids=[
205
+ "policy_revert",
206
+ "structural_revert",
207
+ "behavioral_revert",
208
+ "adversarial_constrain",
209
+ "statistical_adapt",
210
+ "default_observe",
211
+ ],
212
+ )
213
+ def test_recommend_correction(vector: DriftVector, expected: str) -> None:
214
+ assert recommend_correction(vector) == expected
 
 
 
 
 
215
 
216
 
217
+ def test_policy_violation_creates_policy_signal_and_revert_recommendation(
218
+ telemetry: TelemetrySnapshot,
219
+ baseline: dict[str, Any],
220
+ ) -> None:
221
  policy_result = make_policy_result(
222
  decision="constrain",
223
  blocked_modules=["port_scan"],
 
243
  assert any(signal.drift_type == DriftType.POLICY for signal in assessment.signals)
244
 
245
 
246
+ def test_authorization_gate_trigger_creates_policy_signal(
247
+ baseline: dict[str, Any],
248
+ ) -> None:
249
  telemetry = make_telemetry(
250
  modules_requested=["http_headers"],
251
  modules_blocked=["http_headers"],
252
  authorized_target=False,
253
  )
 
254
  policy_result = make_policy_result(
255
  decision="constrain",
256
  blocked_modules=["http_headers"],
 
273
  assert assessment.recommended_correction == "REVERT"
274
 
275
 
276
+ def test_adversarial_patterns_create_constrain_recommendation(
277
+ baseline: dict[str, Any],
278
+ policy_result: dict[str, Any],
279
+ ) -> None:
280
  telemetry = make_telemetry(
281
  input_rejected=True,
282
  rejection_reason="Input contains a blocked pattern.",
283
  sanitized_input_trace="https://example.com/?next=http://169.254.169.254/latest",
284
  )
 
 
285
 
286
  assessment = assess_drift(
287
  telemetry=telemetry,
 
294
  assert assessment.recommended_correction == "CONSTRAIN"
295
 
296
 
297
+ def test_input_rejected_without_trace_does_not_trigger_adversarial_drift(
298
+ baseline: dict[str, Any],
299
+ policy_result: dict[str, Any],
300
+ ) -> None:
301
+ telemetry = make_telemetry(
302
+ input_rejected=True,
303
+ rejection_reason="",
304
+ sanitized_input_trace="",
305
+ )
306
+
307
+ assessment = assess_drift(
308
+ telemetry=telemetry,
309
+ baseline=baseline,
310
+ policy_result=policy_result,
311
+ )
312
+
313
+ assert assessment.drift_vector.adversarial == 0.0
314
+ assert not any(s.drift_type == DriftType.ADVERSARIAL for s in assessment.signals)
315
+
316
+
317
+ def test_operational_runtime_drift_detected(
318
+ baseline: dict[str, Any],
319
+ policy_result: dict[str, Any],
320
+ ) -> None:
321
  telemetry = make_telemetry(duration_ms=1200)
 
 
322
 
323
  assessment = assess_drift(
324
  telemetry=telemetry,
 
330
  assert any(signal.name == "runtime_boundary_exceeded" for signal in assessment.signals)
331
 
332
 
333
+ def test_operational_error_drift_detected(
334
+ baseline: dict[str, Any],
335
+ policy_result: dict[str, Any],
336
+ ) -> None:
337
  telemetry = make_telemetry(error_count=3)
 
 
338
 
339
  assessment = assess_drift(
340
  telemetry=telemetry,
 
346
  assert any(signal.name == "error_threshold_exceeded" for signal in assessment.signals)
347
 
348
 
349
+ def test_operational_timeout_drift_detected(
350
+ baseline: dict[str, Any],
351
+ policy_result: dict[str, Any],
352
+ ) -> None:
353
+ telemetry = make_telemetry(timeout_count=2)
354
+ baseline = make_baseline(timeout_threshold=1)
355
+
356
+ assessment = assess_drift(
357
+ telemetry=telemetry,
358
+ baseline=baseline,
359
+ policy_result=policy_result,
360
+ )
361
+
362
+ assert assessment.drift_vector.operational > 0.0
363
+ assert any(signal.name == "timeout_threshold_exceeded" for signal in assessment.signals)
364
+
365
+
366
+ def test_structural_manifest_mismatch_reverts(
367
+ baseline: dict[str, Any],
368
+ policy_result: dict[str, Any],
369
+ ) -> None:
370
  telemetry = make_telemetry(manifest_hash="manifest_changed")
 
 
371
 
372
  assessment = assess_drift(
373
  telemetry=telemetry,
 
380
  assert assessment.recommended_correction == "REVERT"
381
 
382
 
383
+ def test_structural_dependency_mismatch_reverts(
384
+ baseline: dict[str, Any],
385
+ policy_result: dict[str, Any],
386
+ ) -> None:
387
  telemetry = make_telemetry(dependency_hash="deps_changed")
 
 
388
 
389
  assessment = assess_drift(
390
  telemetry=telemetry,
 
396
  assert assessment.recommended_correction == "REVERT"
397
 
398
 
399
+ def test_structural_runtime_python_version_mismatch_reverts(
400
+ baseline: dict[str, Any],
401
+ policy_result: dict[str, Any],
402
+ ) -> None:
403
+ telemetry = make_telemetry(runtime_python_version="3.13.1")
404
+
405
+ assessment = assess_drift(
406
+ telemetry=telemetry,
407
+ baseline=baseline,
408
+ policy_result=policy_result,
409
+ )
410
+
411
+ assert assessment.drift_vector.structural > 0.0
412
+ assert assessment.recommended_correction == "REVERT"
413
+ assert any(signal.name == "runtime_python_version_changed" for signal in assessment.signals)
414
+
415
+
416
+ def test_behavioral_same_input_different_output_reverts(
417
+ baseline: dict[str, Any],
418
+ policy_result: dict[str, Any],
419
+ ) -> None:
420
  telemetry = make_telemetry(
421
  indicator_hash="hmac_abc123",
422
  output_hash="output_changed",
 
424
  baseline = make_baseline(
425
  known_output_hashes={"hmac_abc123": "output_good"},
426
  )
 
427
 
428
  assessment = assess_drift(
429
  telemetry=telemetry,
 
436
  assert assessment.recommended_correction == "REVERT"
437
 
438
 
439
+ def test_behavioral_invalid_schema_reverts(
440
+ baseline: dict[str, Any],
441
+ policy_result: dict[str, Any],
442
+ ) -> None:
443
  telemetry = make_telemetry(output_schema_valid=False)
 
 
444
 
445
  assessment = assess_drift(
446
  telemetry=telemetry,
 
452
  assert assessment.recommended_correction == "REVERT"
453
 
454
 
455
+ def test_statistical_shift_can_adapt_when_no_higher_priority_signal(
456
+ baseline: dict[str, Any],
457
+ policy_result: dict[str, Any],
458
+ ) -> None:
459
  telemetry = make_telemetry(indicator_type="ip")
460
  baseline = make_baseline(
461
  input_type_distribution={"domain": 0.9, "username": 0.1},
462
  )
 
463
 
464
  assessment = assess_drift(
465
  telemetry=telemetry,
 
472
  assert assessment.recommended_correction == "ADAPT"
473
 
474
 
475
+ def test_statistical_module_usage_shift_detected(
476
+ baseline: dict[str, Any],
477
+ policy_result: dict[str, Any],
478
+ ) -> None:
479
+ telemetry = make_telemetry(
480
+ modules_executed=["resource_links", "dns_lookup"],
481
+ )
482
+ baseline = make_baseline(
483
+ module_usage_distribution={"resource_links": 1.0},
484
+ )
485
+
486
+ assessment = assess_drift(
487
+ telemetry=telemetry,
488
+ baseline=baseline,
489
+ policy_result=policy_result,
490
+ )
491
+
492
+ assert assessment.drift_vector.statistical > 0.0
493
+ assert any(signal.name == "module_usage_distribution_shifted" for signal in assessment.signals)
494
+
495
+
496
+ def test_policy_drift_overrides_statistical_adaptation(
497
+ baseline: dict[str, Any],
498
+ ) -> None:
499
  telemetry = make_telemetry(indicator_type="ip")
500
  baseline = make_baseline(
501
  input_type_distribution={"domain": 0.9, "username": 0.1},
 
524
  assert assessment.recommended_correction == "REVERT"
525
 
526
 
527
+ def test_adversarial_drift_overrides_statistical_adaptation(
528
+ baseline: dict[str, Any],
529
+ policy_result: dict[str, Any],
530
+ ) -> None:
531
  telemetry = make_telemetry(
532
  indicator_type="ip",
533
  sanitized_input_trace="http://169.254.169.254/latest",
 
535
  baseline = make_baseline(
536
  input_type_distribution={"domain": 0.9, "username": 0.1},
537
  )
 
538
 
539
  assessment = assess_drift(
540
  telemetry=telemetry,
 
548
  assert assessment.recommended_correction == "CONSTRAIN"
549
 
550
 
551
+ def test_estimate_confidence_increases_with_signal_count_and_tier() -> None:
552
  low_signal = DriftSignal(
553
  name="weak",
554
  drift_type=DriftType.STATISTICAL,
 
568
 
569
  assert estimate_confidence([]) == 0.0
570
  assert estimate_confidence([high_signal]) > estimate_confidence([low_signal])
 
571
 
572
+ # Contract: adding a signal should strictly increase confidence.
573
+ assert estimate_confidence([low_signal, high_signal]) > estimate_confidence([high_signal])
574
 
 
 
 
 
575
 
576
+ def test_assess_drift_is_pure_and_does_not_mutate_inputs(
577
+ telemetry: TelemetrySnapshot,
578
+ baseline: dict[str, Any],
579
+ policy_result: dict[str, Any],
580
+ ) -> None:
581
  telemetry_before = copy.deepcopy(asdict(telemetry))
582
  baseline_before = copy.deepcopy(baseline)
583
  policy_before = copy.deepcopy(policy_result)
 
593
  assert policy_result == policy_before
594
 
595
 
596
+ def test_clean_execution_observes_without_significant_drift(
597
+ telemetry: TelemetrySnapshot,
598
+ baseline: dict[str, Any],
599
+ policy_result: dict[str, Any],
600
+ ) -> None:
601
  assessment = assess_drift(
602
  telemetry=telemetry,
603
  baseline=baseline,
 
608
  assert assessment.signals == []
609
  assert assessment.dominant_type is None
610
  assert assessment.recommended_correction == "OBSERVE"
611
+ assert assessment.confidence == pytest.approx(0.0)
tests/test_intent.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ tests/test_intent.py
3
+ ====================
4
+
5
+ Contract tests for osint_core.intent.
6
+
7
+ Core invariants:
8
+ - Intent packets are immutable.
9
+ - Intent packets do not store raw indicators.
10
+ - Scope boundaries are explicit and validated.
11
+ - Forbidden operations cannot appear in allowed operations.
12
+ - Packets can be signed and verified.
13
+ - Signature tampering is detected.
14
+ - Risk and rollback helpers are deterministic.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import FrozenInstanceError, replace
20
+
21
+ import pytest
22
+
23
+ from osint_core.intent import (
24
+ DEFAULT_FORBIDDEN_OPERATIONS,
25
+ IntentErrorCode,
26
+ IntentPacket,
27
+ IntentValidationError,
28
+ canonical_json,
29
+ create_intent_packet,
30
+ default_rollback_for_risk,
31
+ derive_risk_label,
32
+ find_raw_indicator_fields,
33
+ hash_manifest_payload,
34
+ intent_fingerprint,
35
+ make_scope,
36
+ risk_score,
37
+ sign_payload,
38
+ unsigned_intent_fingerprint,
39
+ validate_intent,
40
+ validate_scope,
41
+ verify_intent_signature,
42
+ )
43
+
44
+
45
+ TEST_SECRET = "test-intent-signing-secret"
46
+ TARGET_HASH = "a" * 64
47
+ MANIFEST_HASH = "b" * 64
48
+
49
+
50
+ def make_valid_scope(**overrides):
51
+ data = {
52
+ "target_hash": TARGET_HASH,
53
+ "indicator_type": "domain",
54
+ "allowed_operations": ["resource_links"],
55
+ "success_criteria": ["links_generated"],
56
+ }
57
+ data.update(overrides)
58
+ return make_scope(**data)
59
+
60
+
61
+ def make_valid_packet(**overrides):
62
+ scope = overrides.pop("scope", make_valid_scope())
63
+ data = {
64
+ "action": "enrich_indicator",
65
+ "purpose": "Generate passive OSINT source links for a validated indicator.",
66
+ "scope": scope,
67
+ "requested_modules": ["resource_links"],
68
+ "expected_side_effects": ["report_created", "audit_event_created"],
69
+ "rollback_strategy": "observe_only",
70
+ "risk_label": "low",
71
+ "manifest_hash": MANIFEST_HASH,
72
+ "signing_secret": TEST_SECRET,
73
+ }
74
+ data.update(overrides)
75
+ return create_intent_packet(**data)
76
+
77
+
78
+ def test_make_scope_adds_default_forbidden_operations():
79
+ scope = make_valid_scope()
80
+ for operation in DEFAULT_FORBIDDEN_OPERATIONS:
81
+ assert operation in scope.forbidden_operations
82
+ assert scope.target_hash == TARGET_HASH
83
+ assert scope.indicator_type == "domain"
84
+ assert scope.allowed_operations == ("resource_links",)
85
+
86
+
87
+ def test_scope_rejects_missing_target_hash():
88
+ result = validate_scope(make_valid_scope(target_hash="c" * 64))
89
+ assert result.ok is True
90
+ with pytest.raises(IntentValidationError) as exc:
91
+ make_valid_scope(target_hash="")
92
+ assert exc.value.code == IntentErrorCode.MISSING_FIELD
93
+
94
+
95
+ def test_scope_rejects_non_hash_target_identity():
96
+ with pytest.raises(IntentValidationError) as exc:
97
+ make_valid_scope(target_hash="example.com")
98
+ assert exc.value.code == IntentErrorCode.INVALID_SCOPE
99
+
100
+
101
+ def test_scope_rejects_empty_allowed_operations():
102
+ with pytest.raises(IntentValidationError) as exc:
103
+ make_valid_scope(allowed_operations=[])
104
+ assert exc.value.code == IntentErrorCode.MISSING_FIELD
105
+
106
+
107
+ def test_scope_rejects_forbidden_operation_overlap():
108
+ with pytest.raises(IntentValidationError) as exc:
109
+ make_valid_scope(allowed_operations=["resource_links", "port_scan"])
110
+ assert exc.value.code == IntentErrorCode.FORBIDDEN_OPERATION_REQUESTED
111
+
112
+
113
+ def test_scope_rejects_invalid_time_horizon():
114
+ with pytest.raises(IntentValidationError) as exc:
115
+ make_valid_scope(time_horizon_seconds=0)
116
+ assert exc.value.code == IntentErrorCode.INVALID_SCOPE
117
+
118
+ with pytest.raises(IntentValidationError) as exc:
119
+ make_valid_scope(time_horizon_seconds=90_000)
120
+ assert exc.value.code == IntentErrorCode.INVALID_SCOPE
121
+
122
+
123
+ def test_create_intent_packet_signs_and_verifies():
124
+ packet = make_valid_packet()
125
+ assert isinstance(packet, IntentPacket)
126
+ assert packet.signature is not None
127
+ assert verify_intent_signature(packet, secret=TEST_SECRET) is True
128
+
129
+
130
+ def test_intent_packet_is_immutable():
131
+ packet = make_valid_packet()
132
+ with pytest.raises(FrozenInstanceError):
133
+ packet.purpose = "mutated" # type: ignore[misc]
134
+
135
+
136
+ def test_unsigned_payload_excludes_signature():
137
+ packet = make_valid_packet()
138
+ payload = packet.unsigned_payload()
139
+ assert "signature" not in payload
140
+ assert packet.signature is not None
141
+
142
+
143
+ def test_signature_tampering_is_detected():
144
+ packet = make_valid_packet()
145
+ tampered = replace(packet, purpose="Changed purpose after signing.")
146
+ with pytest.raises(IntentValidationError) as exc:
147
+ verify_intent_signature(tampered, secret=TEST_SECRET)
148
+ assert exc.value.code == IntentErrorCode.SIGNATURE_MISMATCH
149
+
150
+
151
+ def test_unsigned_packet_fails_verification():
152
+ packet = create_intent_packet(
153
+ action="enrich_indicator",
154
+ purpose="Generate passive links.",
155
+ scope=make_valid_scope(),
156
+ requested_modules=["resource_links"],
157
+ expected_side_effects=["report_created"],
158
+ rollback_strategy="observe_only",
159
+ risk_label="low",
160
+ manifest_hash=MANIFEST_HASH,
161
+ sign=False,
162
+ )
163
+ assert packet.signature is None
164
+ with pytest.raises(IntentValidationError) as exc:
165
+ verify_intent_signature(packet, secret=TEST_SECRET)
166
+ assert exc.value.code == IntentErrorCode.UNSIGNED_PACKET
167
+
168
+
169
+ def test_packet_rejects_invalid_action():
170
+ with pytest.raises(IntentValidationError) as exc:
171
+ make_valid_packet(action="delete_everything") # type: ignore[arg-type]
172
+ assert exc.value.code == IntentErrorCode.INVALID_ACTION
173
+
174
+
175
+ def test_packet_rejects_invalid_risk_label():
176
+ with pytest.raises(IntentValidationError) as exc:
177
+ make_valid_packet(risk_label="extreme") # type: ignore[arg-type]
178
+ assert exc.value.code == IntentErrorCode.INVALID_RISK
179
+
180
+
181
+ def test_packet_rejects_invalid_rollback_strategy():
182
+ with pytest.raises(IntentValidationError) as exc:
183
+ make_valid_packet(rollback_strategy="YOLO") # type: ignore[arg-type]
184
+ assert exc.value.code == IntentErrorCode.INVALID_ROLLBACK
185
+
186
+
187
+ def test_packet_rejects_invalid_manifest_hash():
188
+ with pytest.raises(IntentValidationError) as exc:
189
+ make_valid_packet(manifest_hash="not-a-hash")
190
+ assert exc.value.code == IntentErrorCode.MISSING_FIELD
191
+
192
+
193
+ def test_packet_rejects_empty_purpose():
194
+ with pytest.raises(IntentValidationError) as exc:
195
+ make_valid_packet(purpose=" ")
196
+ assert exc.value.code == IntentErrorCode.MISSING_FIELD
197
+
198
+
199
+ def test_raw_indicator_field_detection():
200
+ payload = {
201
+ "safe": {"target_hash": TARGET_HASH},
202
+ "unsafe": {
203
+ "raw_indicator": "example.com",
204
+ "nested": {"email": "user@example.com"},
205
+ },
206
+ }
207
+ findings = find_raw_indicator_fields(payload)
208
+ assert "unsafe.raw_indicator" in findings
209
+ assert "unsafe.nested.email" in findings
210
+
211
+
212
+ def test_validate_intent_rejects_raw_indicator_like_fields():
213
+ packet = make_valid_packet()
214
+ unsafe_dict = packet.to_dict()
215
+ unsafe_dict["raw_indicator"] = "example.com"
216
+ findings = find_raw_indicator_fields(unsafe_dict)
217
+ assert "raw_indicator" in findings
218
+
219
+
220
+ def test_canonical_json_is_deterministic():
221
+ assert canonical_json({"b": 2, "a": 1}) == canonical_json({"a": 1, "b": 2})
222
+
223
+
224
+ def test_sign_payload_is_deterministic_for_same_payload_and_secret():
225
+ payload = {"a": 1, "b": 2}
226
+ assert sign_payload(payload, TEST_SECRET) == sign_payload(payload, TEST_SECRET)
227
+ assert sign_payload(payload, TEST_SECRET) != sign_payload(payload, "different-secret")
228
+
229
+
230
+ def test_hash_manifest_payload_is_stable():
231
+ payload = {"artifact": "test", "version": "1.0.0"}
232
+ assert hash_manifest_payload(payload) == hash_manifest_payload(payload)
233
+ assert len(hash_manifest_payload(payload)) == 64
234
+
235
+
236
+ def test_intent_fingerprints_are_stable_and_distinct():
237
+ packet = make_valid_packet()
238
+ signed_fp = intent_fingerprint(packet)
239
+ unsigned_fp = unsigned_intent_fingerprint(packet)
240
+ assert len(signed_fp) == 64
241
+ assert len(unsigned_fp) == 64
242
+ assert signed_fp != unsigned_fp
243
+
244
+
245
+ def test_validate_intent_accepts_valid_packet():
246
+ result = validate_intent(make_valid_packet())
247
+ assert result.ok is True
248
+ assert result.errors == ()
249
+ assert result.error_codes == ()
250
+
251
+
252
+ def test_risk_score_mapping():
253
+ assert risk_score("low") == 0.25
254
+ assert risk_score("medium") == 0.5
255
+ assert risk_score("high") == 0.75
256
+ assert risk_score("critical") == 1.0
257
+
258
+
259
+ def test_default_rollback_for_risk():
260
+ assert default_rollback_for_risk("low") == "observe_only"
261
+ assert default_rollback_for_risk("medium") == "disable_module"
262
+ assert default_rollback_for_risk("high") == "sandbox"
263
+ assert default_rollback_for_risk("critical") == "revert"
264
+
265
+
266
+ def test_derive_risk_label_for_low_risk_passive_modules():
267
+ assert derive_risk_label(
268
+ requested_modules=["resource_links"],
269
+ authorized_target=False,
270
+ ) == "low"
271
+
272
+
273
+ def test_derive_risk_label_for_conditional_authorized_modules():
274
+ assert derive_risk_label(
275
+ requested_modules=["http_headers"],
276
+ authorized_target=True,
277
+ ) == "medium"
278
+
279
+
280
+ def test_derive_risk_label_for_conditional_unauthorized_modules():
281
+ assert derive_risk_label(
282
+ requested_modules=["http_headers"],
283
+ authorized_target=False,
284
+ ) == "high"
285
+
286
+
287
+ def test_derive_risk_label_for_forbidden_modules():
288
+ assert derive_risk_label(
289
+ requested_modules=["nmap"],
290
+ authorized_target=True,
291
+ ) == "critical"
tests/test_orchestrator.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for osint_core.orchestrator module
3
+ """
4
+
5
+ import pytest
6
+
7
+ from osint_core.orchestrator import (
8
+ OrchestratorAgent,
9
+ ExecutionStatus,
10
+ create_orchestrator,
11
+ list_skills,
12
+ get_skill,
13
+ SKILLS_REGISTRY,
14
+ )
15
+ from osint_core.policy import PolicyDecision
16
+
17
+
18
+ def test_create_orchestrator():
19
+ """Test orchestrator agent creation"""
20
+ agent = create_orchestrator()
21
+ assert isinstance(agent, OrchestratorAgent)
22
+ assert agent.role == "orchestrator"
23
+ assert len(agent.skills) > 0
24
+
25
+
26
+ def test_list_skills():
27
+ """Test skills registry listing"""
28
+ skills = list_skills()
29
+ assert isinstance(skills, dict)
30
+ assert "resource_links" in skills
31
+ assert "dns_records" in skills
32
+ assert "http_headers" in skills
33
+
34
+
35
+ def test_get_skill():
36
+ """Test individual skill retrieval"""
37
+ skill = get_skill("resource_links")
38
+ assert skill is not None
39
+ assert skill.name == "Resource Links"
40
+ assert skill.canonical_name == "resource_links"
41
+ assert skill.requires_authorization is False
42
+
43
+ # Test conditional skill
44
+ http_skill = get_skill("http_headers")
45
+ assert http_skill is not None
46
+ assert http_skill.requires_authorization is True
47
+
48
+
49
+ def test_get_nonexistent_skill():
50
+ """Test retrieval of non-existent skill"""
51
+ skill = get_skill("nonexistent_skill")
52
+ assert skill is None
53
+
54
+
55
+ def test_create_context_valid_input():
56
+ """Test execution context creation with valid input"""
57
+ agent = create_orchestrator()
58
+ context = agent.create_context(
59
+ raw_indicator="example.com",
60
+ indicator_type_hint="Domain",
61
+ requested_modules=["resource_links"],
62
+ authorized_target=False,
63
+ passive_only=True,
64
+ )
65
+
66
+ assert context.run_id.startswith("run_")
67
+ assert context.indicator_type == "domain"
68
+ assert context.normalized_indicator == "example.com"
69
+ assert len(context.indicator_hash) == 64 # SHA256 hex
70
+ assert context.requested_modules == ["resource_links"]
71
+ assert context.authorized_target is False
72
+ assert context.passive_only is True
73
+ assert len(context.errors) == 0
74
+
75
+
76
+ def test_create_context_invalid_input():
77
+ """Test execution context creation with invalid input"""
78
+ agent = create_orchestrator()
79
+ context = agent.create_context(
80
+ raw_indicator="<script>alert('xss')</script>",
81
+ indicator_type_hint="Auto",
82
+ requested_modules=["resource_links"],
83
+ authorized_target=False,
84
+ passive_only=True,
85
+ )
86
+
87
+ assert context.indicator_type == "unknown"
88
+ assert context.normalized_indicator == ""
89
+ assert len(context.errors) > 0
90
+
91
+
92
+ def test_execute_workflow_with_valid_domain():
93
+ """Test full workflow execution with valid domain"""
94
+ agent = create_orchestrator()
95
+ workflow = agent.execute_workflow(
96
+ raw_indicator="example.com",
97
+ indicator_type_hint="Domain",
98
+ requested_modules=["resource_links", "dns_records"],
99
+ authorized_target=False,
100
+ passive_only=True,
101
+ )
102
+
103
+ assert workflow.validation_result.ok is True
104
+ assert workflow.context.indicator_type == "domain"
105
+ assert workflow.policy_evaluation.decision == PolicyDecision.ALLOW
106
+ assert len(workflow.policy_evaluation.allowed_modules) == 2
107
+ assert "resource_links" in workflow.policy_evaluation.allowed_modules
108
+ assert "dns_records" in workflow.policy_evaluation.allowed_modules
109
+ assert len(workflow.skill_results) == 2
110
+ assert workflow.duration_ms > 0
111
+
112
+
113
+ def test_execute_workflow_blocks_unauthorized_modules():
114
+ """Test that unauthorized modules are blocked"""
115
+ agent = create_orchestrator()
116
+ workflow = agent.execute_workflow(
117
+ raw_indicator="example.com",
118
+ indicator_type_hint="Domain",
119
+ requested_modules=["resource_links", "http_headers"],
120
+ authorized_target=False, # Not authorized
121
+ passive_only=True,
122
+ )
123
+
124
+ assert workflow.validation_result.ok is True
125
+ assert workflow.policy_evaluation.decision == PolicyDecision.CONSTRAIN
126
+ assert "resource_links" in workflow.policy_evaluation.allowed_modules
127
+ assert "http_headers" in workflow.policy_evaluation.blocked_modules
128
+ # Only resource_links should be executed
129
+ assert len([r for r in workflow.skill_results if r.status == ExecutionStatus.COMPLETED]) == 1
130
+
131
+
132
+ def test_execute_workflow_allows_authorized_modules():
133
+ """Test that authorized modules are allowed when authorized"""
134
+ agent = create_orchestrator()
135
+ workflow = agent.execute_workflow(
136
+ raw_indicator="example.com",
137
+ indicator_type_hint="Domain",
138
+ requested_modules=["http_headers"],
139
+ authorized_target=True, # Authorized
140
+ passive_only=False, # Not passive-only mode
141
+ )
142
+
143
+ assert workflow.validation_result.ok is True
144
+ assert "http_headers" in workflow.policy_evaluation.allowed_modules
145
+ assert len(workflow.policy_evaluation.blocked_modules) == 0
146
+
147
+
148
+ def test_execute_workflow_with_invalid_input():
149
+ """Test workflow execution with invalid input"""
150
+ agent = create_orchestrator()
151
+ workflow = agent.execute_workflow(
152
+ raw_indicator="!!!invalid!!!",
153
+ indicator_type_hint="Auto",
154
+ requested_modules=["resource_links"],
155
+ authorized_target=False,
156
+ passive_only=True,
157
+ )
158
+
159
+ assert workflow.validation_result.ok is False
160
+ assert len(workflow.skill_results) == 0
161
+ assert workflow.correction_verb == "REVERT"
162
+
163
+
164
+ def test_execute_workflow_blocks_wrong_indicator_type():
165
+ """Test that skills requiring specific indicator types are blocked"""
166
+ agent = create_orchestrator()
167
+ workflow = agent.execute_workflow(
168
+ raw_indicator="username123",
169
+ indicator_type_hint="Username",
170
+ requested_modules=["dns_records"], # Requires domain
171
+ authorized_target=False,
172
+ passive_only=True,
173
+ )
174
+
175
+ assert workflow.validation_result.ok is True
176
+ assert workflow.context.indicator_type == "username"
177
+ assert "dns_records" in workflow.policy_evaluation.allowed_modules
178
+ # DNS skill should be blocked because username is not compatible
179
+ dns_result = next((r for r in workflow.skill_results if r.skill_name == "DNS Records"), None)
180
+ assert dns_result is not None
181
+ assert dns_result.status == ExecutionStatus.BLOCKED
182
+
183
+
184
+ def test_drift_detection_with_policy_violations():
185
+ """Test drift detection when policy violations occur"""
186
+ agent = create_orchestrator()
187
+ workflow = agent.execute_workflow(
188
+ raw_indicator="example.com",
189
+ indicator_type_hint="Domain",
190
+ requested_modules=["http_headers"], # Requires auth
191
+ authorized_target=False, # No auth
192
+ passive_only=True,
193
+ )
194
+
195
+ # Should detect policy drift
196
+ assert workflow.drift_vector["policy"] > 0
197
+ assert workflow.correction_verb in ["CONSTRAIN", "REVERT"]
198
+
199
+
200
+ def test_correction_verb_choices():
201
+ """Test that correction verbs follow the priority rules"""
202
+ agent = create_orchestrator()
203
+
204
+ # Low drift should result in OBSERVE
205
+ workflow1 = agent.execute_workflow(
206
+ raw_indicator="example.com",
207
+ indicator_type_hint="Domain",
208
+ requested_modules=["resource_links"],
209
+ authorized_target=False,
210
+ passive_only=True,
211
+ )
212
+ assert workflow1.correction_verb == "OBSERVE"
213
+
214
+ # Policy violation should result in CONSTRAIN or REVERT
215
+ workflow2 = agent.execute_workflow(
216
+ raw_indicator="example.com",
217
+ indicator_type_hint="Domain",
218
+ requested_modules=["http_headers"],
219
+ authorized_target=False,
220
+ passive_only=True,
221
+ )
222
+ assert workflow2.correction_verb in ["CONSTRAIN", "REVERT"]
223
+
224
+
225
+ def test_skill_execution_timing():
226
+ """Test that skill execution tracks duration"""
227
+ agent = create_orchestrator()
228
+ workflow = agent.execute_workflow(
229
+ raw_indicator="example.com",
230
+ indicator_type_hint="Domain",
231
+ requested_modules=["resource_links"],
232
+ authorized_target=False,
233
+ passive_only=True,
234
+ )
235
+
236
+ assert workflow.duration_ms > 0
237
+ for result in workflow.skill_results:
238
+ if result.status == ExecutionStatus.COMPLETED:
239
+ assert result.duration_ms >= 0
240
+
241
+
242
+ def test_skills_registry_structure():
243
+ """Test that skills registry has correct structure"""
244
+ for skill_name, skill in SKILLS_REGISTRY.items():
245
+ assert skill.canonical_name == skill_name
246
+ assert isinstance(skill.name, str)
247
+ assert isinstance(skill.description, str)
248
+ assert isinstance(skill.required_indicator_types, list)
249
+ assert isinstance(skill.tools, list)
250
+ assert isinstance(skill.requires_authorization, bool)
251
+ assert skill.category in ["validation", "passive_lookup", "conditional_fetch", "analysis"]
252
+
253
+
254
+ def test_url_parsing_skill():
255
+ """Test URL parsing skill with URL indicator"""
256
+ agent = create_orchestrator()
257
+ workflow = agent.execute_workflow(
258
+ raw_indicator="https://example.com/path",
259
+ indicator_type_hint="URL",
260
+ requested_modules=["local_url_parse"],
261
+ authorized_target=False,
262
+ passive_only=True,
263
+ )
264
+
265
+ assert workflow.validation_result.ok is True
266
+ assert workflow.context.indicator_type == "url"
267
+ assert len(workflow.skill_results) == 1
268
+ result = workflow.skill_results[0]
269
+ assert result.status == ExecutionStatus.COMPLETED
270
+ assert "scheme" in result.data
271
+
272
+
273
+ def test_multiple_modules_execution():
274
+ """Test execution of multiple modules in parallel"""
275
+ agent = create_orchestrator()
276
+ workflow = agent.execute_workflow(
277
+ raw_indicator="example.com",
278
+ indicator_type_hint="Domain",
279
+ requested_modules=["resource_links", "dns_records"],
280
+ authorized_target=False,
281
+ passive_only=True,
282
+ )
283
+
284
+ assert len(workflow.skill_results) == 2
285
+ completed = [r for r in workflow.skill_results if r.status == ExecutionStatus.COMPLETED]
286
+ assert len(completed) == 2
tests/test_scheduler.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from osint_core.scheduler import (
2
+ INVARIANT_CHECKS,
3
+ DecisionPacket,
4
+ ScheduleReason,
5
+ ShortcutDebt,
6
+ SystemState,
7
+ fits_deadline,
8
+ safe_utility,
9
+ schedule_decision,
10
+ total_required_time_ms,
11
+ )
12
+
13
+
14
+ def make_packet(**overrides):
15
+ data = {
16
+ "intent_id": "intent_123",
17
+ "action": "enrich_indicator",
18
+ "risk_label": "low",
19
+ "confidence": 0.90,
20
+ "reversibility": 0.90,
21
+ "deadline_ms": 1000,
22
+ "verification_cost_ms": 100,
23
+ "execution_cost_ms": 100,
24
+ "rollback_cost_ms": 100,
25
+ "expected_utility_decay": 0.10,
26
+ "required_checks": INVARIANT_CHECKS,
27
+ "rollback_plan": "observe_only",
28
+ "uncertainty_notes": (),
29
+ }
30
+ data.update(overrides)
31
+ return DecisionPacket(**data)
32
+
33
+
34
+ def test_total_required_time_and_deadline_fit():
35
+ packet = make_packet()
36
+ assert total_required_time_ms(packet) == 300
37
+ assert fits_deadline(packet) is True
38
+
39
+
40
+ def test_fast_path_for_low_risk_reversible_action():
41
+ decision = schedule_decision(make_packet(risk_label="low"))
42
+ assert decision.route == "FAST"
43
+ assert decision.allowed is True
44
+ assert decision.reason == ScheduleReason.FAST_PATH_AVAILABLE
45
+
46
+
47
+ def test_deliberative_path_for_high_risk_action_that_fits_deadline():
48
+ packet = make_packet(
49
+ risk_label="high",
50
+ confidence=0.90,
51
+ reversibility=0.70,
52
+ rollback_plan="sandbox",
53
+ rollback_cost_ms=200,
54
+ deadline_ms=1000,
55
+ )
56
+ decision = schedule_decision(packet)
57
+ assert decision.route == "DELIBERATIVE"
58
+ assert decision.allowed is True
59
+
60
+
61
+ def test_invariant_checks_cannot_be_skipped():
62
+ packet = make_packet(required_checks=("scope_validated",))
63
+ decision = schedule_decision(packet)
64
+ assert decision.route == "FAIL_CLOSED"
65
+ assert decision.allowed is False
66
+ assert decision.reason == ScheduleReason.INVARIANT_VIOLATION
67
+ assert "hash_salt_present" in decision.skipped_checks
68
+
69
+
70
+ def test_high_risk_without_rollback_fails_closed():
71
+ packet = make_packet(
72
+ risk_label="critical",
73
+ rollback_plan="",
74
+ rollback_cost_ms=0,
75
+ )
76
+ decision = schedule_decision(packet)
77
+ assert decision.route == "FAIL_CLOSED"
78
+ assert decision.reason == ScheduleReason.MISSING_ROLLBACK
79
+
80
+
81
+ def test_deadline_too_tight_routes_to_containment_when_reversible():
82
+ packet = make_packet(
83
+ deadline_ms=100,
84
+ verification_cost_ms=100,
85
+ execution_cost_ms=100,
86
+ rollback_cost_ms=100,
87
+ reversibility=0.90,
88
+ )
89
+ decision = schedule_decision(packet)
90
+ assert decision.route == "CONTAINMENT"
91
+ assert decision.reason == ScheduleReason.DEADLINE_TOO_TIGHT
92
+ assert decision.authority_scale == 0.25
93
+
94
+
95
+ def test_deadline_too_tight_and_not_reversible_fails_closed():
96
+ packet = make_packet(
97
+ deadline_ms=100,
98
+ reversibility=0.20,
99
+ )
100
+ decision = schedule_decision(packet)
101
+ assert decision.route == "FAIL_CLOSED"
102
+ assert decision.reason == ScheduleReason.NO_SAFE_ACTION_FITS
103
+
104
+
105
+ def test_shortcut_debt_forces_containment():
106
+ state = SystemState(shortcut_debt=ShortcutDebt(emergency_overrides=2), shortcut_debt_limit=0.70)
107
+ decision = schedule_decision(make_packet(), state)
108
+ assert decision.route == "CONTAINMENT"
109
+ assert decision.reason == ScheduleReason.SHORTCUT_DEBT_TOO_HIGH
110
+
111
+
112
+ def test_contested_trust_state_forces_containment():
113
+ state = SystemState(trust_state="contested")
114
+ decision = schedule_decision(make_packet(), state)
115
+ assert decision.route == "CONTAINMENT"
116
+ assert decision.reason == ScheduleReason.TRUST_STATE_DEGRADED
117
+
118
+
119
+ def test_low_confidence_high_risk_forces_containment():
120
+ packet = make_packet(risk_label="high", confidence=0.20, rollback_plan="sandbox")
121
+ decision = schedule_decision(packet)
122
+ assert decision.route == "CONTAINMENT"
123
+ assert decision.reason == ScheduleReason.LOW_CONFIDENCE
124
+
125
+
126
+ def test_safe_utility_is_bounded():
127
+ score = safe_utility(make_packet())
128
+ assert 0.0 <= score <= 1.0