Jonathan Haas commited on
Commit ·
39e5a0d
1
Parent(s): 6bf839c
Add taxonomy docs and richer telemetry error accounting
Browse files- README.md +1 -0
- TODO.md +8 -8
- docs/operations/error-taxonomy.md +63 -0
- src/jarvis/__main__.py +37 -9
- tests/test_main_lifecycle.py +93 -0
README.md
CHANGED
|
@@ -92,6 +92,7 @@ Smart home safety defaults:
|
|
| 92 |
- Integration runbook: [`docs/operations/integration-policy.md`](docs/operations/integration-policy.md).
|
| 93 |
- Release checklist: [`docs/operations/release-checklist.md`](docs/operations/release-checklist.md).
|
| 94 |
- Security maintenance: [`docs/operations/security-maintenance.md`](docs/operations/security-maintenance.md).
|
|
|
|
| 95 |
- Todoist integration:
|
| 96 |
- `TODOIST_PERMISSION_PROFILE=readonly|control`
|
| 97 |
- `readonly` allows `todoist_list_tasks` and denies `todoist_add_task`
|
|
|
|
| 92 |
- Integration runbook: [`docs/operations/integration-policy.md`](docs/operations/integration-policy.md).
|
| 93 |
- Release checklist: [`docs/operations/release-checklist.md`](docs/operations/release-checklist.md).
|
| 94 |
- Security maintenance: [`docs/operations/security-maintenance.md`](docs/operations/security-maintenance.md).
|
| 95 |
+
- Error taxonomy: [`docs/operations/error-taxonomy.md`](docs/operations/error-taxonomy.md).
|
| 96 |
- Todoist integration:
|
| 97 |
- `TODOIST_PERMISSION_PROFILE=readonly|control`
|
| 98 |
- `readonly` allows `todoist_list_tasks` and denies `todoist_add_task`
|
TODO.md
CHANGED
|
@@ -65,14 +65,14 @@ Files: `src/jarvis/tools/services.py`, `tests/test_tools.py`, `docs/operations/h
|
|
| 65 |
|
| 66 |
## 4) Telemetry and Taxonomy (8 items)
|
| 67 |
|
| 68 |
-
- [
|
| 69 |
-
- [
|
| 70 |
-
- [
|
| 71 |
-
- [
|
| 72 |
-
- [
|
| 73 |
-
- [
|
| 74 |
-
- [
|
| 75 |
-
- [
|
| 76 |
|
| 77 |
Files: `src/jarvis/tool_errors.py`, `src/jarvis/__main__.py`, `tests/test_main_lifecycle.py`, `docs/operations/error-taxonomy.md`
|
| 78 |
|
|
|
|
| 65 |
|
| 66 |
## 4) Telemetry and Taxonomy (8 items)
|
| 67 |
|
| 68 |
+
- [x] `T01` Add taxonomy reference doc with each error code and owning tool families.
|
| 69 |
+
- [x] `T02` Add test asserting taxonomy doc examples stay in sync with constants.
|
| 70 |
+
- [x] `T03` Track unknown summary detail count in telemetry snapshot.
|
| 71 |
+
- [x] `T04` Add tests for unknown summary detail accounting.
|
| 72 |
+
- [x] `T05` Add explicit counter for per-code service error totals in telemetry snapshot.
|
| 73 |
+
- [x] `T06` Add tests for per-code aggregation stability.
|
| 74 |
+
- [x] `T07` Add NaN/inf guard tests for telemetry averages.
|
| 75 |
+
- [x] `T08` Add startup log line summarizing taxonomy sizes (service/storage subsets).
|
| 76 |
|
| 77 |
Files: `src/jarvis/tool_errors.py`, `src/jarvis/__main__.py`, `tests/test_main_lifecycle.py`, `docs/operations/error-taxonomy.md`
|
| 78 |
|
docs/operations/error-taxonomy.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Error Taxonomy Reference
|
| 2 |
+
|
| 3 |
+
Canonical tool error taxonomy shared by service tools and telemetry.
|
| 4 |
+
|
| 5 |
+
## Ownership by Tool Family
|
| 6 |
+
|
| 7 |
+
| Error code | Owning tool families |
|
| 8 |
+
|---|---|
|
| 9 |
+
| `api_error` | `pushover_notify` |
|
| 10 |
+
| `auth` | `smart_home`, `smart_home_state`, `todoist_*`, `pushover_notify` |
|
| 11 |
+
| `cancelled` | `smart_home*`, `todoist_*`, `pushover_notify` |
|
| 12 |
+
| `http_error` | `smart_home*`, `todoist_*`, `pushover_notify` |
|
| 13 |
+
| `invalid_data` | `smart_home`, `task_plan_*`, `memory_*` |
|
| 14 |
+
| `invalid_json` | `smart_home*`, `todoist_*`, `pushover_notify` |
|
| 15 |
+
| `invalid_plan` | `task_plan_*` |
|
| 16 |
+
| `invalid_status` | `task_plan_update` |
|
| 17 |
+
| `invalid_steps` | `task_plan_create` |
|
| 18 |
+
| `missing_config` | `smart_home*`, `todoist_*`, `pushover_notify` |
|
| 19 |
+
| `missing_entity` | `smart_home_state` |
|
| 20 |
+
| `missing_fields` | `smart_home`, `todoist_add_task`, `pushover_notify` |
|
| 21 |
+
| `missing_plan` | `task_plan_*` |
|
| 22 |
+
| `missing_query` | `memory_search` |
|
| 23 |
+
| `missing_store` | `memory_*`, `task_plan_*` |
|
| 24 |
+
| `missing_text` | `memory_add` |
|
| 25 |
+
| `network_client_error` | `smart_home*`, `todoist_*`, `pushover_notify` |
|
| 26 |
+
| `not_found` | `smart_home*`, `task_plan_*` |
|
| 27 |
+
| `policy` | cross-cutting tool policy enforcement |
|
| 28 |
+
| `storage_error` | `memory_*`, `task_plan_*` |
|
| 29 |
+
| `summary_unavailable` | `tool_summary*`, `system_status` |
|
| 30 |
+
| `timeout` | `smart_home*`, `todoist_*`, `pushover_notify` |
|
| 31 |
+
| `unexpected` | cross-cutting runtime guard |
|
| 32 |
+
| `unknown_error` | fallback normalization bucket |
|
| 33 |
+
|
| 34 |
+
## Canonical Code Set (Machine-Checked)
|
| 35 |
+
|
| 36 |
+
Do not edit this list without updating `src/jarvis/tool_errors.py` and tests.
|
| 37 |
+
|
| 38 |
+
<!-- SERVICE_ERROR_CODES_START -->
|
| 39 |
+
api_error
|
| 40 |
+
auth
|
| 41 |
+
cancelled
|
| 42 |
+
http_error
|
| 43 |
+
invalid_data
|
| 44 |
+
invalid_json
|
| 45 |
+
invalid_plan
|
| 46 |
+
invalid_status
|
| 47 |
+
invalid_steps
|
| 48 |
+
missing_config
|
| 49 |
+
missing_entity
|
| 50 |
+
missing_fields
|
| 51 |
+
missing_plan
|
| 52 |
+
missing_query
|
| 53 |
+
missing_store
|
| 54 |
+
missing_text
|
| 55 |
+
network_client_error
|
| 56 |
+
not_found
|
| 57 |
+
policy
|
| 58 |
+
storage_error
|
| 59 |
+
summary_unavailable
|
| 60 |
+
timeout
|
| 61 |
+
unexpected
|
| 62 |
+
unknown_error
|
| 63 |
+
<!-- SERVICE_ERROR_CODES_END -->
|
src/jarvis/__main__.py
CHANGED
|
@@ -193,8 +193,10 @@ class Jarvis:
|
|
| 193 |
"tts_first_audio_count": 0.0,
|
| 194 |
"service_errors": 0.0,
|
| 195 |
"storage_errors": 0.0,
|
|
|
|
| 196 |
"fallback_responses": 0.0,
|
| 197 |
}
|
|
|
|
| 198 |
|
| 199 |
def start(self) -> None:
|
| 200 |
"""Initialize all subsystems."""
|
|
@@ -266,6 +268,7 @@ class Jarvis:
|
|
| 266 |
f"Persona style: {self.config.persona_style}",
|
| 267 |
f"Config warnings: {warning_count}",
|
| 268 |
f"Tool policy: allow={len(self.config.tool_allowlist)} deny={len(self.config.tool_denylist)}",
|
|
|
|
| 269 |
]
|
| 270 |
|
| 271 |
def _refresh_tool_error_counters(self) -> None:
|
|
@@ -275,6 +278,8 @@ class Jarvis:
|
|
| 275 |
return
|
| 276 |
service_errors = 0
|
| 277 |
storage_errors = 0
|
|
|
|
|
|
|
| 278 |
for item in recent:
|
| 279 |
if not isinstance(item, dict):
|
| 280 |
continue
|
|
@@ -282,30 +287,53 @@ class Jarvis:
|
|
| 282 |
detail = str(item.get("detail", ""))
|
| 283 |
if status != "error":
|
| 284 |
continue
|
|
|
|
|
|
|
| 285 |
if detail in TELEMETRY_STORAGE_ERROR_DETAILS:
|
| 286 |
storage_errors += 1
|
| 287 |
continue
|
| 288 |
if detail in TELEMETRY_SERVICE_ERROR_DETAILS:
|
| 289 |
service_errors += 1
|
|
|
|
|
|
|
| 290 |
self._telemetry["service_errors"] = float(service_errors)
|
| 291 |
self._telemetry["storage_errors"] = float(storage_errors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
-
def _telemetry_snapshot(self) -> dict[str, float]:
|
| 294 |
def avg(total_key: str, count_key: str) -> float:
|
| 295 |
-
count =
|
| 296 |
-
if count <= 0:
|
| 297 |
return 0.0
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
return {
|
| 301 |
-
"turns":
|
| 302 |
-
"barge_ins":
|
| 303 |
"avg_stt_latency_ms": avg("stt_latency_total_ms", "stt_latency_count"),
|
| 304 |
"avg_llm_first_sentence_ms": avg("llm_first_sentence_total_ms", "llm_first_sentence_count"),
|
| 305 |
"avg_tts_first_audio_ms": avg("tts_first_audio_total_ms", "tts_first_audio_count"),
|
| 306 |
-
"service_errors":
|
| 307 |
-
"storage_errors":
|
| 308 |
-
"
|
|
|
|
|
|
|
| 309 |
}
|
| 310 |
|
| 311 |
def stop(self) -> None:
|
|
|
|
| 193 |
"tts_first_audio_count": 0.0,
|
| 194 |
"service_errors": 0.0,
|
| 195 |
"storage_errors": 0.0,
|
| 196 |
+
"unknown_summary_details": 0.0,
|
| 197 |
"fallback_responses": 0.0,
|
| 198 |
}
|
| 199 |
+
self._telemetry_error_counts: dict[str, float] = {}
|
| 200 |
|
| 201 |
def start(self) -> None:
|
| 202 |
"""Initialize all subsystems."""
|
|
|
|
| 268 |
f"Persona style: {self.config.persona_style}",
|
| 269 |
f"Config warnings: {warning_count}",
|
| 270 |
f"Tool policy: allow={len(self.config.tool_allowlist)} deny={len(self.config.tool_denylist)}",
|
| 271 |
+
f"Error taxonomy: total={len(TOOL_SERVICE_ERROR_CODES)} service={len(TELEMETRY_SERVICE_ERROR_DETAILS)} storage={len(TELEMETRY_STORAGE_ERROR_DETAILS)}",
|
| 272 |
]
|
| 273 |
|
| 274 |
def _refresh_tool_error_counters(self) -> None:
|
|
|
|
| 278 |
return
|
| 279 |
service_errors = 0
|
| 280 |
storage_errors = 0
|
| 281 |
+
unknown_summary_details = 0
|
| 282 |
+
per_code: dict[str, float] = {}
|
| 283 |
for item in recent:
|
| 284 |
if not isinstance(item, dict):
|
| 285 |
continue
|
|
|
|
| 287 |
detail = str(item.get("detail", ""))
|
| 288 |
if status != "error":
|
| 289 |
continue
|
| 290 |
+
if detail in TOOL_SERVICE_ERROR_CODES:
|
| 291 |
+
per_code[detail] = per_code.get(detail, 0.0) + 1.0
|
| 292 |
if detail in TELEMETRY_STORAGE_ERROR_DETAILS:
|
| 293 |
storage_errors += 1
|
| 294 |
continue
|
| 295 |
if detail in TELEMETRY_SERVICE_ERROR_DETAILS:
|
| 296 |
service_errors += 1
|
| 297 |
+
continue
|
| 298 |
+
unknown_summary_details += 1
|
| 299 |
self._telemetry["service_errors"] = float(service_errors)
|
| 300 |
self._telemetry["storage_errors"] = float(storage_errors)
|
| 301 |
+
self._telemetry["unknown_summary_details"] = float(unknown_summary_details)
|
| 302 |
+
self._telemetry_error_counts = {name: per_code[name] for name in sorted(per_code)}
|
| 303 |
+
|
| 304 |
+
def _telemetry_snapshot(self) -> dict[str, float | dict[str, float]]:
|
| 305 |
+
def metric(key: str) -> float:
|
| 306 |
+
value = self._telemetry.get(key, 0.0)
|
| 307 |
+
if not math.isfinite(value):
|
| 308 |
+
return 0.0
|
| 309 |
+
return value
|
| 310 |
|
|
|
|
| 311 |
def avg(total_key: str, count_key: str) -> float:
|
| 312 |
+
count = metric(count_key)
|
| 313 |
+
if count <= 0.0:
|
| 314 |
return 0.0
|
| 315 |
+
total = metric(total_key)
|
| 316 |
+
value = total / count
|
| 317 |
+
if not math.isfinite(value):
|
| 318 |
+
return 0.0
|
| 319 |
+
return value
|
| 320 |
|
| 321 |
+
counts = {
|
| 322 |
+
name: value
|
| 323 |
+
for name, value in getattr(self, "_telemetry_error_counts", {}).items()
|
| 324 |
+
if math.isfinite(value)
|
| 325 |
+
}
|
| 326 |
return {
|
| 327 |
+
"turns": metric("turns"),
|
| 328 |
+
"barge_ins": metric("barge_ins"),
|
| 329 |
"avg_stt_latency_ms": avg("stt_latency_total_ms", "stt_latency_count"),
|
| 330 |
"avg_llm_first_sentence_ms": avg("llm_first_sentence_total_ms", "llm_first_sentence_count"),
|
| 331 |
"avg_tts_first_audio_ms": avg("tts_first_audio_total_ms", "tts_first_audio_count"),
|
| 332 |
+
"service_errors": metric("service_errors"),
|
| 333 |
+
"storage_errors": metric("storage_errors"),
|
| 334 |
+
"unknown_summary_details": metric("unknown_summary_details"),
|
| 335 |
+
"service_error_counts": counts,
|
| 336 |
+
"fallback_responses": metric("fallback_responses"),
|
| 337 |
}
|
| 338 |
|
| 339 |
def stop(self) -> None:
|
tests/test_main_lifecycle.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""Lifecycle robustness tests for jarvis.__main__.Jarvis."""
|
| 2 |
|
| 3 |
import pytest
|
|
|
|
| 4 |
from types import SimpleNamespace
|
| 5 |
from unittest.mock import MagicMock, AsyncMock
|
| 6 |
|
|
@@ -79,6 +80,21 @@ def test_startup_summary_lines_include_core_status():
|
|
| 79 |
assert "Memory: enabled" in joined
|
| 80 |
assert "Config warnings: 0" in joined
|
| 81 |
assert "Tool policy: allow=1 deny=2" in joined
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
def test_telemetry_snapshot_averages():
|
|
@@ -104,6 +120,7 @@ def test_telemetry_snapshot_averages():
|
|
| 104 |
assert snapshot["avg_tts_first_audio_ms"] == 250.0
|
| 105 |
assert snapshot["service_errors"] == 3.0
|
| 106 |
assert snapshot["storage_errors"] == 1.0
|
|
|
|
| 107 |
assert snapshot["fallback_responses"] == 4.0
|
| 108 |
|
| 109 |
|
|
@@ -121,6 +138,7 @@ def test_refresh_tool_error_counters():
|
|
| 121 |
|
| 122 |
assert jarvis._telemetry["service_errors"] == 1.0
|
| 123 |
assert jarvis._telemetry["storage_errors"] == 1.0
|
|
|
|
| 124 |
|
| 125 |
|
| 126 |
def test_refresh_tool_error_counters_classifies_missing_store_as_storage():
|
|
@@ -137,6 +155,7 @@ def test_refresh_tool_error_counters_classifies_missing_store_as_storage():
|
|
| 137 |
|
| 138 |
assert jarvis._telemetry["service_errors"] == 1.0
|
| 139 |
assert jarvis._telemetry["storage_errors"] == 2.0
|
|
|
|
| 140 |
|
| 141 |
|
| 142 |
def test_refresh_tool_error_counters_includes_network_taxonomy():
|
|
@@ -153,6 +172,80 @@ def test_refresh_tool_error_counters_includes_network_taxonomy():
|
|
| 153 |
|
| 154 |
assert jarvis._telemetry["service_errors"] == 3.0
|
| 155 |
assert jarvis._telemetry["storage_errors"] == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
|
| 158 |
def test_telemetry_error_taxonomy_matches_service_error_codes():
|
|
|
|
| 1 |
"""Lifecycle robustness tests for jarvis.__main__.Jarvis."""
|
| 2 |
|
| 3 |
import pytest
|
| 4 |
+
from pathlib import Path
|
| 5 |
from types import SimpleNamespace
|
| 6 |
from unittest.mock import MagicMock, AsyncMock
|
| 7 |
|
|
|
|
| 80 |
assert "Memory: enabled" in joined
|
| 81 |
assert "Config warnings: 0" in joined
|
| 82 |
assert "Tool policy: allow=1 deny=2" in joined
|
| 83 |
+
assert "Error taxonomy: total=" in joined
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def test_error_taxonomy_doc_matches_constants():
|
| 87 |
+
from jarvis.tool_errors import TOOL_SERVICE_ERROR_CODES
|
| 88 |
+
|
| 89 |
+
doc_path = Path(__file__).resolve().parents[1] / "docs" / "operations" / "error-taxonomy.md"
|
| 90 |
+
text = doc_path.read_text(encoding="utf-8")
|
| 91 |
+
start = "<!-- SERVICE_ERROR_CODES_START -->"
|
| 92 |
+
end = "<!-- SERVICE_ERROR_CODES_END -->"
|
| 93 |
+
assert start in text and end in text
|
| 94 |
+
|
| 95 |
+
block = text.split(start, 1)[1].split(end, 1)[0]
|
| 96 |
+
documented = {line.strip() for line in block.splitlines() if line.strip()}
|
| 97 |
+
assert documented == TOOL_SERVICE_ERROR_CODES
|
| 98 |
|
| 99 |
|
| 100 |
def test_telemetry_snapshot_averages():
|
|
|
|
| 120 |
assert snapshot["avg_tts_first_audio_ms"] == 250.0
|
| 121 |
assert snapshot["service_errors"] == 3.0
|
| 122 |
assert snapshot["storage_errors"] == 1.0
|
| 123 |
+
assert snapshot["unknown_summary_details"] == 0.0
|
| 124 |
assert snapshot["fallback_responses"] == 4.0
|
| 125 |
|
| 126 |
|
|
|
|
| 138 |
|
| 139 |
assert jarvis._telemetry["service_errors"] == 1.0
|
| 140 |
assert jarvis._telemetry["storage_errors"] == 1.0
|
| 141 |
+
assert jarvis._telemetry["unknown_summary_details"] == 0.0
|
| 142 |
|
| 143 |
|
| 144 |
def test_refresh_tool_error_counters_classifies_missing_store_as_storage():
|
|
|
|
| 155 |
|
| 156 |
assert jarvis._telemetry["service_errors"] == 1.0
|
| 157 |
assert jarvis._telemetry["storage_errors"] == 2.0
|
| 158 |
+
assert jarvis._telemetry["unknown_summary_details"] == 0.0
|
| 159 |
|
| 160 |
|
| 161 |
def test_refresh_tool_error_counters_includes_network_taxonomy():
|
|
|
|
| 172 |
|
| 173 |
assert jarvis._telemetry["service_errors"] == 3.0
|
| 174 |
assert jarvis._telemetry["storage_errors"] == 0.0
|
| 175 |
+
assert jarvis._telemetry["unknown_summary_details"] == 0.0
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def test_refresh_tool_error_counters_tracks_unknown_summary_detail_count():
|
| 179 |
+
jarvis = Jarvis.__new__(Jarvis)
|
| 180 |
+
jarvis._telemetry = {"service_errors": 0.0, "storage_errors": 0.0, "unknown_summary_details": 0.0}
|
| 181 |
+
from unittest.mock import patch
|
| 182 |
+
|
| 183 |
+
with patch("jarvis.__main__.list_summaries", return_value=[
|
| 184 |
+
{"status": "error", "detail": "timeout"},
|
| 185 |
+
{"status": "error", "detail": "not_a_real_code"},
|
| 186 |
+
{"status": "error", "detail": "another_unknown"},
|
| 187 |
+
]):
|
| 188 |
+
Jarvis._refresh_tool_error_counters(jarvis)
|
| 189 |
+
|
| 190 |
+
assert jarvis._telemetry["service_errors"] == 1.0
|
| 191 |
+
assert jarvis._telemetry["storage_errors"] == 0.0
|
| 192 |
+
assert jarvis._telemetry["unknown_summary_details"] == 2.0
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def test_refresh_tool_error_counters_reports_per_code_totals():
|
| 196 |
+
jarvis = Jarvis.__new__(Jarvis)
|
| 197 |
+
jarvis._telemetry = {"service_errors": 0.0, "storage_errors": 0.0, "unknown_summary_details": 0.0}
|
| 198 |
+
from unittest.mock import patch
|
| 199 |
+
|
| 200 |
+
with patch("jarvis.__main__.list_summaries", return_value=[
|
| 201 |
+
{"status": "error", "detail": "timeout"},
|
| 202 |
+
{"status": "error", "detail": "timeout"},
|
| 203 |
+
{"status": "error", "detail": "storage_error"},
|
| 204 |
+
{"status": "error", "detail": "unknown_error"},
|
| 205 |
+
{"status": "error", "detail": "not_a_real_code"},
|
| 206 |
+
{"status": "ok", "detail": "timeout"},
|
| 207 |
+
]):
|
| 208 |
+
Jarvis._refresh_tool_error_counters(jarvis)
|
| 209 |
+
|
| 210 |
+
assert jarvis._telemetry_error_counts == {
|
| 211 |
+
"storage_error": 1.0,
|
| 212 |
+
"timeout": 2.0,
|
| 213 |
+
"unknown_error": 1.0,
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
snapshot = Jarvis._telemetry_snapshot(jarvis)
|
| 217 |
+
assert snapshot["service_error_counts"] == jarvis._telemetry_error_counts
|
| 218 |
+
assert snapshot["unknown_summary_details"] == 1.0
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def test_telemetry_snapshot_guards_non_finite_values():
|
| 222 |
+
jarvis = Jarvis.__new__(Jarvis)
|
| 223 |
+
jarvis._telemetry = {
|
| 224 |
+
"turns": float("nan"),
|
| 225 |
+
"barge_ins": float("inf"),
|
| 226 |
+
"stt_latency_total_ms": float("inf"),
|
| 227 |
+
"stt_latency_count": 1.0,
|
| 228 |
+
"llm_first_sentence_total_ms": 100.0,
|
| 229 |
+
"llm_first_sentence_count": float("nan"),
|
| 230 |
+
"tts_first_audio_total_ms": 100.0,
|
| 231 |
+
"tts_first_audio_count": 0.0,
|
| 232 |
+
"service_errors": float("nan"),
|
| 233 |
+
"storage_errors": float("inf"),
|
| 234 |
+
"unknown_summary_details": float("nan"),
|
| 235 |
+
"fallback_responses": float("inf"),
|
| 236 |
+
}
|
| 237 |
+
jarvis._telemetry_error_counts = {"timeout": float("nan"), "auth": 2.0}
|
| 238 |
+
|
| 239 |
+
snapshot = Jarvis._telemetry_snapshot(jarvis)
|
| 240 |
+
assert snapshot["turns"] == 0.0
|
| 241 |
+
assert snapshot["barge_ins"] == 0.0
|
| 242 |
+
assert snapshot["avg_stt_latency_ms"] == 0.0
|
| 243 |
+
assert snapshot["avg_llm_first_sentence_ms"] == 0.0
|
| 244 |
+
assert snapshot["service_errors"] == 0.0
|
| 245 |
+
assert snapshot["storage_errors"] == 0.0
|
| 246 |
+
assert snapshot["unknown_summary_details"] == 0.0
|
| 247 |
+
assert snapshot["fallback_responses"] == 0.0
|
| 248 |
+
assert snapshot["service_error_counts"] == {"auth": 2.0}
|
| 249 |
|
| 250 |
|
| 251 |
def test_telemetry_error_taxonomy_matches_service_error_codes():
|