machina-cli 0.4.2__tar.gz → 0.4.4__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.4}/CHANGELOG.md +12 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/PKG-INFO +1 -1
- {machina_cli-0.4.2 → machina_cli-0.4.4}/pyproject.toml +1 -1
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/org.py +90 -22
- {machina_cli-0.4.2 → machina_cli-0.4.4}/tests/test_org_usage.py +57 -7
- {machina_cli-0.4.2 → machina_cli-0.4.4}/.github/workflows/ci.yml +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/.github/workflows/release.yml +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/.gitignore +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/CLAUDE.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/README.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs/agentic-harness-loop.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs/credentials.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs/harness-loop-kit/PLAYBOOK-SCORECARD.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs/harness-loop-kit/README.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs/harness-loop-kit/VALIDATION.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs/harness-loop-kit/context-verify.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs/harness-loop-kit/provision.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs/loop-tools-connector.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/.gitignore +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/.vitepress/config.mts +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/.vitepress/theme/custom.css +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/.vitepress/theme/index.ts +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/README.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/agent.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/config.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/connector.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/credentials.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/deploy.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/document.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/execution.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/factory.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/login.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/loop.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/mapping.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/org.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/project.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/prompt.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/skills.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/sports.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/template.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/update.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/commands/workflow.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/guide/authentication.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/guide/configuration.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/guide/installation.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/guide/quickstart.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/guide/repl.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/guide/running.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/index.md +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/package-lock.json +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/package.json +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/public/favicon.ico +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/public/machina-logo-dark.png +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/public/machina-logo-light.png +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/docs-site/vercel.json +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/install.ps1 +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/install.sh +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/__init__.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/browser_auth.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/client.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/__init__.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/agent.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/auth.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/config_cmd.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/connect.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/connector.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/credentials.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/deploy.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/document.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/execution.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/factory.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/loop.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/mapping.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/mcp.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/project.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/prompt.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/skills.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/sports.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/template.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/commands/workflow.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/config.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/factory_client.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/loop_client.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/main.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/models/__init__.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/project_client.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/repl.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/src/machina_cli/updater.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/tests/__init__.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/tests/test_cli_output.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/tests/test_connect.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/tests/test_mcp_url.py +0 -0
- {machina_cli-0.4.2 → machina_cli-0.4.4}/tests/test_sports_integration.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to machina-cli are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.4] - 2026-06-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`machina org usage` shows a readable label for nameless project stubs.** Some `user/projects/search` records are bare membership stubs (`project_id` only, no `project_name` — e.g. a deleted project's leftover association); they were rendered as a raw 24-char ObjectId in the "Skipped" line. They now show `(unnamed:<id8>)`. The project is still scanned (never silently excluded over a missing name), so a real project can't be dropped this way.
|
|
9
|
+
|
|
10
|
+
## [0.4.3] - 2026-06-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`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:
|
|
14
|
+
- Each project session + execution page is **retried** on transient failures (`500`/timeout) with backoff, so the heavy projects scan through.
|
|
15
|
+
- 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`.
|
|
16
|
+
|
|
5
17
|
## [0.4.2] - 2026-06-29
|
|
6
18
|
|
|
7
19
|
### 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]]:
|
|
@@ -161,8 +169,12 @@ def _resolve_org_projects(org_id: str) -> list[tuple[str, str]]:
|
|
|
161
169
|
for proj in rows:
|
|
162
170
|
if proj.get("organization_id") != org_id:
|
|
163
171
|
continue
|
|
164
|
-
pid = proj.get("project_id"
|
|
165
|
-
|
|
172
|
+
pid = proj.get("project_id") or proj.get("_id") or ""
|
|
173
|
+
# Some membership records are bare stubs (project_id only, no name — e.g. a
|
|
174
|
+
# deleted project's leftover association). Show a readable label instead of a
|
|
175
|
+
# raw 24-char ObjectId, but still scan them so a real project is never
|
|
176
|
+
# silently dropped just because its name didn't resolve.
|
|
177
|
+
pname = proj.get("project_name") or proj.get("name") or f"(unnamed:{pid[:8]})"
|
|
166
178
|
if pid and proj.get("status") != "archived":
|
|
167
179
|
targets.append((pid, pname))
|
|
168
180
|
pagination = res.get("pagination", {})
|
|
@@ -180,10 +192,30 @@ def _sum_project_tokens(project_id: str, token_filter: dict, page_size: int, on_
|
|
|
180
192
|
ProjectClient) on auth/connection failure — callers should catch and skip.
|
|
181
193
|
`on_page(count, total_tokens)` is invoked after each page for progress display.
|
|
182
194
|
"""
|
|
195
|
+
import time
|
|
183
196
|
from collections import defaultdict
|
|
184
197
|
from email.utils import parsedate_to_datetime
|
|
185
198
|
|
|
186
|
-
|
|
199
|
+
def _retry(fn, *, tries, incomplete_on_fail):
|
|
200
|
+
# Retry transient failures: ProjectClient raises SystemExit on a 500, httpx
|
|
201
|
+
# raises on a timeout. On give-up, either re-raise (caller skips an undeployed
|
|
202
|
+
# project) or raise _ScanIncomplete (caller flags a reachable project's partial
|
|
203
|
+
# scan loudly instead of silently dropping it).
|
|
204
|
+
last = None
|
|
205
|
+
for attempt in range(tries):
|
|
206
|
+
try:
|
|
207
|
+
return fn()
|
|
208
|
+
except (SystemExit, httpx.HTTPError) as exc:
|
|
209
|
+
last = exc
|
|
210
|
+
if attempt == tries - 1:
|
|
211
|
+
if incomplete_on_fail:
|
|
212
|
+
raise _ScanIncomplete(project_id) from last
|
|
213
|
+
raise
|
|
214
|
+
time.sleep(_RETRY_BACKOFF * (attempt + 1))
|
|
215
|
+
|
|
216
|
+
# Project session. A persistent failure means undeployed/no-access — let it
|
|
217
|
+
# propagate so the caller records a benign skip.
|
|
218
|
+
client = _retry(lambda: ProjectClient(project_id), tries=2, incomplete_on_fail=False)
|
|
187
219
|
proj = {"prompt": 0, "completion": 0, "total": 0, "count": 0}
|
|
188
220
|
by_agent: dict = defaultdict(lambda: {"prompt": 0, "completion": 0, "total": 0, "count": 0})
|
|
189
221
|
by_day: dict = defaultdict(lambda: {"total": 0, "count": 0})
|
|
@@ -192,9 +224,20 @@ def _sum_project_tokens(project_id: str, token_filter: dict, page_size: int, on_
|
|
|
192
224
|
max_pages = 400 # safety net; the `total` check below normally breaks first
|
|
193
225
|
truncated = False
|
|
194
226
|
while True:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
227
|
+
# The search on a reachable project: a persistent failure (e.g. sbot-prd's
|
|
228
|
+
# heavy collection 500ing) means the scan is INCOMPLETE, not skippable.
|
|
229
|
+
res = _retry(
|
|
230
|
+
lambda: client.post(
|
|
231
|
+
"execution/agent-search",
|
|
232
|
+
{
|
|
233
|
+
"filters": token_filter,
|
|
234
|
+
"page": page,
|
|
235
|
+
"page_size": page_size,
|
|
236
|
+
"sorters": ["_id", -1],
|
|
237
|
+
},
|
|
238
|
+
),
|
|
239
|
+
tries=_SCAN_RETRIES,
|
|
240
|
+
incomplete_on_fail=True,
|
|
198
241
|
)
|
|
199
242
|
rows = res.get("data", [])
|
|
200
243
|
if not rows:
|
|
@@ -297,7 +340,8 @@ def usage(
|
|
|
297
340
|
by_project: dict = {}
|
|
298
341
|
by_agent: dict = defaultdict(lambda: {"prompt": 0, "completion": 0, "total": 0, "count": 0})
|
|
299
342
|
by_day: dict = defaultdict(lambda: {"total": 0, "count": 0})
|
|
300
|
-
|
|
343
|
+
skipped: list[str] = [] # undeployed / no access — expected, excluded
|
|
344
|
+
errored: list[str] = [] # reachable but scan failed after retries — total is PARTIAL
|
|
301
345
|
truncated_projects: list[str] = []
|
|
302
346
|
|
|
303
347
|
status_cm = (
|
|
@@ -308,7 +352,7 @@ def usage(
|
|
|
308
352
|
n = len(targets)
|
|
309
353
|
# _quiet_clients silences the core/project HTTP clients: undeployed or
|
|
310
354
|
# inaccessible projects print a red error and raise SystemExit, which we catch
|
|
311
|
-
# and record as
|
|
355
|
+
# and record as skipped/errored — the leaked error would just be noise mid-scan.
|
|
312
356
|
with status_cm as status, _quiet_clients():
|
|
313
357
|
for i, (pid, pname) in enumerate(targets, 1):
|
|
314
358
|
|
|
@@ -322,11 +366,14 @@ def usage(
|
|
|
322
366
|
proj, p_agents, p_days, truncated = _sum_project_tokens(
|
|
323
367
|
pid, token_filter, page_size, on_page=_progress
|
|
324
368
|
)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
369
|
+
except _ScanIncomplete:
|
|
370
|
+
# reachable project whose search kept failing — its tokens are MISSING,
|
|
371
|
+
# so the org total is partial (flagged loudly below), not silently dropped.
|
|
372
|
+
errored.append(pname)
|
|
373
|
+
continue
|
|
328
374
|
except (SystemExit, httpx.HTTPError):
|
|
329
|
-
|
|
375
|
+
# login/lookup failed after retries -> undeployed or no access; expected.
|
|
376
|
+
skipped.append(pname)
|
|
330
377
|
continue
|
|
331
378
|
if proj["count"] == 0:
|
|
332
379
|
continue
|
|
@@ -351,19 +398,27 @@ def usage(
|
|
|
351
398
|
"by_project": by_project,
|
|
352
399
|
"by_agent": dict(sorted(by_agent.items(), key=lambda kv: -kv[1]["total"])),
|
|
353
400
|
"by_day": dict(sorted(by_day.items())),
|
|
354
|
-
"
|
|
401
|
+
"incomplete": bool(errored),
|
|
402
|
+
"projects_errored": errored,
|
|
403
|
+
"projects_skipped": skipped,
|
|
355
404
|
"projects_truncated": truncated_projects,
|
|
356
405
|
}
|
|
357
406
|
console.print_json(_json.dumps(payload, default=str))
|
|
358
407
|
return
|
|
359
408
|
|
|
360
409
|
if grand["count"] == 0:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
410
|
+
if errored:
|
|
411
|
+
console.print(
|
|
412
|
+
f"[bold red]⚠ {len(errored)} project(s) failed to scan: "
|
|
413
|
+
f"{', '.join(errored)} — re-run to retry (results would be partial).[/bold red]"
|
|
414
|
+
)
|
|
415
|
+
else:
|
|
416
|
+
console.print(
|
|
417
|
+
f"[yellow]No token-bearing agent executions in the last {days} day(s) "
|
|
418
|
+
f"for organization {org_id}.[/yellow]"
|
|
419
|
+
)
|
|
420
|
+
if skipped:
|
|
421
|
+
console.print(f"[dim]Skipped (undeployed/no access): {', '.join(skipped)}[/dim]")
|
|
367
422
|
return
|
|
368
423
|
|
|
369
424
|
total = grand["total"]
|
|
@@ -373,11 +428,19 @@ def usage(
|
|
|
373
428
|
|
|
374
429
|
from rich.panel import Panel
|
|
375
430
|
|
|
431
|
+
if errored:
|
|
432
|
+
console.print(
|
|
433
|
+
f"[bold red]⚠ INCOMPLETE — {len(errored)} reachable project(s) failed to scan "
|
|
434
|
+
f"and are NOT in the totals below:[/bold red] {', '.join(errored)}\n"
|
|
435
|
+
f"[red]The numbers below are PARTIAL. Re-run to retry those projects.[/red]\n"
|
|
436
|
+
)
|
|
437
|
+
total_label = "Total tokens (PARTIAL):" if errored else "Total tokens:"
|
|
438
|
+
|
|
376
439
|
console.print(
|
|
377
440
|
Panel.fit(
|
|
378
441
|
f"[bold]Organization:[/bold] {org_id}\n"
|
|
379
442
|
f"[bold]Window:[/bold] last {days}d ({date_from[:10]} → {date_to[:10]})\n"
|
|
380
|
-
f"[bold]
|
|
443
|
+
f"[bold]{total_label}[/bold] {total:,}\n"
|
|
381
444
|
f"[bold] prompt:[/bold] {grand['prompt']:,} ({prompt_pct:.1f}%) "
|
|
382
445
|
f"[bold]completion:[/bold] {grand['completion']:,} ({completion_pct:.1f}%)\n"
|
|
383
446
|
f"[bold]Executions:[/bold] {grand['count']:,} "
|
|
@@ -427,8 +490,13 @@ def usage(
|
|
|
427
490
|
dt.add_row(day, f"{agg['count']:,}", f"{agg['total']:,}")
|
|
428
491
|
console.print(dt)
|
|
429
492
|
|
|
430
|
-
if
|
|
431
|
-
console.print(
|
|
493
|
+
if errored:
|
|
494
|
+
console.print(
|
|
495
|
+
f"\n[bold red]⚠ Excluded (scan failed): {', '.join(errored)} — totals are PARTIAL; "
|
|
496
|
+
"re-run to retry.[/bold red]"
|
|
497
|
+
)
|
|
498
|
+
if skipped:
|
|
499
|
+
console.print(f"[dim]Skipped (undeployed/no access): {', '.join(skipped)}[/dim]")
|
|
432
500
|
if truncated_projects:
|
|
433
501
|
console.print(
|
|
434
502
|
f"[yellow]Note:[/yellow] hit the page cap for {', '.join(truncated_projects)} — "
|
|
@@ -155,33 +155,83 @@ def test_usage_resolves_projects_for_org():
|
|
|
155
155
|
assert payload["totals"]["total"] == _TOTAL
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
def
|
|
159
|
-
|
|
158
|
+
def test_resolve_org_projects_labels_unnamed_stub():
|
|
159
|
+
# a bare membership stub (project_id only, no name) gets a readable label, not a
|
|
160
|
+
# raw ObjectId — and is still returned as a target (never silently excluded)
|
|
161
|
+
from machina_cli.commands import org as org_mod
|
|
162
|
+
|
|
163
|
+
projects = [
|
|
164
|
+
{"project_id": "p1", "project_name": "Real One", "organization_id": "org_1"},
|
|
165
|
+
{"project_id": "69ffa178815a5305f540dcac", "organization_id": "org_1"}, # bare stub
|
|
166
|
+
{"project_id": "p3", "project_name": "Other Org", "organization_id": "org_2"},
|
|
167
|
+
]
|
|
168
|
+
with patch("machina_cli.commands.org.MachinaClient", _projects_client(projects)):
|
|
169
|
+
targets = dict(org_mod._resolve_org_projects("org_1"))
|
|
170
|
+
assert targets["p1"] == "Real One"
|
|
171
|
+
assert targets["69ffa178815a5305f540dcac"] == "(unnamed:69ffa178)"
|
|
172
|
+
assert "p3" not in targets # other org excluded
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_usage_skips_undeployed_project_as_benign():
|
|
176
|
+
# ProjectClient() (session login) fails persistently -> undeployed/no access ->
|
|
177
|
+
# benign skip, NOT marked incomplete.
|
|
178
|
+
bad = MagicMock(side_effect=SystemExit(1))
|
|
160
179
|
with (
|
|
161
180
|
patch("machina_cli.commands.org.get_config", return_value=None),
|
|
181
|
+
patch("time.sleep", lambda *a: None), # don't wait on retry backoff
|
|
162
182
|
patch("machina_cli.commands.org.ProjectClient", bad),
|
|
163
183
|
):
|
|
164
184
|
result = runner.invoke(app, ["org", "usage", "--project", "p1", "--json"])
|
|
165
185
|
assert result.exit_code == 0, result.output
|
|
166
186
|
payload = json.loads(result.output)
|
|
167
187
|
assert payload["totals"]["count"] == 0
|
|
168
|
-
assert payload["
|
|
188
|
+
assert payload["projects_skipped"] == ["p1"]
|
|
189
|
+
assert payload["projects_errored"] == []
|
|
190
|
+
assert payload["incomplete"] is False
|
|
169
191
|
|
|
170
192
|
|
|
171
|
-
def
|
|
172
|
-
#
|
|
173
|
-
# project
|
|
193
|
+
def test_usage_flags_incomplete_when_reachable_project_scan_fails():
|
|
194
|
+
# session opens fine (reachable) but the search keeps 500ing/timing out -> the
|
|
195
|
+
# project is ERRORED and the total must be flagged PARTIAL, never silently dropped.
|
|
174
196
|
client = MagicMock()
|
|
175
197
|
client.return_value.post.side_effect = httpx.ReadTimeout("slow")
|
|
176
198
|
with (
|
|
177
199
|
patch("machina_cli.commands.org.get_config", return_value=None),
|
|
200
|
+
patch("time.sleep", lambda *a: None),
|
|
178
201
|
patch("machina_cli.commands.org.ProjectClient", client),
|
|
179
202
|
):
|
|
180
203
|
result = runner.invoke(app, ["org", "usage", "--project", "p1", "--json"])
|
|
181
204
|
assert result.exit_code == 0, result.output
|
|
182
205
|
payload = json.loads(result.output)
|
|
183
206
|
assert payload["totals"]["count"] == 0
|
|
184
|
-
assert payload["
|
|
207
|
+
assert payload["projects_errored"] == ["p1"]
|
|
208
|
+
assert payload["projects_skipped"] == []
|
|
209
|
+
assert payload["incomplete"] is True
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_usage_retries_then_succeeds():
|
|
213
|
+
# transient failures (sbot-prd-style 500s) recover on retry -> project IS counted
|
|
214
|
+
client = MagicMock()
|
|
215
|
+
calls = {"n": 0}
|
|
216
|
+
|
|
217
|
+
def _post(path, payload=None):
|
|
218
|
+
calls["n"] += 1
|
|
219
|
+
if calls["n"] <= 2: # fail the first two attempts, succeed on the third
|
|
220
|
+
raise httpx.ReadTimeout("transient 500")
|
|
221
|
+
return {"data": _ROWS, "total_documents": len(_ROWS), "status": True}
|
|
222
|
+
|
|
223
|
+
client.return_value.post.side_effect = _post
|
|
224
|
+
with (
|
|
225
|
+
patch("machina_cli.commands.org.get_config", return_value=None),
|
|
226
|
+
patch("time.sleep", lambda *a: None),
|
|
227
|
+
patch("machina_cli.commands.org.ProjectClient", client),
|
|
228
|
+
):
|
|
229
|
+
result = runner.invoke(app, ["org", "usage", "--project", "p1", "--json"])
|
|
230
|
+
assert result.exit_code == 0, result.output
|
|
231
|
+
payload = json.loads(result.output)
|
|
232
|
+
assert payload["totals"]["total"] == _TOTAL # recovered, fully counted
|
|
233
|
+
assert payload["incomplete"] is False
|
|
234
|
+
assert payload["projects_errored"] == []
|
|
185
235
|
|
|
186
236
|
|
|
187
237
|
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
|