machina-cli 0.4.2__tar.gz → 0.4.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.
- {machina_cli-0.4.2 → machina_cli-0.4.3}/CHANGELOG.md +7 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/PKG-INFO +1 -1
- {machina_cli-0.4.2 → machina_cli-0.4.3}/pyproject.toml +1 -1
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/org.py +84 -20
- {machina_cli-0.4.2 → machina_cli-0.4.3}/tests/test_org_usage.py +40 -7
- {machina_cli-0.4.2 → machina_cli-0.4.3}/.github/workflows/ci.yml +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/.github/workflows/release.yml +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/.gitignore +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/CLAUDE.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/README.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs/agentic-harness-loop.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs/credentials.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs/harness-loop-kit/PLAYBOOK-SCORECARD.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs/harness-loop-kit/README.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs/harness-loop-kit/VALIDATION.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs/harness-loop-kit/context-verify.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs/harness-loop-kit/provision.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs/loop-tools-connector.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/.gitignore +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/.vitepress/config.mts +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/.vitepress/theme/custom.css +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/.vitepress/theme/index.ts +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/README.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/agent.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/config.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/connector.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/credentials.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/deploy.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/document.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/execution.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/factory.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/login.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/loop.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/mapping.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/org.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/project.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/prompt.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/skills.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/sports.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/template.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/update.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/commands/workflow.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/guide/authentication.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/guide/configuration.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/guide/installation.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/guide/quickstart.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/guide/repl.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/guide/running.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/index.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/package-lock.json +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/package.json +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/public/favicon.ico +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/public/machina-logo-dark.png +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/public/machina-logo-light.png +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/docs-site/vercel.json +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/install.ps1 +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/install.sh +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/__init__.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/browser_auth.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/client.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/__init__.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/agent.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/auth.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/config_cmd.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/connect.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/connector.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/credentials.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/deploy.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/document.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/execution.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/factory.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/loop.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/mapping.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/mcp.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/project.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/prompt.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/skills.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/sports.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/template.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/commands/workflow.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/config.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/factory_client.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/loop_client.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/main.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/models/__init__.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/project_client.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/repl.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/src/machina_cli/updater.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/tests/__init__.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/tests/test_cli_output.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/tests/test_connect.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/tests/test_mcp_url.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.3}/tests/test_sports_integration.py +0 -0
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to machina-cli are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.3] - 2026-06-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`machina org usage` no longer silently drops a project on a transient error.** The big collections (e.g. `sbot-prd`) return intermittent `500`s under load; the previous build caught that as "unreachable" and skipped the project, producing a complete-looking but wildly undercounted total (e.g. 73M instead of ~440M when SBOT Prd dropped). Now:
|
|
9
|
+
- Each project session + execution page is **retried** on transient failures (`500`/timeout) with backoff, so the heavy projects scan through.
|
|
10
|
+
- A reachable project whose scan still fails is reported as **errored** and the run is flagged **INCOMPLETE / PARTIAL** in red (with the affected projects listed) — distinct from a benign **skip** of an undeployed/no-access project. A partial total is never presented as if it were whole. `--json` gains `incomplete`, `projects_errored`, `projects_skipped`.
|
|
11
|
+
|
|
5
12
|
## [0.4.2] - 2026-06-29
|
|
6
13
|
|
|
7
14
|
### Added
|
|
@@ -143,6 +143,14 @@ def use(
|
|
|
143
143
|
# we paginate and sum client-side. A frozen upper date bound keeps pagination stable
|
|
144
144
|
# as new executions arrive mid-scan.
|
|
145
145
|
_TOKEN_FILTER_KEY = "execution_tokens.total_tokens"
|
|
146
|
+
_SCAN_RETRIES = 3 # big collections (e.g. sbot-prd) 500 intermittently under load; retry recovers
|
|
147
|
+
_RETRY_BACKOFF = 0.8
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class _ScanIncomplete(Exception):
|
|
151
|
+
"""A reachable project's scan kept failing after retries. Its tokens are missing,
|
|
152
|
+
so the org total must be flagged PARTIAL — never silently dropped (that would turn
|
|
153
|
+
a transient 500 on the biggest project into a plausible-looking but wrong total)."""
|
|
146
154
|
|
|
147
155
|
|
|
148
156
|
def _resolve_org_projects(org_id: str) -> list[tuple[str, str]]:
|
|
@@ -180,10 +188,30 @@ def _sum_project_tokens(project_id: str, token_filter: dict, page_size: int, on_
|
|
|
180
188
|
ProjectClient) on auth/connection failure — callers should catch and skip.
|
|
181
189
|
`on_page(count, total_tokens)` is invoked after each page for progress display.
|
|
182
190
|
"""
|
|
191
|
+
import time
|
|
183
192
|
from collections import defaultdict
|
|
184
193
|
from email.utils import parsedate_to_datetime
|
|
185
194
|
|
|
186
|
-
|
|
195
|
+
def _retry(fn, *, tries, incomplete_on_fail):
|
|
196
|
+
# Retry transient failures: ProjectClient raises SystemExit on a 500, httpx
|
|
197
|
+
# raises on a timeout. On give-up, either re-raise (caller skips an undeployed
|
|
198
|
+
# project) or raise _ScanIncomplete (caller flags a reachable project's partial
|
|
199
|
+
# scan loudly instead of silently dropping it).
|
|
200
|
+
last = None
|
|
201
|
+
for attempt in range(tries):
|
|
202
|
+
try:
|
|
203
|
+
return fn()
|
|
204
|
+
except (SystemExit, httpx.HTTPError) as exc:
|
|
205
|
+
last = exc
|
|
206
|
+
if attempt == tries - 1:
|
|
207
|
+
if incomplete_on_fail:
|
|
208
|
+
raise _ScanIncomplete(project_id) from last
|
|
209
|
+
raise
|
|
210
|
+
time.sleep(_RETRY_BACKOFF * (attempt + 1))
|
|
211
|
+
|
|
212
|
+
# Project session. A persistent failure means undeployed/no-access — let it
|
|
213
|
+
# propagate so the caller records a benign skip.
|
|
214
|
+
client = _retry(lambda: ProjectClient(project_id), tries=2, incomplete_on_fail=False)
|
|
187
215
|
proj = {"prompt": 0, "completion": 0, "total": 0, "count": 0}
|
|
188
216
|
by_agent: dict = defaultdict(lambda: {"prompt": 0, "completion": 0, "total": 0, "count": 0})
|
|
189
217
|
by_day: dict = defaultdict(lambda: {"total": 0, "count": 0})
|
|
@@ -192,9 +220,20 @@ def _sum_project_tokens(project_id: str, token_filter: dict, page_size: int, on_
|
|
|
192
220
|
max_pages = 400 # safety net; the `total` check below normally breaks first
|
|
193
221
|
truncated = False
|
|
194
222
|
while True:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
223
|
+
# The search on a reachable project: a persistent failure (e.g. sbot-prd's
|
|
224
|
+
# heavy collection 500ing) means the scan is INCOMPLETE, not skippable.
|
|
225
|
+
res = _retry(
|
|
226
|
+
lambda: client.post(
|
|
227
|
+
"execution/agent-search",
|
|
228
|
+
{
|
|
229
|
+
"filters": token_filter,
|
|
230
|
+
"page": page,
|
|
231
|
+
"page_size": page_size,
|
|
232
|
+
"sorters": ["_id", -1],
|
|
233
|
+
},
|
|
234
|
+
),
|
|
235
|
+
tries=_SCAN_RETRIES,
|
|
236
|
+
incomplete_on_fail=True,
|
|
198
237
|
)
|
|
199
238
|
rows = res.get("data", [])
|
|
200
239
|
if not rows:
|
|
@@ -297,7 +336,8 @@ def usage(
|
|
|
297
336
|
by_project: dict = {}
|
|
298
337
|
by_agent: dict = defaultdict(lambda: {"prompt": 0, "completion": 0, "total": 0, "count": 0})
|
|
299
338
|
by_day: dict = defaultdict(lambda: {"total": 0, "count": 0})
|
|
300
|
-
|
|
339
|
+
skipped: list[str] = [] # undeployed / no access — expected, excluded
|
|
340
|
+
errored: list[str] = [] # reachable but scan failed after retries — total is PARTIAL
|
|
301
341
|
truncated_projects: list[str] = []
|
|
302
342
|
|
|
303
343
|
status_cm = (
|
|
@@ -308,7 +348,7 @@ def usage(
|
|
|
308
348
|
n = len(targets)
|
|
309
349
|
# _quiet_clients silences the core/project HTTP clients: undeployed or
|
|
310
350
|
# inaccessible projects print a red error and raise SystemExit, which we catch
|
|
311
|
-
# and record as
|
|
351
|
+
# and record as skipped/errored — the leaked error would just be noise mid-scan.
|
|
312
352
|
with status_cm as status, _quiet_clients():
|
|
313
353
|
for i, (pid, pname) in enumerate(targets, 1):
|
|
314
354
|
|
|
@@ -322,11 +362,14 @@ def usage(
|
|
|
322
362
|
proj, p_agents, p_days, truncated = _sum_project_tokens(
|
|
323
363
|
pid, token_filter, page_size, on_page=_progress
|
|
324
364
|
)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
365
|
+
except _ScanIncomplete:
|
|
366
|
+
# reachable project whose search kept failing — its tokens are MISSING,
|
|
367
|
+
# so the org total is partial (flagged loudly below), not silently dropped.
|
|
368
|
+
errored.append(pname)
|
|
369
|
+
continue
|
|
328
370
|
except (SystemExit, httpx.HTTPError):
|
|
329
|
-
|
|
371
|
+
# login/lookup failed after retries -> undeployed or no access; expected.
|
|
372
|
+
skipped.append(pname)
|
|
330
373
|
continue
|
|
331
374
|
if proj["count"] == 0:
|
|
332
375
|
continue
|
|
@@ -351,19 +394,27 @@ def usage(
|
|
|
351
394
|
"by_project": by_project,
|
|
352
395
|
"by_agent": dict(sorted(by_agent.items(), key=lambda kv: -kv[1]["total"])),
|
|
353
396
|
"by_day": dict(sorted(by_day.items())),
|
|
354
|
-
"
|
|
397
|
+
"incomplete": bool(errored),
|
|
398
|
+
"projects_errored": errored,
|
|
399
|
+
"projects_skipped": skipped,
|
|
355
400
|
"projects_truncated": truncated_projects,
|
|
356
401
|
}
|
|
357
402
|
console.print_json(_json.dumps(payload, default=str))
|
|
358
403
|
return
|
|
359
404
|
|
|
360
405
|
if grand["count"] == 0:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
406
|
+
if errored:
|
|
407
|
+
console.print(
|
|
408
|
+
f"[bold red]⚠ {len(errored)} project(s) failed to scan: "
|
|
409
|
+
f"{', '.join(errored)} — re-run to retry (results would be partial).[/bold red]"
|
|
410
|
+
)
|
|
411
|
+
else:
|
|
412
|
+
console.print(
|
|
413
|
+
f"[yellow]No token-bearing agent executions in the last {days} day(s) "
|
|
414
|
+
f"for organization {org_id}.[/yellow]"
|
|
415
|
+
)
|
|
416
|
+
if skipped:
|
|
417
|
+
console.print(f"[dim]Skipped (undeployed/no access): {', '.join(skipped)}[/dim]")
|
|
367
418
|
return
|
|
368
419
|
|
|
369
420
|
total = grand["total"]
|
|
@@ -373,11 +424,19 @@ def usage(
|
|
|
373
424
|
|
|
374
425
|
from rich.panel import Panel
|
|
375
426
|
|
|
427
|
+
if errored:
|
|
428
|
+
console.print(
|
|
429
|
+
f"[bold red]⚠ INCOMPLETE — {len(errored)} reachable project(s) failed to scan "
|
|
430
|
+
f"and are NOT in the totals below:[/bold red] {', '.join(errored)}\n"
|
|
431
|
+
f"[red]The numbers below are PARTIAL. Re-run to retry those projects.[/red]\n"
|
|
432
|
+
)
|
|
433
|
+
total_label = "Total tokens (PARTIAL):" if errored else "Total tokens:"
|
|
434
|
+
|
|
376
435
|
console.print(
|
|
377
436
|
Panel.fit(
|
|
378
437
|
f"[bold]Organization:[/bold] {org_id}\n"
|
|
379
438
|
f"[bold]Window:[/bold] last {days}d ({date_from[:10]} → {date_to[:10]})\n"
|
|
380
|
-
f"[bold]
|
|
439
|
+
f"[bold]{total_label}[/bold] {total:,}\n"
|
|
381
440
|
f"[bold] prompt:[/bold] {grand['prompt']:,} ({prompt_pct:.1f}%) "
|
|
382
441
|
f"[bold]completion:[/bold] {grand['completion']:,} ({completion_pct:.1f}%)\n"
|
|
383
442
|
f"[bold]Executions:[/bold] {grand['count']:,} "
|
|
@@ -427,8 +486,13 @@ def usage(
|
|
|
427
486
|
dt.add_row(day, f"{agg['count']:,}", f"{agg['total']:,}")
|
|
428
487
|
console.print(dt)
|
|
429
488
|
|
|
430
|
-
if
|
|
431
|
-
console.print(
|
|
489
|
+
if errored:
|
|
490
|
+
console.print(
|
|
491
|
+
f"\n[bold red]⚠ Excluded (scan failed): {', '.join(errored)} — totals are PARTIAL; "
|
|
492
|
+
"re-run to retry.[/bold red]"
|
|
493
|
+
)
|
|
494
|
+
if skipped:
|
|
495
|
+
console.print(f"[dim]Skipped (undeployed/no access): {', '.join(skipped)}[/dim]")
|
|
432
496
|
if truncated_projects:
|
|
433
497
|
console.print(
|
|
434
498
|
f"[yellow]Note:[/yellow] hit the page cap for {', '.join(truncated_projects)} — "
|
|
@@ -155,33 +155,66 @@ def test_usage_resolves_projects_for_org():
|
|
|
155
155
|
assert payload["totals"]["total"] == _TOTAL
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
def
|
|
159
|
-
|
|
158
|
+
def test_usage_skips_undeployed_project_as_benign():
|
|
159
|
+
# ProjectClient() (session login) fails persistently -> undeployed/no access ->
|
|
160
|
+
# benign skip, NOT marked incomplete.
|
|
161
|
+
bad = MagicMock(side_effect=SystemExit(1))
|
|
160
162
|
with (
|
|
161
163
|
patch("machina_cli.commands.org.get_config", return_value=None),
|
|
164
|
+
patch("time.sleep", lambda *a: None), # don't wait on retry backoff
|
|
162
165
|
patch("machina_cli.commands.org.ProjectClient", bad),
|
|
163
166
|
):
|
|
164
167
|
result = runner.invoke(app, ["org", "usage", "--project", "p1", "--json"])
|
|
165
168
|
assert result.exit_code == 0, result.output
|
|
166
169
|
payload = json.loads(result.output)
|
|
167
170
|
assert payload["totals"]["count"] == 0
|
|
168
|
-
assert payload["
|
|
171
|
+
assert payload["projects_skipped"] == ["p1"]
|
|
172
|
+
assert payload["projects_errored"] == []
|
|
173
|
+
assert payload["incomplete"] is False
|
|
169
174
|
|
|
170
175
|
|
|
171
|
-
def
|
|
172
|
-
#
|
|
173
|
-
# project
|
|
176
|
+
def test_usage_flags_incomplete_when_reachable_project_scan_fails():
|
|
177
|
+
# session opens fine (reachable) but the search keeps 500ing/timing out -> the
|
|
178
|
+
# project is ERRORED and the total must be flagged PARTIAL, never silently dropped.
|
|
174
179
|
client = MagicMock()
|
|
175
180
|
client.return_value.post.side_effect = httpx.ReadTimeout("slow")
|
|
176
181
|
with (
|
|
177
182
|
patch("machina_cli.commands.org.get_config", return_value=None),
|
|
183
|
+
patch("time.sleep", lambda *a: None),
|
|
178
184
|
patch("machina_cli.commands.org.ProjectClient", client),
|
|
179
185
|
):
|
|
180
186
|
result = runner.invoke(app, ["org", "usage", "--project", "p1", "--json"])
|
|
181
187
|
assert result.exit_code == 0, result.output
|
|
182
188
|
payload = json.loads(result.output)
|
|
183
189
|
assert payload["totals"]["count"] == 0
|
|
184
|
-
assert payload["
|
|
190
|
+
assert payload["projects_errored"] == ["p1"]
|
|
191
|
+
assert payload["projects_skipped"] == []
|
|
192
|
+
assert payload["incomplete"] is True
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_usage_retries_then_succeeds():
|
|
196
|
+
# transient failures (sbot-prd-style 500s) recover on retry -> project IS counted
|
|
197
|
+
client = MagicMock()
|
|
198
|
+
calls = {"n": 0}
|
|
199
|
+
|
|
200
|
+
def _post(path, payload=None):
|
|
201
|
+
calls["n"] += 1
|
|
202
|
+
if calls["n"] <= 2: # fail the first two attempts, succeed on the third
|
|
203
|
+
raise httpx.ReadTimeout("transient 500")
|
|
204
|
+
return {"data": _ROWS, "total_documents": len(_ROWS), "status": True}
|
|
205
|
+
|
|
206
|
+
client.return_value.post.side_effect = _post
|
|
207
|
+
with (
|
|
208
|
+
patch("machina_cli.commands.org.get_config", return_value=None),
|
|
209
|
+
patch("time.sleep", lambda *a: None),
|
|
210
|
+
patch("machina_cli.commands.org.ProjectClient", client),
|
|
211
|
+
):
|
|
212
|
+
result = runner.invoke(app, ["org", "usage", "--project", "p1", "--json"])
|
|
213
|
+
assert result.exit_code == 0, result.output
|
|
214
|
+
payload = json.loads(result.output)
|
|
215
|
+
assert payload["totals"]["total"] == _TOTAL # recovered, fully counted
|
|
216
|
+
assert payload["incomplete"] is False
|
|
217
|
+
assert payload["projects_errored"] == []
|
|
185
218
|
|
|
186
219
|
|
|
187
220
|
def test_usage_no_org_no_project_errors():
|
|
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
|
|
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
|