applied-cli 0.6.4__tar.gz → 0.6.5__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 (69) hide show
  1. {applied_cli-0.6.4 → applied_cli-0.6.5}/PKG-INFO +1 -1
  2. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/cli.py +12 -0
  3. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/tools.py +89 -6
  4. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/scenarios.py +47 -1
  5. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/PKG-INFO +1 -1
  6. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/SOURCES.txt +1 -0
  7. {applied_cli-0.6.4 → applied_cli-0.6.5}/pyproject.toml +1 -1
  8. applied_cli-0.6.5/tests/test_scenario_bulk_run_wait.py +107 -0
  9. {applied_cli-0.6.4 → applied_cli-0.6.5}/README.md +0 -0
  10. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/__init__.py +0 -0
  11. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/agent_scoped_flows.py +0 -0
  12. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/auth.py +0 -0
  13. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/client.py +0 -0
  14. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/conversation_lookup.py +0 -0
  15. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/conversations.py +0 -0
  16. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/credentials.py +0 -0
  17. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/flow_helpers.py +0 -0
  18. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/formatters.py +0 -0
  19. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/mcp.py +0 -0
  20. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/recovery.py +0 -0
  21. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/toolkit.py +0 -0
  22. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/__init__.py +0 -0
  23. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/agents.py +0 -0
  24. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/articles.py +0 -0
  25. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/catalog.py +0 -0
  26. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/connectors.py +0 -0
  27. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/content.py +0 -0
  28. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/conversations.py +0 -0
  29. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/domains.py +0 -0
  30. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/flows.py +0 -0
  31. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/knowledge.py +0 -0
  32. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/manifest.py +0 -0
  33. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/products.py +0 -0
  34. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/taxonomy.py +0 -0
  35. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/tickets.py +0 -0
  36. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/dependency_links.txt +0 -0
  37. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/entry_points.txt +0 -0
  38. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/requires.txt +0 -0
  39. {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/top_level.txt +0 -0
  40. {applied_cli-0.6.4 → applied_cli-0.6.5}/setup.cfg +0 -0
  41. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_agent_scoped_flows.py +0 -0
  42. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_audit_tools.py +0 -0
  43. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_auth_context.py +0 -0
  44. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_benchmark_clone.py +0 -0
  45. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_benchmark_delete_guardrail.py +0 -0
  46. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_benchmark_scenario_tools.py +0 -0
  47. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_cli.py +0 -0
  48. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_cli_v2.py +0 -0
  49. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_client.py +0 -0
  50. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_client_v2.py +0 -0
  51. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_conversation_tools.py +0 -0
  52. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_flow_tools.py +0 -0
  53. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_knowledge_content_tools.py +0 -0
  54. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_recovery.py +0 -0
  55. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_scenario_bulk_cancel.py +0 -0
  56. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_scenario_bulk_run_contact.py +0 -0
  57. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_toolkit_contract.py +0 -0
  58. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_agents.py +0 -0
  59. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_articles.py +0 -0
  60. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_catalog_and_mcp.py +0 -0
  61. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_connectors.py +0 -0
  62. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_content.py +0 -0
  63. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_conversations.py +0 -0
  64. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_flows.py +0 -0
  65. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_knowledge.py +0 -0
  66. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_products.py +0 -0
  67. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_scenarios.py +0 -0
  68. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_taxonomy.py +0 -0
  69. {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_tickets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.6.4
3
+ Version: 0.6.5
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -1928,6 +1928,15 @@ def scenario_bulk_run(
1928
1928
  anonymous: bool = typer.Option(
1929
1929
  False, "--anonymous", help="Run with an anonymous contact"
1930
1930
  ),
1931
+ wait: bool = typer.Option(
1932
+ False, "--wait", help="Poll until all runs finish, then print final status"
1933
+ ),
1934
+ wait_timeout: float = typer.Option(
1935
+ 300.0, "--wait-timeout", help="Max seconds to wait with --wait (default 300)"
1936
+ ),
1937
+ poll_interval: float = typer.Option(
1938
+ 3.0, "--poll-interval", help="Seconds between status polls with --wait"
1939
+ ),
1931
1940
  shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1932
1941
  format: str = typer.Option(
1933
1942
  "text", "--format", "-f", help="Output format: text or json"
@@ -1944,6 +1953,9 @@ def scenario_bulk_run(
1944
1953
  contact_email=contact_email,
1945
1954
  contact_id=contact_id,
1946
1955
  anonymous=anonymous,
1956
+ wait=wait,
1957
+ wait_timeout=wait_timeout,
1958
+ poll_interval=poll_interval,
1947
1959
  output_format=format,
1948
1960
  )
1949
1961
  )
@@ -8,6 +8,7 @@ import asyncio
8
8
  import difflib
9
9
  import json
10
10
  import re
11
+ import time
11
12
  from contextlib import suppress
12
13
  from html.parser import HTMLParser
13
14
  from typing import Any
@@ -6082,6 +6083,41 @@ async def scenario_run_delete(
6082
6083
  return f"Scenario run {run_id} deleted successfully."
6083
6084
 
6084
6085
 
6086
+ def _bulk_status_counts(counts: dict | None) -> dict[str, int]:
6087
+ """Normalize bulk-status counts to lowercase keys with int values."""
6088
+ lowered: dict[str, int] = {}
6089
+ for key, value in (counts or {}).items():
6090
+ lowered[str(key).lower()] = int(value or 0)
6091
+ return lowered
6092
+
6093
+
6094
+ def _bulk_pending_count(counts: dict | None) -> int:
6095
+ """Count runs still queued or running (case-insensitive)."""
6096
+ normalized = _bulk_status_counts(counts)
6097
+ return normalized.get("queued", 0) + normalized.get("running", 0)
6098
+
6099
+
6100
+ async def _await_bulk_run(
6101
+ client: AppliedClient,
6102
+ job_id: str,
6103
+ *,
6104
+ timeout: float,
6105
+ poll_interval: float,
6106
+ ) -> tuple[dict, bool]:
6107
+ """Poll a bulk run until no runs are queued/running or the timeout elapses.
6108
+
6109
+ Returns (latest_status_payload, timed_out).
6110
+ """
6111
+ start = time.monotonic()
6112
+ status = await client.get_scenario_bulk_run_status(job_id)
6113
+ while _bulk_pending_count(status.get("counts")) > 0:
6114
+ if time.monotonic() - start >= timeout:
6115
+ return status, True
6116
+ await asyncio.sleep(poll_interval)
6117
+ status = await client.get_scenario_bulk_run_status(job_id)
6118
+ return status, False
6119
+
6120
+
6085
6121
  async def _resolve_contact_override(
6086
6122
  client: AppliedClient,
6087
6123
  *,
@@ -6123,6 +6159,9 @@ async def scenario_bulk_run(
6123
6159
  contact_id: str | None = None,
6124
6160
  contact_email: str | None = None,
6125
6161
  anonymous: bool = False,
6162
+ wait: bool = False,
6163
+ wait_timeout: float = 300.0,
6164
+ poll_interval: float = 3.0,
6126
6165
  output_format: str = "text",
6127
6166
  ) -> str:
6128
6167
  """
@@ -6134,6 +6173,10 @@ async def scenario_bulk_run(
6134
6173
  to run the scenarios as a contact that has an email, so the test conversation
6135
6174
  carries it.
6136
6175
 
6176
+ With wait=True, this polls until every run finishes (or the timeout elapses)
6177
+ and returns the final status, so you can run a benchmark and read results in
6178
+ one call instead of polling scenario_bulk_status yourself.
6179
+
6137
6180
  Args:
6138
6181
  client: Authenticated AppliedClient
6139
6182
  scenario_ids: List of scenario UUIDs to run
@@ -6144,9 +6187,12 @@ async def scenario_bulk_run(
6144
6187
  contact_id: Run scenarios as this existing contact (gives test convos its email)
6145
6188
  contact_email: Resolve/create a contact with this email and run as them
6146
6189
  anonymous: Run with an anonymous contact (mode='anonymous')
6190
+ wait: Poll until all runs finish (or wait_timeout elapses)
6191
+ wait_timeout: Max seconds to wait when wait=True (default 300)
6192
+ poll_interval: Seconds between status polls when wait=True (default 3)
6147
6193
 
6148
6194
  Returns:
6149
- Summary of runs created
6195
+ Summary of runs created (plus the final status when wait=True)
6150
6196
  """
6151
6197
  resolved_scenario_ids = list(scenario_ids or [])
6152
6198
  if not resolved_scenario_ids:
@@ -6196,6 +6242,26 @@ async def scenario_bulk_run(
6196
6242
  "contact_override": result.get("contact_override"),
6197
6243
  }
6198
6244
 
6245
+ job_id = payload.get("job_id")
6246
+ final_status: dict | None = None
6247
+ timed_out = False
6248
+ if wait and job_id:
6249
+ try:
6250
+ final_status, timed_out = await _await_bulk_run(
6251
+ client,
6252
+ str(job_id),
6253
+ timeout=wait_timeout,
6254
+ poll_interval=poll_interval,
6255
+ )
6256
+ except AppliedAPIError as e:
6257
+ return _format_error(e)
6258
+ counts = _bulk_status_counts(final_status.get("counts"))
6259
+ payload["final_counts"] = counts
6260
+ payload["timed_out"] = timed_out
6261
+ payload["duration_seconds"] = final_status.get("duration_seconds")
6262
+ payload["completed_at"] = final_status.get("completed_at")
6263
+ payload["failed"] = final_status.get("failed") or []
6264
+
6199
6265
  if output_format == "json":
6200
6266
  return to_json(payload)
6201
6267
 
@@ -6212,6 +6278,23 @@ async def scenario_bulk_run(
6212
6278
  output += f"scenario_run_ids: {preview_ids}\n"
6213
6279
  if len(run_ids) > 10:
6214
6280
  output += f"more_runs: {len(run_ids) - 10}\n"
6281
+
6282
+ if final_status is not None:
6283
+ counts = payload["final_counts"]
6284
+ output += "\n# Final Status\n"
6285
+ output += "timed_out: " + ("true (still pending)" if timed_out else "false") + "\n"
6286
+ output += f"completed: {counts.get('completed', 0)}\n"
6287
+ output += f"failed: {counts.get('failed', 0)}\n"
6288
+ pending = counts.get("queued", 0) + counts.get("running", 0)
6289
+ output += f"still_pending: {pending}\n"
6290
+ if payload.get("duration_seconds") is not None:
6291
+ output += f"duration_seconds: {payload['duration_seconds']}\n"
6292
+ failed_runs = payload.get("failed") or []
6293
+ if failed_runs:
6294
+ output += f"\n# Failed Runs ({len(failed_runs)})\n"
6295
+ output += to_json(failed_runs)
6296
+ return output
6297
+
6215
6298
  output += "\nTip: use scenario_bulk_status(job_id, include_runs=True) or scenario_run_list(bulk_job_id=job_id) to get per-run details with scenario mappings."
6216
6299
  return output
6217
6300
 
@@ -6245,14 +6328,14 @@ async def scenario_bulk_status(
6245
6328
  payload.pop("runs", None)
6246
6329
  return to_json(payload)
6247
6330
 
6248
- counts = result.get("counts") or {}
6331
+ counts = _bulk_status_counts(result.get("counts"))
6249
6332
  output = "# Bulk Run Status\n"
6250
6333
  output += f"job_id: {result.get('job_id')}\n"
6251
6334
  output += f"total: {result.get('total')}\n"
6252
- output += f"queued: {counts.get('QUEUED', 0)}\n"
6253
- output += f"running: {counts.get('RUNNING', 0)}\n"
6254
- output += f"completed: {counts.get('COMPLETED', 0)}\n"
6255
- output += f"failed: {counts.get('FAILED', 0)}\n"
6335
+ output += f"queued: {counts.get('queued', 0)}\n"
6336
+ output += f"running: {counts.get('running', 0)}\n"
6337
+ output += f"completed: {counts.get('completed', 0)}\n"
6338
+ output += f"failed: {counts.get('failed', 0)}\n"
6256
6339
  output += f"created_at: {result.get('created_at')}\n"
6257
6340
  output += f"updated_at: {result.get('updated_at')}\n"
6258
6341
  if result.get("completed_at"):
@@ -30,6 +30,9 @@ class ScenariosBulkRunInput(StrictInput):
30
30
  contact_id: str | None = None
31
31
  contact_email: str | None = None
32
32
  anonymous: bool = False
33
+ wait: bool = False
34
+ wait_timeout: float = 300.0
35
+ poll_interval: float = 3.0
33
36
 
34
37
 
35
38
  class ScenariosBulkCancelInput(StrictInput):
@@ -828,6 +831,48 @@ async def scenarios_bulk_run_handler(
828
831
  "duplicated_scenarios": result.get("duplicated_scenarios"),
829
832
  "contact_override": result.get("contact_override"),
830
833
  }
834
+
835
+ job_id = payload.get("job_id")
836
+ if params.wait and job_id:
837
+ from applied_cli.tools import _await_bulk_run, _bulk_status_counts
838
+
839
+ try:
840
+ final_status, timed_out = await _await_bulk_run(
841
+ client,
842
+ str(job_id),
843
+ timeout=params.wait_timeout,
844
+ poll_interval=params.poll_interval,
845
+ )
846
+ except AppliedAPIError as exc:
847
+ return _api_error_result(exc)
848
+ counts = _bulk_status_counts(final_status.get("counts"))
849
+ payload["final_counts"] = counts
850
+ payload["timed_out"] = timed_out
851
+ payload["duration_seconds"] = final_status.get("duration_seconds")
852
+ payload["failed"] = final_status.get("failed") or []
853
+ pending = counts.get("queued", 0) + counts.get("running", 0)
854
+ summary = (
855
+ f"Bulk job {job_id} "
856
+ + ("timed out with " if timed_out else "finished: ")
857
+ + f"{counts.get('completed', 0)} completed, "
858
+ + f"{counts.get('failed', 0)} failed"
859
+ + (f", {pending} still pending" if pending else "")
860
+ + "."
861
+ )
862
+ warnings = []
863
+ if counts.get("failed"):
864
+ warnings.append(f"{counts['failed']} run(s) failed.")
865
+ if timed_out:
866
+ warnings.append("Timed out before all runs finished.")
867
+ return ToolResult(
868
+ data=payload,
869
+ summary=summary,
870
+ warnings=warnings,
871
+ next_actions=[
872
+ "Use scenarios_bulk_status with include_runs=true to inspect runs.",
873
+ ],
874
+ )
875
+
831
876
  queued = payload.get("queued") or 0
832
877
  return ToolResult(
833
878
  data=payload,
@@ -1064,7 +1109,8 @@ def scenario_specs() -> list[ToolSpec]:
1064
1109
  "Run selected scenarios or every scenario in a benchmark and "
1065
1110
  "return the queued job metadata. Pass contact_email or contact_id "
1066
1111
  "to run as a contact with an email (fixes 'Email is not present' "
1067
- "failures on test conversations)."
1112
+ "failures on test conversations). Pass wait=true to block until "
1113
+ "all runs finish and return the final status in one call."
1068
1114
  ),
1069
1115
  input_model=ScenariosBulkRunInput,
1070
1116
  output_model=None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.6.4
3
+ Version: 0.6.5
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -51,6 +51,7 @@ tests/test_knowledge_content_tools.py
51
51
  tests/test_recovery.py
52
52
  tests/test_scenario_bulk_cancel.py
53
53
  tests/test_scenario_bulk_run_contact.py
54
+ tests/test_scenario_bulk_run_wait.py
54
55
  tests/test_toolkit_contract.py
55
56
  tests/test_v2_agents.py
56
57
  tests/test_v2_articles.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "applied-cli"
3
- version = "0.6.4"
3
+ version = "0.6.5"
4
4
  description = "CLI and shared client library for Applied Labs AI support agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,107 @@
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from applied_cli import tools
6
+
7
+
8
+ class FakeWaitClient:
9
+ """Bulk client whose status transitions to done after N polls."""
10
+
11
+ def __init__(self, status_sequence):
12
+ self._status_sequence = list(status_sequence)
13
+ self._poll = 0
14
+ self.status_calls = 0
15
+
16
+ async def list_scenarios(self, benchmark_id=None, limit=500, **kwargs):
17
+ return [{"id": "s1"}, {"id": "s2"}]
18
+
19
+ async def bulk_run_scenarios(
20
+ self, scenario_ids=None, target_agent_id=None, contact_override=None
21
+ ):
22
+ return {
23
+ "job_id": "job-1",
24
+ "total": len(scenario_ids or []),
25
+ "queued": len(scenario_ids or []),
26
+ "scenario_run_ids": ["r1", "r2"],
27
+ }
28
+
29
+ async def get_scenario_bulk_run_status(self, job_id):
30
+ self.status_calls += 1
31
+ idx = min(self._poll, len(self._status_sequence) - 1)
32
+ self._poll += 1
33
+ return self._status_sequence[idx]
34
+
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_wait_polls_until_no_pending(monkeypatch):
38
+ # Avoid real sleeping between polls.
39
+ async def _no_sleep(_seconds):
40
+ return None
41
+
42
+ monkeypatch.setattr(tools.asyncio, "sleep", _no_sleep)
43
+
44
+ client = FakeWaitClient(
45
+ status_sequence=[
46
+ {"counts": {"queued": 2, "running": 0, "completed": 0, "failed": 0}},
47
+ {"counts": {"queued": 0, "running": 1, "completed": 1, "failed": 0}},
48
+ {
49
+ "counts": {"queued": 0, "running": 0, "completed": 2, "failed": 0},
50
+ "duration_seconds": 12.5,
51
+ "completed_at": "2026-06-05T10:00:00Z",
52
+ "failed": [],
53
+ },
54
+ ]
55
+ )
56
+ result = await tools.scenario_bulk_run(
57
+ client, benchmark_id="bench-1", wait=True, output_format="json"
58
+ )
59
+ data = json.loads(result)
60
+ assert data["timed_out"] is False
61
+ assert data["final_counts"]["completed"] == 2
62
+ assert data["duration_seconds"] == 12.5
63
+ assert client.status_calls == 3
64
+
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_wait_times_out_when_runs_stay_pending(monkeypatch):
68
+ async def _no_sleep(_seconds):
69
+ return None
70
+
71
+ monkeypatch.setattr(tools.asyncio, "sleep", _no_sleep)
72
+
73
+ # Always pending → must hit the timeout path.
74
+ client = FakeWaitClient(
75
+ status_sequence=[
76
+ {"counts": {"queued": 2, "running": 0, "completed": 0, "failed": 0}}
77
+ ]
78
+ )
79
+ result = await tools.scenario_bulk_run(
80
+ client,
81
+ benchmark_id="bench-1",
82
+ wait=True,
83
+ wait_timeout=0.0, # immediate timeout after first poll
84
+ output_format="json",
85
+ )
86
+ data = json.loads(result)
87
+ assert data["timed_out"] is True
88
+ assert data["final_counts"]["queued"] == 2
89
+
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_no_wait_returns_started_summary():
93
+ client = FakeWaitClient(status_sequence=[{"counts": {}}])
94
+ result = await tools.scenario_bulk_run(
95
+ client, benchmark_id="bench-1", output_format="json"
96
+ )
97
+ data = json.loads(result)
98
+ assert "final_counts" not in data
99
+ assert client.status_calls == 0 # no polling when wait is False
100
+
101
+
102
+ def test_bulk_status_counts_normalizes_case_and_types():
103
+ assert tools._bulk_status_counts({"QUEUED": 2, "Running": "1"}) == {
104
+ "queued": 2,
105
+ "running": 1,
106
+ }
107
+ assert tools._bulk_pending_count({"QUEUED": 3, "RUNNING": 4, "COMPLETED": 9}) == 7
File without changes
File without changes