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.
- {applied_cli-0.6.4 → applied_cli-0.6.5}/PKG-INFO +1 -1
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/cli.py +12 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/tools.py +89 -6
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/scenarios.py +47 -1
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/SOURCES.txt +1 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/pyproject.toml +1 -1
- applied_cli-0.6.5/tests/test_scenario_bulk_run_wait.py +107 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/README.md +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/__init__.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/auth.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/client.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/conversations.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/credentials.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/formatters.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/mcp.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/recovery.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/toolkit.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/__init__.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/agents.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/articles.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/catalog.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/connectors.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/content.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/conversations.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/domains.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/flows.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/knowledge.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/manifest.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/products.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/taxonomy.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli/v2/tickets.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/setup.cfg +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_auth_context.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_benchmark_clone.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_benchmark_delete_guardrail.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_cli.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_cli_v2.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_client.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_client_v2.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_knowledge_content_tools.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_recovery.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_scenario_bulk_cancel.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_scenario_bulk_run_contact.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_toolkit_contract.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_agents.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_articles.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_catalog_and_mcp.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_connectors.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_content.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_conversations.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_flows.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_knowledge.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_products.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_scenarios.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_taxonomy.py +0 -0
- {applied_cli-0.6.4 → applied_cli-0.6.5}/tests/test_v2_tickets.py +0 -0
|
@@ -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")
|
|
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('
|
|
6253
|
-
output += f"running: {counts.get('
|
|
6254
|
-
output += f"completed: {counts.get('
|
|
6255
|
-
output += f"failed: {counts.get('
|
|
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,
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|