hermes-agent/tests/telemetry/test_cli_telemetry.py
emozilla 24c86e160b fix(telemetry): aggregate requires local telemetry to be on
Aggregate metrics are derived from the local tel_* tables — they're a coarsened
view of local data, not an independent capture path. With telemetry.local=false
nothing is written, so an aggregate opt-in had nothing to aggregate, yet
may_upload_aggregate() returned True and `status` showed "Aggregate metrics: on".
The config could claim a state it couldn't fulfill.

Gate aggregate on local being enabled:
  - may_upload_aggregate() now requires local_enabled AND allow_aggregate AND
    consent_state == aggregate.
  - `telemetry status` computes aggregate_enabled the same way and, when consent is
    aggregate but local is off, prints "inert: local telemetry is off — nothing to
    aggregate" instead of the opt-in hint.

Happy path is unchanged (local on + consent aggregate -> on). Adds policy and CLI
tests for the inert combo.
2026-07-24 18:54:45 +00:00

108 lines
3.7 KiB
Python

"""`hermes telemetry` handler smoke tests (local-only; no upload)."""
from __future__ import annotations
import sqlite3
import time
import types
import pytest
import hermes_state
@pytest.fixture
def home(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
# state.db with tel_* + seeded data
db = tmp_path / "state.db"
hermes_state.SessionDB(db_path=db)
from agent.telemetry.emitter import TelemetryEmitter
from agent.telemetry.events import RunEvent, ModelCallEvent, ToolCallEvent
em = TelemetryEmitter(events_path=tmp_path / "telemetry" / "e.jsonl", db_path=db)
now = time.time_ns()
em.emit(RunEvent(run_id="r1", trace_id="t1", entrypoint="cli", end_reason="completed",
start_ns=now - 60_000_000, end_ns=now, model_call_count=1,
tool_call_count=1, estimated_cost_usd=0.3))
em.emit(ModelCallEvent(span_id="m1", run_id="r1", provider="anthropic",
model="claude-opus-4",
input_tokens=20000, output_tokens=2000))
em.emit(ToolCallEvent(span_id="w1", run_id="r1", tool_name="web_search",
result_class="ok"))
em.flush()
em.close()
yield tmp_path
def _run(action, **kw):
from hermes_cli.main import cmd_telemetry
args = types.SimpleNamespace(telemetry_action=action, days=30, limit=10, json=False)
for k, v in kw.items():
setattr(args, k, v)
cmd_telemetry(args)
def test_status_runs(home, capsys):
_run("status")
out = capsys.readouterr().out
assert "Telemetry status" in out
assert "Upload:" in out and "DISABLED" in out
assert "Local data:" in out
def test_preview_shows_real_values(home, capsys):
_run("preview")
out = capsys.readouterr().out
assert "NOT uploaded" in out
assert "workflow_completed" in out
# real model + tool names ARE shown (this is the user's own local data)
assert "claude-opus-4" in out
assert "web_search" in out
def test_status_reflects_consent_set_via_config(home, capsys):
# Opting in is a plain config write now (no `enable` verb). status should
# reflect consent_state=aggregate as aggregate metrics being on.
from hermes_cli.config import load_config, save_config
cfg = load_config()
cfg.setdefault("telemetry", {})["consent_state"] = "aggregate"
save_config(cfg)
_run("status")
out = capsys.readouterr().out
assert "consent_state=aggregate" in out
assert "Aggregate metrics: on" in out
def test_status_shows_optin_hint_when_unknown(home, capsys):
_run("status")
out = capsys.readouterr().out
assert "Aggregate metrics: off" in out
assert "config set telemetry.consent_state aggregate" in out
def test_allow_aggregate_false_keeps_metrics_off_in_status(home, capsys):
# Even with consent opted in, a managed allow_aggregate:false wins.
from hermes_cli.config import load_config, save_config
cfg = load_config()
tel = cfg.setdefault("telemetry", {})
tel["consent_state"] = "aggregate"
tel["allow_aggregate"] = False
save_config(cfg)
_run("status")
out = capsys.readouterr().out
assert "Aggregate metrics: off" in out
assert "allow_aggregate is false" in out
def test_local_off_with_consent_shows_inert_in_status(home, capsys):
# local off + opted in: aggregate is off and the status explains why.
from hermes_cli.config import load_config, save_config
cfg = load_config()
tel = cfg.setdefault("telemetry", {})
tel["local"] = False
tel["consent_state"] = "aggregate"
save_config(cfg)
_run("status")
out = capsys.readouterr().out
assert "Aggregate metrics: off" in out
assert "inert: local telemetry is off" in out