agentprobe-testing 0.5.1__tar.gz → 0.5.3__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 (92) hide show
  1. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/PKG-INFO +1 -1
  2. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/README.md +32 -14
  3. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/__init__.py +1 -1
  4. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/quickstart.py +22 -2
  5. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/registry.py +6 -6
  6. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/report.py +4 -4
  7. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe_testing.egg-info/PKG-INFO +1 -1
  8. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/pyproject.toml +1 -1
  9. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_quickstart.py +23 -0
  10. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_registry.py +2 -2
  11. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/LICENSE +0 -0
  12. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/agents/__init__.py +0 -0
  13. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/agents/base.py +0 -0
  14. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/agents/rule_based.py +0 -0
  15. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/agents/scripted.py +0 -0
  16. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/agents/target_agent.py +0 -0
  17. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/agreement.py +0 -0
  18. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/classifier.py +0 -0
  19. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/cli.py +0 -0
  20. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/diff.py +0 -0
  21. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domain.py +0 -0
  22. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/__init__.py +0 -0
  23. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/__init__.py +0 -0
  24. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/agent.py +0 -0
  25. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/clean.py +0 -0
  26. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/complex_agent.py +0 -0
  27. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/decoy.py +0 -0
  28. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/domain.py +0 -0
  29. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/entities.py +0 -0
  30. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  31. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  32. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/scenarios.py +0 -0
  33. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/split.py +0 -0
  34. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/tools.py +0 -0
  35. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/domains/access_control/trap.py +0 -0
  36. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/feedback.py +0 -0
  37. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/generic_world.py +0 -0
  38. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/injection.py +0 -0
  39. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/injector.py +0 -0
  40. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/llm.py +0 -0
  41. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/playbook.py +0 -0
  42. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/reachability.py +0 -0
  43. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/runner.py +0 -0
  44. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/scenario.py +0 -0
  45. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/scenarios/__init__.py +0 -0
  46. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/scenarios/clean.py +0 -0
  47. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/scenarios/decoy.py +0 -0
  48. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/scenarios/registry.py +0 -0
  49. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/scenarios/split.py +0 -0
  50. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/scenarios/trap.py +0 -0
  51. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/termui.py +0 -0
  52. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/tools.py +0 -0
  53. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/trajectory.py +0 -0
  54. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/triage.py +0 -0
  55. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/validate_scenarios.py +0 -0
  56. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe/world.py +0 -0
  57. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe_testing.egg-info/SOURCES.txt +0 -0
  58. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  59. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  60. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe_testing.egg-info/requires.txt +0 -0
  61. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/agentprobe_testing.egg-info/top_level.txt +0 -0
  62. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/setup.cfg +0 -0
  63. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_access_control_agent.py +0 -0
  64. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_access_control_domain.py +0 -0
  65. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_access_control_rule_based_agent.py +0 -0
  66. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_access_control_scenarios.py +0 -0
  67. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_access_control_tools.py +0 -0
  68. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_agreement.py +0 -0
  69. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_classifier.py +0 -0
  70. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_cli.py +0 -0
  71. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_complex_access_control_agent.py +0 -0
  72. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_diff.py +0 -0
  73. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_domain.py +0 -0
  74. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_feedback.py +0 -0
  75. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_generic_world.py +0 -0
  76. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_injection.py +0 -0
  77. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_injector.py +0 -0
  78. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_llm.py +0 -0
  79. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_package_api.py +0 -0
  80. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_playbook.py +0 -0
  81. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_reachability.py +0 -0
  82. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_report.py +0 -0
  83. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_rule_based_agent.py +0 -0
  84. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_runner.py +0 -0
  85. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_scenarios.py +0 -0
  86. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_target_agent.py +0 -0
  87. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_termui.py +0 -0
  88. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_tools.py +0 -0
  89. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_trajectory.py +0 -0
  90. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_triage.py +0 -0
  91. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_validate_scenarios.py +0 -0
  92. {agentprobe_testing-0.5.1 → agentprobe_testing-0.5.3}/tests/test_world.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 0.5.1
3
+ Version: 0.5.3
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
 
@@ -185,15 +185,18 @@ Writing `agentprobe/domain.py`'s `Domain` bundle by hand (entities, tools,
185
185
  business rules) is real, unavoidable work — see "Domains" above. If you'd
186
186
  rather not write it yourself, [agentprobe-api.agentprobe.workers.dev](https://agentprobe-api.agentprobe.workers.dev)
187
187
  can generate one from your existing tool schemas plus a plain-English
188
- description of your business rules, and hand you back a key:
188
+ description of your business rules, and hand you back a key — either via
189
+ [/generate](https://agentprobe-api.agentprobe.workers.dev/generate) on the
190
+ site (sign in, fill in a form, get the key and the generated source back
191
+ right on the page — no `curl` needed) or the raw API:
189
192
 
190
193
  ```python
191
194
  import agentprobe as ap
192
195
 
193
- fetched = ap.fetch_domain("joeblow-ai-agents_bd0725d86767c7481581fc41")
196
+ fetched = ap.fetch_domain("meridian-ai-agents_bd0725d86767c7481581fc41")
194
197
  report = ap.quick_test(
195
198
  fetched.scenarios["wrong-order-among-several"],
196
- lambda: ap.TargetAgent(system_prompt="You are JoeBlow's order support agent."),
199
+ lambda: ap.TargetAgent(system_prompt="You are Meridian's order support agent."),
197
200
  domain=fetched.domain,
198
201
  )
199
202
  print(report.render())
@@ -210,19 +213,34 @@ it just makes your test pass without proving anything real about your
210
213
  agent. Only fetch a key you generated for your own business, the same
211
214
  rule as never installing a package from someone you don't trust.
212
215
 
213
- ## The website: accounts, personal API keys, and a dashboard
216
+ ## The website: sign in and run a real test in your browser
214
217
 
215
218
  [agentprobe-api.agentprobe.workers.dev](https://agentprobe-api.agentprobe.workers.dev)
216
219
  is the real site, not just the API — sign in with GitHub and you get:
217
220
 
218
- - **Your Keys** — every domain you've generated (via `/v1/generate` with
219
- your API key attached), click-to-copy, so you don't have to keep your
220
- own notes of which key was which business.
221
- - **Your Runs** — a history of test runs you've uploaded, each with its
222
- full report viewable in the browser.
223
- - **API access** (on the Dashboard) — create a personal key
224
- (`ap_live_...`, shown once at creation) to authenticate the two things
225
- above from your own scripts.
221
+ - **`/run`** — one flow, describing to running: describe your business in
222
+ plain English (same generation as `POST /v1/generate`, as a web form
223
+ instead of `curl`), and it writes a real test domain for it, then falls
224
+ straight into picking a scenario from a real dropdown (or "All
225
+ scenarios" to run every one, combined into a single report), naming the
226
+ run, and pointing at your agent's HTTP endpoint plus your own Anthropic
227
+ API key. It runs in the background — the page comes back immediately so
228
+ you can go do something else on the site while it finishes, and it
229
+ shows up under Your Runs once it does. Free to use — you're billed by
230
+ Anthropic for your own API usage, the same as running it yourself. See
231
+ [`runner/README.md`](runner/README.md) for how this actually executes server-side (the real `agentprobe-testing`
232
+ package, not a reimplementation).
233
+ - **Your Runs** — a history of test runs you've uploaded or executed,
234
+ each with its full report viewable in the browser, and a Rerun link
235
+ that jumps straight back into `/run`'s scenario/agent step for any run
236
+ that was started from the site.
237
+
238
+ There's no separate domain-key page on the site anymore — a domain
239
+ generated through `/run` is only ever referenced internally, not
240
+ something you need to copy around. A personal API key
241
+ (`ap_live_...`) for scripting the CLI/library against your account, if
242
+ your account has one, is issued separately from the website (there's no
243
+ self-serve "create a key" page).
226
244
 
227
245
  Attribute a generated domain to your account by passing the key as a
228
246
  bearer token:
@@ -240,7 +258,7 @@ same key to `quick_test()`:
240
258
  ```python
241
259
  report = ap.quick_test(
242
260
  scenario, my_agent, domain=fetched.domain,
243
- upload_api_key="ap_live_...", # from Your Dashboard
261
+ upload_api_key="ap_live_...", # a personal key issued for your account
244
262
  domain_key=fetched_key, # optional -- links the run back to its domain
245
263
  )
246
264
  ```
@@ -288,7 +306,7 @@ The website/API server (`server/`) has its own separate suite, same rule:
288
306
  cd server && node --test test/
289
307
  ```
290
308
 
291
- 90+ tests against fake D1/KV/fetch — no real Cloudflare account, network
309
+ 160+ tests against fake D1/KV/fetch — no real Cloudflare account, network
292
310
  access, or secrets needed for either suite. Both run in CI on every push.
293
311
 
294
312
  See [CHANGELOG.md](CHANGELOG.md) for what's new in each version.
@@ -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__ = "0.5.1"
63
+ __version__ = "0.5.3"
64
64
 
65
65
  __all__ = [
66
66
  "Agent",
@@ -10,6 +10,7 @@ what to do next, test it against one scenario."
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
13
+ import time
13
14
  import urllib.request
14
15
  from typing import Any, Callable, Optional
15
16
 
@@ -135,7 +136,7 @@ def quick_test(
135
136
  or a plain decide_fn(task, history) -> AgentAction, which gets wrap_agent()
136
137
  applied automatically -- either way, nothing else here changes.
137
138
 
138
- Pass `upload_api_key` (a personal key from /dashboard) to also upload the
139
+ Pass `upload_api_key` (a personal key issued for your account) to also upload the
139
140
  finished report to your AgentProbe account, so it shows up under Your
140
141
  Runs -- see registry.upload_run for what this does and its failure
141
142
  behavior. Pass `domain_key` too if `domain` came from fetch_domain(), so
@@ -209,6 +210,7 @@ def quick_test_all(
209
210
  upload_api_key: Optional[str] = None,
210
211
  domain_key: Optional[str] = None,
211
212
  record_injections: bool = False,
213
+ max_duration_seconds: Optional[float] = None,
212
214
  ) -> Report:
213
215
  """Run every scenario in `scenarios` (a dict[str, Scenario] -- e.g.
214
216
  TICKET_SCENARIOS or a FetchedDomain's .scenarios -- or a plain list of
@@ -228,6 +230,16 @@ def quick_test_all(
228
230
  run (so Your Runs shows them individually, the same granularity as
229
231
  calling quick_test() per scenario and uploading each) -- the Report
230
232
  this function returns is still the combined summary across all of them.
233
+
234
+ `max_duration_seconds`, if given, stops starting new scenarios once
235
+ that much wall-clock time has passed since the call began -- for a
236
+ caller with its own hard execution ceiling (e.g. a serverless platform
237
+ that kills a job outright past some limit, which can't be caught or
238
+ cleaned up after), a report covering fewer scenarios than asked for is
239
+ far better than getting killed mid-scenario with nothing to show for
240
+ it. The returned Report's `_scenarios_run`/`_scenarios_total`
241
+ attributes say whether this happened, so a caller can tell the
242
+ difference between "ran everything" and "stopped early."
231
243
  """
232
244
  items = list(scenarios.items()) if isinstance(scenarios, dict) else [(s.id, s) for s in scenarios]
233
245
  if not items:
@@ -246,8 +258,13 @@ def quick_test_all(
246
258
  all_records: list[InjectionRecord] = []
247
259
  total_injector_cost = 0.0
248
260
  combined_injector_model: Optional[str] = None
261
+ start = time.monotonic()
262
+ scenarios_run = 0
249
263
 
250
264
  for _scenario_id, scenario in items:
265
+ if max_duration_seconds is not None and scenarios_run > 0 and (time.monotonic() - start) >= max_duration_seconds:
266
+ break # already have at least one scenario's worth of real results -- stop here, not mid-scenario
267
+ scenarios_run += 1
251
268
  injector = make_injector()
252
269
  resolved_injector_model = injector_model if injector_model is not None else type(injector).__name__
253
270
  if combined_injector_model is None:
@@ -289,7 +306,7 @@ def quick_test_all(
289
306
  recorded = [serialize_armed_injection(a) for a in recording.recorded] if recording is not None else None
290
307
  upload_run(scenario_report, api_key=upload_api_key, domain_key=domain_key, recorded_injections=recorded)
291
308
 
292
- return Report(
309
+ report = Report(
293
310
  mode=mode,
294
311
  injector_model=combined_injector_model or "none",
295
312
  target_model=target_model,
@@ -298,6 +315,9 @@ def quick_test_all(
298
315
  injection_records=all_records,
299
316
  injector_cost_usd=total_injector_cost,
300
317
  )
318
+ report._scenarios_run = len(all_chaos)
319
+ report._scenarios_total = len(items)
320
+ return report
301
321
 
302
322
 
303
323
  def assert_passes(report: Report, save_html_on_failure: Optional[str] = None) -> None:
@@ -125,11 +125,11 @@ def upload_run(
125
125
  recorded_injections: Optional[list[dict]] = None,
126
126
  ) -> None:
127
127
  """Upload a finished quick_test() report to your AgentProbe account (POST
128
- /v1/runs) so it shows up under Your Runs on the dashboard. `api_key` is a
129
- personal key created on /dashboard -- create one there, it's never shown
130
- again after creation so save it somewhere real (an env var, not a
131
- committed file). `report` must cover exactly one scenario, same as every
132
- report quick_test() itself produces.
128
+ /v1/runs) so it shows up under Your Runs on the website. `api_key` is a
129
+ personal key issued for your account -- it's never shown again after
130
+ issuance so save it somewhere real (an env var, not a committed file).
131
+ `report` must cover exactly one scenario, same as every report
132
+ quick_test() itself produces.
133
133
 
134
134
  Pass `recorded_injections` (a list of agentprobe.injector.
135
135
  serialize_armed_injection() dicts) to make the uploaded run replayable
@@ -274,7 +274,7 @@ def replay_run(
274
274
  target_model: str = "custom",
275
275
  ) -> "Report":
276
276
  """Replay the exact chaos sequence recorded for `run_id` (an id from
277
- your dashboard's Your Runs table, or a RegressionResult.previous_run_id)
277
+ the website's Your Runs table, or a RegressionResult.previous_run_id)
278
278
  against a new agent or model version, instead of paying for a fresh
279
279
  ModelInjector decision sequence -- only the Target's own calls repeat.
280
280
 
@@ -564,14 +564,14 @@ class Report:
564
564
  {classification_html}
565
565
  </section>
566
566
  <section>
567
- <h2>By injection kind &mdash; handled rate</h2>
567
+ <h2>By injection kind: handled rate</h2>
568
568
  {kind_table}
569
569
  </section>
570
570
  <section>
571
- <h2>Scenario detail &mdash; step by step</h2>
571
+ <h2>Scenario detail: step by step</h2>
572
572
  {scenario_detail_html}
573
573
  </section>
574
- <footer>Generated by AgentProbe &mdash; adaptive chaos-testing for LLM agents.</footer>
574
+ <footer>Generated by AgentProbe. Adaptive chaos-testing for LLM agents.</footer>
575
575
  </div>
576
576
  </body></html>"""
577
577
 
@@ -621,7 +621,7 @@ class Report:
621
621
  notes = ""
622
622
  for a in traj.injections:
623
623
  note_class = "injection-note" if a.valid else "injection-note injection-note--invalid"
624
- validity = "" if a.valid else " &mdash; <strong>INVALID</strong>"
624
+ validity = "" if a.valid else " (<strong>INVALID</strong>)"
625
625
  notes += (
626
626
  f"<div class='{note_class}'>&#9889; injection fired: <strong>{esc(a.injection.kind.value)}</strong> "
627
627
  f"@ step {a.fired_at_step} (trigger: <code>{esc(a.trigger_kind)}</code>){validity}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 0.5.1
3
+ Version: 0.5.3
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentprobe-testing"
3
- version = "0.5.1"
3
+ version = "0.5.3"
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"]
@@ -273,6 +273,29 @@ def test_quick_test_all_recovery_mode_has_no_clean_trajectories():
273
273
  assert len(report.chaos_trajectories) == 2
274
274
 
275
275
 
276
+ def test_quick_test_all_reports_scenarios_run_and_total_when_nothing_stops_it_early():
277
+ scenarios = {"clean-1": BY_ID["clean-1"], "clean-2": BY_ID["clean-2"]}
278
+ report = quick_test_all(scenarios, lambda: RuleBasedAgent())
279
+ assert report._scenarios_run == 2
280
+ assert report._scenarios_total == 2
281
+
282
+
283
+ def test_quick_test_all_stops_early_past_max_duration_seconds_but_always_runs_at_least_one():
284
+ # A caller with its own hard execution ceiling (e.g. a serverless
285
+ # platform that kills the whole job outright past some limit, with no
286
+ # chance to clean up) needs a report covering fewer scenarios instead
287
+ # of getting killed mid-run with nothing to show. max_duration_seconds=0
288
+ # means "already over budget" as soon as any real time has passed --
289
+ # deterministic regardless of how fast RuleBasedAgent actually runs,
290
+ # since real wall-clock time (time.monotonic()) always advances by a
291
+ # nonzero amount between the loop's start and its first elapsed check.
292
+ scenarios = {"clean-1": BY_ID["clean-1"], "clean-2": BY_ID["clean-2"], "clean-1-again": BY_ID["clean-1"]}
293
+ report = quick_test_all(scenarios, lambda: RuleBasedAgent(), max_duration_seconds=0)
294
+ assert report._scenarios_run == 1 # never zero -- see the "always run at least one" guard
295
+ assert report._scenarios_total == 3
296
+ assert report._compute()["n"] == 1 # the returned Report itself only covers what actually ran
297
+
298
+
276
299
  def test_quick_test_all_uploads_each_scenario_as_its_own_run():
277
300
  import agentprobe.registry as registry_module
278
301
 
@@ -175,9 +175,9 @@ def test_upload_run_includes_domain_key_when_given():
175
175
  report = quick_test(scenario, _clean1_decide)
176
176
  poster = make_poster()
177
177
 
178
- upload_run(report, api_key="ap_live_test123", domain_key="joeblow_1234", poster=poster)
178
+ upload_run(report, api_key="ap_live_test123", domain_key="meridian_1234", poster=poster)
179
179
 
180
- assert poster.calls[0]["body"]["domain_key"] == "joeblow_1234"
180
+ assert poster.calls[0]["body"]["domain_key"] == "meridian_1234"
181
181
 
182
182
 
183
183
  def test_upload_run_rejects_a_report_covering_more_or_fewer_than_one_scenario():