Files
hermes-agent/tests/hermes_cli/test_setup_telemetry.py
teknium1 7a06cb50d7 feat(telemetry): shared-metrics opt-in offer on every surface
One answer per profile, asked once, with the Desktop strip's three equal
choices (Send to Nous / Local only / No thanks) now offered by:

- `hermes setup`: at the end of every flow (Quick, Full, Blank Slate,
  Portal, --quick), via the setup-completed record they all funnel into.
- `hermes` / `hermes --tui`: once before an interactive chat on a profile
  that never answered. Skipped for -q, piped/JSON output, spawned actions
  (HERMES_NONINTERACTIVE), Desktop-hosted panes and managed installs.
- Web dashboard: a banner for the profile being managed, backed by
  GET/PUT /api/shared-metrics/consent.

"No thanks" is the terminal default so Enter never opts anyone in; Esc and
the banner's X leave the question open.

Also fixes the answer that could not stick: a CLI "no" equals the shipped
defaults, so save_config stripped it and every surface (Desktop strip
included) kept asking. The answer is now written to config.yaml directly,
and `decided` has one definition shared by the RPC, the CLI and the router.
2026-09-30 21:15:08 -07:00

116 lines
4.3 KiB
Python

"""Tests for shared-metrics configuration discovery and setup."""
from __future__ import annotations
import argparse
from hermes_cli.config import DEFAULT_CONFIG
from hermes_cli.setup import setup_telemetry
from hermes_cli.subcommands.setup import build_setup_parser
def test_shared_metrics_are_registered_disabled_by_default():
assert DEFAULT_CONFIG["telemetry"]["shared_metrics"]["enabled"] is False
def test_setup_telemetry_enables_shared_metrics(monkeypatch):
config = {}
monkeypatch.setattr(
"hermes_cli.setup.prompt_yes_no",
lambda _question, default: not default,
)
setup_telemetry(config)
assert config["telemetry"]["shared_metrics"]["enabled"] is True
def test_disabling_collection_closes_the_send_consent_window(monkeypatch, tmp_path):
"""`hermes tools` -> disable shared metrics must withdraw send consent.
The not-enabled branch returned early without recording anything, so the
consent window stayed open and re-enabling later would release every
package collected in between.
"""
from hermes_cli.observability.shared_metrics import SharedMetricsStore
from hermes_cli.observability.shared_metrics_sender import (
reconcile_send_consent,
)
from hermes_cli.sqlite_util import write_txn
store = SharedMetricsStore(
database_path=tmp_path / "m.db", outbox_directory=tmp_path / "o"
)
monkeypatch.setattr(
"hermes_cli.observability.shared_metrics.SharedMetricsStore",
lambda *a, **k: store,
)
# The user had consented; now they turn collection off entirely.
monkeypatch.setattr(
"hermes_cli.setup.prompt_yes_no", lambda _question, default: False
)
config = {"telemetry": {"shared_metrics": {"enabled": True, "send": True}}}
# Consent was granted earlier, so a window is open — that is precisely
# the state whose closure must be recorded.
with store._connection() as connection:
with write_txn(connection):
reconcile_send_consent(connection, True)
setup_telemetry(config)
assert config["telemetry"]["shared_metrics"]["enabled"] is False
assert config["telemetry"]["shared_metrics"]["send"] is False
with store._connection() as connection:
open_windows = connection.execute(
"SELECT COUNT(*) FROM send_consent_windows WHERE closed_at IS NULL"
).fetchone()[0]
assert open_windows == 0, (
"disabling collection left the send consent window open"
)
def test_setup_parser_accepts_telemetry_section():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command")
handler = object()
build_setup_parser(subparsers, cmd_setup=handler)
args = parser.parse_args(["setup", "telemetry"])
assert args.section == "telemetry"
assert args.func is handler
def test_no_answer_survives_the_callers_later_config_save(monkeypatch):
"""A "no" equals the shipped defaults; saved only through ``save_config`` it was stripped, so
the profile read undecided and every surface (Desktop strip, CLI offer) asked again."""
from hermes_cli.config import load_config, read_raw_config, save_config
monkeypatch.setattr("hermes_cli.setup.prompt_yes_no", lambda _question, default: False)
config = load_config()
setup_telemetry(config)
save_config(config) # what the wizard and `hermes tools` do afterwards
assert read_raw_config()["telemetry"]["shared_metrics"] == {"enabled": False, "send": False}
def test_chat_offer_asks_an_undecided_profile_once(monkeypatch):
from hermes_cli.config import read_raw_config
from hermes_cli.observability import shared_metrics_consent as consent
asked = []
monkeypatch.setattr("hermes_cli.curses_ui.curses_radiolist", lambda *a, **k: asked.append(a) or 1)
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
monkeypatch.setattr("sys.stdout.isatty", lambda: True)
monkeypatch.delenv("HERMES_DESKTOP", raising=False)
monkeypatch.delenv("HERMES_NONINTERACTIVE", raising=False)
consent.offer_consent_before_chat(argparse.Namespace(query="hi")) # no person to ask
assert asked == []
consent.offer_consent_before_chat(argparse.Namespace())
consent.offer_consent_before_chat(argparse.Namespace())
assert len(asked) == 1
assert consent.consent_state(read_raw_config()) == {"enabled": True, "send": False, "decided": True}