agentprobe-testing 1.3.0__tar.gz → 1.4.0__tar.gz

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 (110) hide show
  1. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/PKG-INFO +1 -1
  2. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/__init__.py +1 -1
  3. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/quickstart.py +21 -1
  4. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/registry.py +1 -0
  5. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/report.py +14 -2
  6. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/PKG-INFO +1 -1
  7. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/SOURCES.txt +1 -0
  8. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/pyproject.toml +1 -1
  9. agentprobe_testing-1.4.0/tests/test_injection_mode.py +80 -0
  10. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/LICENSE +0 -0
  11. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/README.md +0 -0
  12. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/agents/__init__.py +0 -0
  13. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/agents/base.py +0 -0
  14. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/agents/browser_agent.py +0 -0
  15. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/agents/rule_based.py +0 -0
  16. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/agents/scripted.py +0 -0
  17. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/agents/target_agent.py +0 -0
  18. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/agreement.py +0 -0
  19. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/classifier.py +0 -0
  20. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/cli.py +0 -0
  21. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/diff.py +0 -0
  22. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domain.py +0 -0
  23. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/__init__.py +0 -0
  24. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/__init__.py +0 -0
  25. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/agent.py +0 -0
  26. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/clean.py +0 -0
  27. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/complex_agent.py +0 -0
  28. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/decoy.py +0 -0
  29. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/domain.py +0 -0
  30. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/entities.py +0 -0
  31. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  32. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  33. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/scenarios.py +0 -0
  34. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/split.py +0 -0
  35. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/tools.py +0 -0
  36. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/trap.py +0 -0
  37. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/__init__.py +0 -0
  38. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/agent.py +0 -0
  39. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/clean.py +0 -0
  40. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/domain.py +0 -0
  41. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/entities.py +0 -0
  42. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/injector_prompt.py +0 -0
  43. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/rule_based_agent.py +0 -0
  44. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/scenarios.py +0 -0
  45. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/tools.py +0 -0
  46. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/trap.py +0 -0
  47. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/feedback.py +0 -0
  48. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/generic_world.py +0 -0
  49. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/injection.py +0 -0
  50. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/injector.py +0 -0
  51. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/llm.py +0 -0
  52. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/playbook.py +0 -0
  53. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/priority.py +0 -0
  54. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/reachability.py +0 -0
  55. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/runner.py +0 -0
  56. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/scenario.py +0 -0
  57. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/__init__.py +0 -0
  58. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/clean.py +0 -0
  59. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/decoy.py +0 -0
  60. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/registry.py +0 -0
  61. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/split.py +0 -0
  62. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/trap.py +0 -0
  63. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/summarize.py +0 -0
  64. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/termui.py +0 -0
  65. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/tools.py +0 -0
  66. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/trajectory.py +0 -0
  67. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/triage.py +0 -0
  68. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/validate_scenarios.py +0 -0
  69. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe/world.py +0 -0
  70. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  71. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  72. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/requires.txt +0 -0
  73. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/top_level.txt +0 -0
  74. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/setup.cfg +0 -0
  75. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_access_control_agent.py +0 -0
  76. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_access_control_domain.py +0 -0
  77. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_access_control_rule_based_agent.py +0 -0
  78. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_access_control_scenarios.py +0 -0
  79. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_access_control_tools.py +0 -0
  80. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_agreement.py +0 -0
  81. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_browser_agent.py +0 -0
  82. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_classifier.py +0 -0
  83. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_cli.py +0 -0
  84. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_complex_access_control_agent.py +0 -0
  85. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_diff.py +0 -0
  86. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_domain.py +0 -0
  87. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_failures.py +0 -0
  88. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_feedback.py +0 -0
  89. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_generic_world.py +0 -0
  90. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_injection.py +0 -0
  91. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_injector.py +0 -0
  92. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_llm.py +0 -0
  93. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_package_api.py +0 -0
  94. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_playbook.py +0 -0
  95. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_priority.py +0 -0
  96. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_quickstart.py +0 -0
  97. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_reachability.py +0 -0
  98. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_registry.py +0 -0
  99. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_report.py +0 -0
  100. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_rule_based_agent.py +0 -0
  101. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_runner.py +0 -0
  102. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_scenarios.py +0 -0
  103. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_target_agent.py +0 -0
  104. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_termui.py +0 -0
  105. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_tools.py +0 -0
  106. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_trajectory.py +0 -0
  107. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_triage.py +0 -0
  108. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_validate_scenarios.py +0 -0
  109. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_web_browsing_domain.py +0 -0
  110. {agentprobe_testing-1.3.0 → agentprobe_testing-1.4.0}/tests/test_world.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
5
5
  License: Business Source License 1.1
6
6
 
@@ -60,7 +60,7 @@ from agentprobe.scenario import CommitPattern, FactPattern, GoalSpec, Scenario
60
60
  from agentprobe.scenarios.registry import ALL_SCENARIOS as TICKET_SCENARIOS
61
61
  from agentprobe.scenarios.registry import BY_ID as TICKET_SCENARIOS_BY_ID
62
62
 
63
- __version__ = "1.3.0"
63
+ __version__ = "1.4.0"
64
64
 
65
65
  __all__ = [
66
66
  "Agent",
@@ -17,7 +17,7 @@ from typing import Any, Callable, Optional
17
17
  from agentprobe.agents.base import Agent, AgentAction
18
18
  from agentprobe.domain import Domain, TICKET_DOMAIN
19
19
  from agentprobe.injection import injection_was_triggered
20
- from agentprobe.injector import Injector, NullInjector, RecordingInjector, serialize_armed_injection
20
+ from agentprobe.injector import Injector, ModelInjector, NullInjector, RecordingInjector, ReplayInjector, serialize_armed_injection
21
21
  from agentprobe.priority import clean_overrides
22
22
  from agentprobe.report import InjectionRecord, Report
23
23
  from agentprobe.runner import run_recovery, run_robustness_pair
@@ -186,6 +186,7 @@ def quick_test(
186
186
  injector = injector if injector is not None else NullInjector()
187
187
  target_factory = agent if _looks_like_factory(agent) else wrap_agent(agent)
188
188
  resolved_injector_model = injector_model if injector_model is not None else type(injector).__name__
189
+ injection_mode = _injection_mode(injector)
189
190
 
190
191
  recording: Optional[RecordingInjector] = None
191
192
  if record_injections:
@@ -217,6 +218,7 @@ def quick_test(
217
218
  injection_records=injection_records,
218
219
  injector_cost_usd=_injector_cost_usd(injector),
219
220
  priority_overrides=clean_overrides(priority_overrides),
221
+ injection_mode=injection_mode,
220
222
  )
221
223
  if recording is not None:
222
224
  # Stashed here (not a Report field -- Report is a general-purpose
@@ -300,6 +302,7 @@ def quick_test_all(
300
302
  all_records: list[InjectionRecord] = []
301
303
  total_injector_cost = 0.0
302
304
  combined_injector_model: Optional[str] = None
305
+ combined_injection_mode: Optional[str] = None
303
306
  start = time.monotonic()
304
307
  scenarios_run = 0
305
308
 
@@ -311,6 +314,7 @@ def quick_test_all(
311
314
  resolved_injector_model = injector_model if injector_model is not None else type(injector).__name__
312
315
  if combined_injector_model is None:
313
316
  combined_injector_model = resolved_injector_model
317
+ combined_injection_mode = _injection_mode(injector)
314
318
 
315
319
  recording: Optional[RecordingInjector] = None
316
320
  if record_injections:
@@ -357,6 +361,7 @@ def quick_test_all(
357
361
  injection_records=all_records,
358
362
  injector_cost_usd=total_injector_cost,
359
363
  priority_overrides=clean_overrides(priority_overrides),
364
+ injection_mode=combined_injection_mode,
360
365
  )
361
366
  report._scenarios_run = len(all_chaos)
362
367
  if summarize_failures:
@@ -396,6 +401,21 @@ def assert_passes(report: Report, save_html_on_failure: Optional[str] = None) ->
396
401
  raise AssertionError("agentprobe: " + "; ".join(failures))
397
402
 
398
403
 
404
+ def _injection_mode(injector: Injector) -> Optional[str]:
405
+ """How this run's failures were chosen, for labeling its report: an
406
+ adaptive ModelInjector decides afresh each time ("exploration" -- finds new
407
+ weaknesses, but two runs aren't directly comparable), a ReplayInjector
408
+ re-fires a recorded sequence ("pinned" -- the same failures every time, so
409
+ scores compare). Anything else (none, hardcoded) isn't labeled. Looks
410
+ through a RecordingInjector's wrapping."""
411
+ inner = getattr(injector, "_inner", injector)
412
+ if isinstance(inner, ReplayInjector):
413
+ return "pinned"
414
+ if isinstance(inner, ModelInjector):
415
+ return "exploration"
416
+ return None
417
+
418
+
399
419
  def _injector_cost_usd(injector: Injector) -> float:
400
420
  """ModelInjector is the only injector type with real API cost (none/
401
421
  hardcoded/replay never call a model) -- same logic as cli.py's version,
@@ -175,6 +175,7 @@ def upload_run(
175
175
  # caller's explicit consent to pool them, anonymously, into
176
176
  # aggregate benchmarks; False (the default) keeps them private.
177
177
  "findings": report.findings(),
178
+ "injection_mode": report.injection_mode,
178
179
  "share_anonymized": bool(share_anonymized),
179
180
  }
180
181
  if recorded_injections is None:
@@ -306,6 +306,11 @@ class Report:
306
306
  holds Trajectories/InjectionRecords, not the Injector instances
307
307
  themselves. 0.0 (and omitted from output) for none/hardcoded/replay
308
308
  injectors, which never call a model at all."""
309
+ injection_mode: Optional[str] = None
310
+ """How this run's failures were chosen: "exploration" (an adaptive injector
311
+ decided them -- finds new weaknesses, but two runs aren't directly
312
+ comparable) or "pinned" (a recorded sequence was replayed -- the same
313
+ failures every time, so scores compare). None when unknown/unlabeled."""
309
314
  priority_overrides: dict[str, str] = field(default_factory=dict)
310
315
  """Tiers (High/Medium/Low) the reader has set for particular failure kinds,
311
316
  e.g. {"FALSE_SUCCESS": "High"}. They replace the automatic estimate in
@@ -412,6 +417,13 @@ class Report:
412
417
  )
413
418
  return out
414
419
 
420
+ def injection_mode_label(self) -> Optional[str]:
421
+ """Plain-language form of injection_mode for report headers."""
422
+ return {
423
+ "exploration": "adaptive failures (exploration: not directly comparable with other runs)",
424
+ "pinned": "replayed failures (pinned: comparable with other replays of the same recording)",
425
+ }.get(self.injection_mode or "")
426
+
415
427
  def priorities(self) -> list[dict]:
416
428
  """The run's issues ranked highest to lowest priority, with any
417
429
  `priority_overrides` applied. See priority.py for how it's scored."""
@@ -475,7 +487,7 @@ class Report:
475
487
  lines = [
476
488
  blue(
477
489
  f"scenarios: {c['n']} mode: {self.mode} injector: {self.injector_model} "
478
- f"target: {self.target_model}"
490
+ f"target: {self.target_model}" + (f" failures: {self.injection_mode_label()}" if self.injection_mode_label() else "")
479
491
  ),
480
492
  ]
481
493
  if c["clean_passed"] is not None:
@@ -811,7 +823,7 @@ class Report:
811
823
  <div class="page">
812
824
  <header>
813
825
  <div class="title-row"><span class="health-dot health-dot--{health}"></span><h1>AgentProbe report</h1></div>
814
- <div class="subtitle">mode: {esc(self.mode)} &middot; injector: {esc(self.injector_model)} &middot; target: {esc(self.target_model)} &middot; {esc(c['n'])} scenarios</div>
826
+ <div class="subtitle">mode: {esc(self.mode)} &middot; injector: {esc(self.injector_model)} &middot; target: {esc(self.target_model)} &middot; {esc(c['n'])} scenarios{(' &middot; ' + esc(self.injection_mode_label())) if self.injection_mode_label() else ''}</div>
815
827
  </header>
816
828
  {injector_error_banner}
817
829
  <div class="stats">
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
5
5
  License: Business Source License 1.1
6
6
 
@@ -85,6 +85,7 @@ tests/test_failures.py
85
85
  tests/test_feedback.py
86
86
  tests/test_generic_world.py
87
87
  tests/test_injection.py
88
+ tests/test_injection_mode.py
88
89
  tests/test_injector.py
89
90
  tests/test_llm.py
90
91
  tests/test_package_api.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentprobe-testing"
3
- version = "1.3.0"
3
+ version = "1.4.0"
4
4
  description = "Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = ["anthropic>=1.0.0", "python-dotenv>=1.0.0"]
@@ -0,0 +1,80 @@
1
+ """Labeling a run exploration (adaptive injector) or pinned (replayed recording)."""
2
+
3
+ from agentprobe.agents.scripted import ScriptedAgent
4
+ from agentprobe.injection import ArmedInjection, Injection, InjectionKind, ResponseSignature, Trigger
5
+ from agentprobe.injector import HardcodedToolErrorInjector, ModelInjector, NullInjector, RecordingInjector, ReplayInjector, serialize_armed_injection, deserialize_armed_injection
6
+ from agentprobe.quickstart import _injection_mode, quick_test, quick_test_all
7
+ from agentprobe.registry import upload_run
8
+ from agentprobe.scenarios.registry import BY_ID
9
+
10
+ AGENT = lambda: ScriptedAgent(script=[("get_ticket", {"id": "T-1001"})])
11
+
12
+
13
+ def _armed(step=1):
14
+ return ArmedInjection(
15
+ injection=Injection(kind=InjectionKind.TOOL_ERROR, payload={"tool_name": "close_ticket", "count": 1}, intent="i",
16
+ expected_signature=ResponseSignature("s"), rationale="r"),
17
+ trigger=Trigger.on_next_action(),
18
+ armed_at_step=step,
19
+ )
20
+
21
+
22
+ def test_injection_mode_reads_the_injector_type_through_a_recording_wrapper():
23
+ adaptive = ModelInjector.__new__(ModelInjector) # only its type matters here; no client needed
24
+ replay = ReplayInjector([_armed()])
25
+ assert _injection_mode(adaptive) == "exploration"
26
+ assert _injection_mode(replay) == "pinned"
27
+ assert _injection_mode(RecordingInjector(adaptive)) == "exploration"
28
+ assert _injection_mode(RecordingInjector(replay)) == "pinned"
29
+ assert _injection_mode(NullInjector()) is None
30
+ assert _injection_mode(HardcodedToolErrorInjector()) is None
31
+
32
+
33
+ def test_a_replayed_run_is_labeled_pinned_in_the_report():
34
+ report = quick_test(BY_ID["clean-1"], AGENT, injector=ReplayInjector([]), mode="recovery")
35
+ assert report.injection_mode == "pinned"
36
+ assert "replayed failures (pinned: comparable with other replays of the same recording)" in report.render_html()
37
+ assert "failures: replayed failures (pinned" in report.render()
38
+
39
+
40
+ def test_recording_a_replay_still_labels_it_pinned():
41
+ report = quick_test(BY_ID["clean-1"], AGENT, injector=ReplayInjector([]), mode="recovery", record_injections=True)
42
+ assert report.injection_mode == "pinned"
43
+
44
+
45
+ def test_an_unlabeled_injector_adds_no_mode_text():
46
+ report = quick_test(BY_ID["clean-1"], AGENT, injector=NullInjector(), mode="recovery")
47
+ assert report.injection_mode is None and report.injection_mode_label() is None
48
+ assert "pinned" not in report.render_html() and "exploration" not in report.render_html()
49
+
50
+
51
+ def test_the_exploration_label_says_runs_are_not_directly_comparable():
52
+ from agentprobe.report import Report
53
+
54
+ r = Report("robustness", "m", "t", [], [], injection_mode="exploration")
55
+ assert "adaptive failures (exploration: not directly comparable with other runs)" in r.render_html()
56
+
57
+
58
+ def test_quick_test_all_labels_the_combined_report_from_its_injectors():
59
+ scenarios = {k: BY_ID[k] for k in ("clean-1", "clean-2")}
60
+ report = quick_test_all(scenarios, AGENT, injector_factory=lambda: ReplayInjector([]), mode="recovery")
61
+ assert report.injection_mode == "pinned"
62
+
63
+
64
+ def test_upload_run_sends_the_mode_so_the_website_can_label_it():
65
+ sent = {}
66
+ report = quick_test(BY_ID["clean-1"], AGENT, injector=ReplayInjector([]), mode="recovery")
67
+ upload_run(report, api_key="k", poster=lambda url, body, key: sent.update(body))
68
+ assert sent["injection_mode"] == "pinned"
69
+ report2 = quick_test(BY_ID["clean-1"], AGENT, injector=NullInjector(), mode="recovery")
70
+ upload_run(report2, api_key="k", poster=lambda url, body, key: sent.update(body))
71
+ assert sent["injection_mode"] is None
72
+
73
+
74
+ def test_a_serialized_recording_survives_the_round_trip_the_website_relies_on():
75
+ # The website stores serialize_armed_injection() dicts and the runner rebuilds them with
76
+ # deserialize_armed_injection() before replaying -- this is the contract between the two.
77
+ original = [_armed(2), _armed(5)]
78
+ rebuilt = ReplayInjector([deserialize_armed_injection(serialize_armed_injection(a)) for a in original])
79
+ assert sorted(rebuilt._by_step) == [2, 5]
80
+ assert rebuilt._by_step[2].injection.kind == InjectionKind.TOOL_ERROR