Skip to content

Commit aa2c33e

Browse files
OriNachumclaude
andcommitted
fix(sonar): S3516/S3776 — assess dispatcher + probes exit code; S5713 redundant JSONDecodeError
lobes assess --probes now exits EXIT_ENV_ERROR (2) when any probed role fails, 0 when all pass — same contract as tunnel's status exit. Payload shapes unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
2 parents 89f48ed + 9b8d3f7 commit aa2c33e

3 files changed

Lines changed: 94 additions & 43 deletions

File tree

lobes/assess.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ def probe_embed_correctness(
734734
(time.monotonic() - t0) * 1000,
735735
error=f"transport failed (timeout or connection error): {exc}",
736736
)
737-
except (KeyError, IndexError, TypeError, ValueError, json.JSONDecodeError) as exc:
737+
except (KeyError, IndexError, TypeError, ValueError) as exc:
738738
return _probe_result(
739739
"embedder",
740740
PROBE_NAMES["embedder"],

lobes/cli/_commands/assess.py

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
SEMANTIC answer, not just ``/health`` — a role that is healthy but wrong FAILS
1414
its probe. Throughput lives in ``lobes benchmark``; RUNTIME-only metrics
1515
(latency/throughput/RTF, never correctness) live in ``lobes measure``.
16+
17+
Exit code: plain ``lobes assess`` / ``--preserve-thinking`` always exit
18+
``EXIT_SUCCESS`` — they only emit a report. ``--probes`` is scriptable: it
19+
exits ``EXIT_SUCCESS`` when every probed role passes and ``EXIT_ENV_ERROR``
20+
when any role fails (mirrors ``lobes tunnel``'s status-driven exit code) —
21+
the ``--json``/text payload is unchanged either way, only the exit code
22+
differentiates pass from fail.
1623
"""
1724

1825
from __future__ import annotations
@@ -21,11 +28,66 @@
2128

2229
from lobes import assess as _assess
2330
from lobes.cli import _runtime_ops
31+
from lobes.cli._errors import EXIT_ENV_ERROR, EXIT_SUCCESS
2432
from lobes.cli._output import emit_result
2533
from lobes.roles import role_registry_from_env
2634
from lobes.runtime import _compose, _env
2735

2836

37+
def _cmd_assess_probes(args: argparse.Namespace, url: str, json_mode: bool) -> int:
38+
"""``--probes``: per-role CORRECTNESS probes (issue #81, t7).
39+
40+
Each of cortex/embedder/reranker is resolved to its own endpoint via the
41+
shared role registry (same builder `lobes capabilities`/`lobes measure`
42+
use) rather than the single gateway `url`. Exits EXIT_SUCCESS when every
43+
probed role passes, EXIT_ENV_ERROR when any fails — the payload shape is
44+
identical either way.
45+
"""
46+
env = _runtime_ops.deployment_env_soft(args)
47+
registry = role_registry_from_env(env, gateway_url=url)
48+
roles = (args.role,) if getattr(args, "role", None) else _assess.PROBE_ROLES
49+
timeout = float(getattr(args, "timeout", None) or _assess.DEFAULT_PROBE_TIMEOUT)
50+
endpoints = {
51+
role: (info.endpoint, info.model) if info.loaded and info.endpoint else None
52+
for role, info in registry.items()
53+
}
54+
results = _assess.run_role_probes(endpoints, roles=roles, timeout=timeout)
55+
passed = all(r["ok"] for r in results.values())
56+
if json_mode:
57+
emit_result({"passed": passed, "probes": results}, json_mode=True)
58+
else:
59+
emit_result(_assess.render_role_probes(results), json_mode=False)
60+
return EXIT_SUCCESS if passed else EXIT_ENV_ERROR
61+
62+
63+
def _cmd_assess_preserve_thinking(url: str, model: str | None, json_mode: bool) -> int:
64+
"""``--preserve-thinking``: the two-turn token-delta diagnostic (issue #93).
65+
66+
Reports both prompt-token counts and the delta; no host facts / correctness
67+
probes are needed.
68+
"""
69+
pt = _assess.run_preserve_thinking_probe(url, model)
70+
emit_result(pt if json_mode else _assess.render_preserve_thinking(pt), json_mode=json_mode)
71+
return EXIT_SUCCESS
72+
73+
74+
def _cmd_assess_correctness(
75+
args: argparse.Namespace, url: str, model: str | None, json_mode: bool
76+
) -> int:
77+
"""Default path: the two fixed correctness probes plus host-side facts."""
78+
result = _assess.run_correctness(url, model, check_tools=bool(getattr(args, "tools", False)))
79+
host = {"image": _compose.container_image(), "gpu_memory": _compose.gpu_engine_mem()}
80+
if json_mode:
81+
emit_result({**result, "host": host}, json_mode=True)
82+
else:
83+
header = (
84+
"### Host-side\n"
85+
f"- Image: `{host['image']}` · GPU memory (EngineCore): {host['gpu_memory']}\n"
86+
)
87+
emit_result(header + "\n" + _assess.render_correctness(result), json_mode=False)
88+
return EXIT_SUCCESS
89+
90+
2991
def cmd_assess(args: argparse.Namespace) -> int:
3092
json_mode = bool(getattr(args, "json", False))
3193
port, deploy_dir = _runtime_ops.resolve_port_soft(args)
@@ -35,48 +97,11 @@ def cmd_assess(args: argparse.Namespace) -> int:
3597

3698
url = f"http://localhost:{port}"
3799

38-
# --probes is a standalone read-only diagnostic (issue #81, t7): per-role
39-
# CORRECTNESS probes for cortex/embedder/reranker, each resolved to its own
40-
# endpoint via the shared role registry (same builder `lobes capabilities`/
41-
# `lobes measure` use) rather than the single `url` above. No host facts /
42-
# single-model correctness probes are needed.
43100
if bool(getattr(args, "probes", False)):
44-
env = _runtime_ops.deployment_env_soft(args)
45-
registry = role_registry_from_env(env, gateway_url=url)
46-
roles = (args.role,) if getattr(args, "role", None) else _assess.PROBE_ROLES
47-
timeout = float(getattr(args, "timeout", None) or _assess.DEFAULT_PROBE_TIMEOUT)
48-
endpoints = {
49-
role: (info.endpoint, info.model) if info.loaded and info.endpoint else None
50-
for role, info in registry.items()
51-
}
52-
results = _assess.run_role_probes(endpoints, roles=roles, timeout=timeout)
53-
passed = all(r["ok"] for r in results.values())
54-
if json_mode:
55-
emit_result({"passed": passed, "probes": results}, json_mode=True)
56-
else:
57-
emit_result(_assess.render_role_probes(results), json_mode=False)
58-
return 0
59-
60-
# --preserve-thinking is a standalone read-only diagnostic (issue #93): it
61-
# runs the two-turn token-delta probe and reports both prompt-token counts +
62-
# the delta. No host facts / correctness probes are needed.
101+
return _cmd_assess_probes(args, url, json_mode)
63102
if bool(getattr(args, "preserve_thinking", False)):
64-
pt = _assess.run_preserve_thinking_probe(url, model)
65-
emit_result(pt if json_mode else _assess.render_preserve_thinking(pt), json_mode=json_mode)
66-
else:
67-
result = _assess.run_correctness(
68-
url, model, check_tools=bool(getattr(args, "tools", False))
69-
)
70-
host = {"image": _compose.container_image(), "gpu_memory": _compose.gpu_engine_mem()}
71-
if json_mode:
72-
emit_result({**result, "host": host}, json_mode=True)
73-
else:
74-
header = (
75-
"### Host-side\n"
76-
f"- Image: `{host['image']}` · GPU memory (EngineCore): {host['gpu_memory']}\n"
77-
)
78-
emit_result(header + "\n" + _assess.render_correctness(result), json_mode=False)
79-
return 0
103+
return _cmd_assess_preserve_thinking(url, model, json_mode)
104+
return _cmd_assess_correctness(args, url, model, json_mode)
80105

81106

82107
def register(sub: argparse._SubParsersAction) -> None:

tests/test_role_probes.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import lobes.assess as A
2727
from lobes.cli import main
28+
from lobes.cli._errors import EXIT_ENV_ERROR, EXIT_SUCCESS
2829
from lobes.runtime import _compose
2930

3031
# ---------------------------------------------------------------------------
@@ -333,7 +334,7 @@ def test_cli_assess_probes_json_all_pass(tmp_path, monkeypatch: pytest.MonkeyPat
333334
_scaffold_fleet(tmp_path)
334335
monkeypatch.setattr(A, "_post", _fake_post_all_correct)
335336
rc = main(["assess", "--probes", "--compose-dir", str(tmp_path), "--port", "8000", "--json"])
336-
assert rc == 0
337+
assert rc == EXIT_SUCCESS
337338
payload = json.loads(capsys.readouterr().out)
338339
assert payload["passed"] is True
339340
assert set(payload["probes"]) == set(A.PROBE_ROLES)
@@ -359,13 +360,38 @@ def test_cli_assess_probes_json_reports_fail_for_wrong_role(
359360
"--json",
360361
]
361362
)
362-
assert rc == 0
363+
# A failing probe now differentiates the exit code (S3516) — EXIT_ENV_ERROR,
364+
# mirroring `lobes tunnel`'s status-driven exit code — while the --json
365+
# payload contract is unchanged (passed: false, same shape as a pass).
366+
assert rc == EXIT_ENV_ERROR
363367
payload = json.loads(capsys.readouterr().out)
364368
assert payload["passed"] is False
365369
assert set(payload["probes"]) == {"reranker"}
366370
assert payload["probes"]["reranker"]["ok"] is False
367371

368372

373+
def test_cli_assess_probes_text_mode_exit_code_reflects_failure(
374+
tmp_path, monkeypatch: pytest.MonkeyPatch, capsys
375+
) -> None:
376+
_scaffold_fleet(tmp_path)
377+
monkeypatch.setattr(A, "_post", _fake_post_rerank_wrong)
378+
rc = main(
379+
[
380+
"assess",
381+
"--probes",
382+
"--role",
383+
"reranker",
384+
"--compose-dir",
385+
str(tmp_path),
386+
"--port",
387+
"8000",
388+
]
389+
)
390+
assert rc == EXIT_ENV_ERROR
391+
out = capsys.readouterr().out
392+
assert "FAIL" in out
393+
394+
369395
def test_cli_assess_probes_role_filter(tmp_path, monkeypatch: pytest.MonkeyPatch, capsys) -> None:
370396
_scaffold_fleet(tmp_path)
371397
monkeypatch.setattr(A, "_post", _fake_post_all_correct)

0 commit comments

Comments
 (0)