mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
903 lines
31 KiB
Python
903 lines
31 KiB
Python
"""Focused tests for the Hermes shared-metrics durable store."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import multiprocessing as mp
|
|
import os
|
|
import sqlite3
|
|
import stat
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from copy import deepcopy
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from hermes_cli.observability import shared_metrics as shared_metrics_module
|
|
from hermes_cli.observability.shared_metrics import SharedMetricsStore
|
|
from hermes_cli.observability.shared_metrics_contract import (
|
|
COUNT_BUCKETS,
|
|
DURATION_BUCKETS,
|
|
EXECUTION_SURFACES,
|
|
MODEL_FAMILIES,
|
|
MODEL_LOCALITIES,
|
|
MODEL_OUTCOMES,
|
|
PRIMARY_MODEL_CALL_ROLE,
|
|
PROVIDER_FAMILIES,
|
|
TASK_END_REASONS,
|
|
TASK_ENTRYPOINTS,
|
|
TASK_OUTCOMES,
|
|
TASK_TERMINATIONS,
|
|
count_bucket,
|
|
duration_bucket,
|
|
execution_surface,
|
|
model_call_outcome,
|
|
model_call_dimensions,
|
|
model_family,
|
|
model_locality,
|
|
provider_family,
|
|
task_counter,
|
|
task_start_fields,
|
|
task_terminal_fields,
|
|
task_terminal_state,
|
|
)
|
|
|
|
|
|
SCHEMA_PATH = (
|
|
Path(__file__).resolve().parents[2]
|
|
/ "hermes_cli"
|
|
/ "observability"
|
|
/ "schemas"
|
|
/ "hermes.shared_metrics.v1.schema.json"
|
|
)
|
|
|
|
|
|
def _schema_validator():
|
|
jsonschema = pytest.importorskip("jsonschema")
|
|
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
jsonschema.Draft202012Validator.check_schema(schema)
|
|
return jsonschema.Draft202012Validator(
|
|
schema,
|
|
format_checker=jsonschema.FormatChecker(),
|
|
)
|
|
|
|
|
|
def _package_dimension_schema() -> dict[str, object]:
|
|
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
return schema["$defs"]["model_call_counter"]["properties"]["dimensions"]
|
|
|
|
|
|
def _task_dimension_schema(kind: str) -> dict[str, object]:
|
|
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
return schema["$defs"][kind]["properties"]["dimensions"]
|
|
|
|
|
|
def _dimensions() -> dict[str, str]:
|
|
return {
|
|
"call_role": PRIMARY_MODEL_CALL_ROLE,
|
|
"locality": "remote",
|
|
"model_family": "claude",
|
|
"outcome": "success",
|
|
"provider_family": "direct",
|
|
}
|
|
|
|
|
|
def _record_model_calls_in_process(
|
|
database_path: str,
|
|
outbox_directory: str,
|
|
count: int,
|
|
start_barrier: Any | None = None,
|
|
) -> None:
|
|
if start_barrier is not None:
|
|
start_barrier.wait()
|
|
store = SharedMetricsStore(Path(database_path), Path(outbox_directory))
|
|
for _ in range(count):
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
|
|
|
|
def test_model_call_counter_survives_restart_and_exports_only_new_deltas(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
|
|
first_paths = store.create_and_export_package()
|
|
|
|
assert len(first_paths) == 1
|
|
first_package = json.loads(first_paths[0].read_text(encoding="utf-8"))
|
|
_schema_validator().validate(first_package)
|
|
uuid.UUID(first_package["package_id"])
|
|
uuid.UUID(first_package["install_id"])
|
|
assert first_package["schema_version"] == "hermes.shared_metrics.v1"
|
|
assert first_package["resource"] == {"hermes_version": "test-version"}
|
|
assert first_package["metrics"] == [
|
|
{
|
|
"name": "hermes.model_call.count",
|
|
"type": "counter",
|
|
"dimensions": _dimensions(),
|
|
"value": 2,
|
|
}
|
|
]
|
|
|
|
restarted = SharedMetricsStore(database_path, outbox_directory)
|
|
assert restarted.counter_snapshot()[0]["value"] == 2
|
|
assert restarted.counter_snapshot()[0]["packaged_value"] == 2
|
|
assert restarted.create_and_export_package() == []
|
|
assert len(list(outbox_directory.glob("*.json"))) == 1
|
|
|
|
restarted.record_model_call(_dimensions(), "test-version")
|
|
second_paths = restarted.create_and_export_package()
|
|
|
|
assert len(second_paths) == 1
|
|
second_package = json.loads(second_paths[0].read_text(encoding="utf-8"))
|
|
assert second_package["package_id"] != first_package["package_id"]
|
|
assert second_package["install_id"] == first_package["install_id"]
|
|
assert second_package["metrics"][0]["value"] == 1
|
|
assert restarted.counter_snapshot()[0]["value"] == 3
|
|
assert restarted.counter_snapshot()[0]["packaged_value"] == 3
|
|
|
|
|
|
def test_due_export_runs_once_per_utc_day_and_catches_up_pending_deltas(
|
|
tmp_path, monkeypatch
|
|
):
|
|
current_time = datetime(2026, 7, 28, 9, tzinfo=timezone.utc)
|
|
monkeypatch.setattr(shared_metrics_module, "_utc_now", lambda: current_time)
|
|
store = SharedMetricsStore(tmp_path / "metrics.sqlite3", tmp_path / "outbox")
|
|
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
assert len(store.create_and_export_package_if_due()) == 1
|
|
|
|
current_time = datetime(2026, 7, 28, 18, tzinfo=timezone.utc)
|
|
store = SharedMetricsStore(tmp_path / "metrics.sqlite3", tmp_path / "outbox")
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
assert store.create_and_export_package_if_due() == []
|
|
assert len(list((tmp_path / "outbox").glob("*.json"))) == 1
|
|
assert store.counter_snapshot()[0] == {
|
|
"period_start": "2026-07-28",
|
|
"metric_name": "hermes.model_call.count",
|
|
"hermes_version": "test-version",
|
|
"dimensions": _dimensions(),
|
|
"value": 2,
|
|
"packaged_value": 1,
|
|
}
|
|
|
|
current_time = datetime(2026, 7, 29, 9, tzinfo=timezone.utc)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
assert len(store.create_and_export_package_if_due()) == 2
|
|
assert len(list((tmp_path / "outbox").glob("*.json"))) == 3
|
|
assert all(
|
|
row["value"] == row["packaged_value"] for row in store.counter_snapshot()
|
|
)
|
|
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
assert store.create_and_export_package_if_due() == []
|
|
assert len(list((tmp_path / "outbox").glob("*.json"))) == 3
|
|
|
|
|
|
def test_package_schema_matches_the_model_call_contract():
|
|
properties = _package_dimension_schema()["properties"]
|
|
|
|
assert properties["call_role"] == {"const": PRIMARY_MODEL_CALL_ROLE}
|
|
assert set(properties["locality"]["enum"]) == MODEL_LOCALITIES
|
|
assert set(properties["model_family"]["enum"]) == MODEL_FAMILIES
|
|
assert set(properties["outcome"]["enum"]) == MODEL_OUTCOMES
|
|
assert set(properties["provider_family"]["enum"]) == PROVIDER_FAMILIES
|
|
|
|
|
|
def test_package_schema_matches_the_task_contract():
|
|
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
start = _task_dimension_schema("task_started_counter")["properties"]
|
|
terminal = _task_dimension_schema("task_finished_counter")["properties"]
|
|
|
|
assert set(schema["$defs"]["execution_surface"]["enum"]) == EXECUTION_SURFACES
|
|
assert set(schema["$defs"]["task_entrypoint"]["enum"]) == TASK_ENTRYPOINTS
|
|
assert set(schema["$defs"]["duration_bucket"]["enum"]) == DURATION_BUCKETS
|
|
assert set(schema["$defs"]["count_bucket"]["enum"]) == COUNT_BUCKETS
|
|
assert start["entrypoint"] == {"$ref": "#/$defs/task_entrypoint"}
|
|
assert set(terminal["end_reason"]["enum"]) == TASK_END_REASONS
|
|
assert set(terminal["outcome"]["enum"]) == TASK_OUTCOMES
|
|
assert set(terminal["termination"]["enum"]) == TASK_TERMINATIONS
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("provider", "expected"),
|
|
[
|
|
("", "unknown"),
|
|
("not-a-hermes-provider", "unknown"),
|
|
("custom", "custom"),
|
|
("custom-local", "custom"),
|
|
("custom:private-endpoint", "custom"),
|
|
("lmstudio", "local"),
|
|
("lm_studio", "local"),
|
|
("ollama", "local"),
|
|
("nous", "aggregator"),
|
|
("openrouter", "aggregator"),
|
|
("kilo", "aggregator"),
|
|
("copilot-acp", "aggregator"),
|
|
("huggingface", "aggregator"),
|
|
("novita", "aggregator"),
|
|
("anthropic", "direct"),
|
|
("google", "direct"),
|
|
("openai-api", "direct"),
|
|
],
|
|
)
|
|
def test_provider_family_uses_bounded_product_categories(provider, expected):
|
|
assert provider_family({"provider": provider}) == expected
|
|
|
|
|
|
def test_provider_family_does_not_resolve_live_provider_metadata(monkeypatch):
|
|
def fail_live_lookup(_provider):
|
|
raise AssertionError("telemetry must not refresh provider metadata")
|
|
|
|
monkeypatch.setattr("hermes_cli.providers.get_provider", fail_live_lookup)
|
|
assert provider_family({"provider": "anthropic"}) == "direct"
|
|
|
|
|
|
def test_locality_uses_the_endpoint_only_for_local_classification():
|
|
kwargs = {
|
|
"provider": "custom",
|
|
"base_url": "http://127.0.0.1:11434/v1",
|
|
}
|
|
|
|
assert provider_family(kwargs) == "custom"
|
|
assert model_locality(kwargs) == "local"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model", "expected"),
|
|
[
|
|
("google/gemma-3", "gemma"),
|
|
("x-ai/grok-4", "grok"),
|
|
("minimax/minimax-m2.5", "minimax"),
|
|
("xiaomi/mimo-v2", "mimo"),
|
|
("amazon/nova-pro", "nova"),
|
|
("stepfun/step-3.5", "step"),
|
|
("arcee-ai/trinity-large", "trinity"),
|
|
],
|
|
)
|
|
def test_model_family_covers_families_evidenced_by_the_hermes_catalog(model, expected):
|
|
assert model_family({"model": model}) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model",
|
|
[
|
|
"private-gptish-model",
|
|
"innovation-private",
|
|
"mimosa-private",
|
|
"stepstone-private",
|
|
"supernova-private",
|
|
],
|
|
)
|
|
def test_model_family_requires_identifier_boundaries(model):
|
|
assert model_family({"model": model}) == "unknown"
|
|
|
|
|
|
def test_model_family_accepts_only_allowlisted_declared_metadata():
|
|
assert model_family({"model": "private", "model_family": "qwen"}) == "qwen"
|
|
assert model_family({"model": "private", "model_family": "private"}) == "unknown"
|
|
|
|
|
|
def test_model_family_prefers_the_provider_reported_terminal_model():
|
|
assert (
|
|
model_family({"model": "gpt-5", "response_model": "claude-sonnet"}) == "claude"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("platform", "expected"),
|
|
[
|
|
("", "unknown"),
|
|
("cli", "cli"),
|
|
("api_server", "api"),
|
|
("cron", "scheduled_task"),
|
|
("whatsapp_cloud", "gateway"),
|
|
("private-surface", "other"),
|
|
],
|
|
)
|
|
def test_execution_surface_uses_the_hermes_platform_registry(platform, expected):
|
|
assert execution_surface({"platform": platform}) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("platform", "expected"),
|
|
[
|
|
("cli", "interactive"),
|
|
("tui", "interactive"),
|
|
("whatsapp_cloud", "gateway_message"),
|
|
("cron", "scheduled_task"),
|
|
("api_server", "api"),
|
|
("private-surface", "other"),
|
|
],
|
|
)
|
|
def test_task_start_fields_use_bounded_surface_and_entrypoint(platform, expected):
|
|
fields = task_start_fields({"platform": platform})
|
|
|
|
assert fields["entrypoint"] == expected
|
|
assert fields["execution_surface"] in EXECUTION_SURFACES
|
|
|
|
|
|
def test_task_start_fields_identify_delegated_work_without_exporting_parent_id():
|
|
fields = task_start_fields({
|
|
"platform": "cli",
|
|
"parent_session_id": "private-parent-session",
|
|
})
|
|
|
|
assert fields == {
|
|
"entrypoint": "delegated",
|
|
"execution_surface": "cli",
|
|
}
|
|
assert "private-parent-session" not in json.dumps(fields)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("duration_ms", "expected"),
|
|
[
|
|
(0, "lt_1s"),
|
|
(999, "lt_1s"),
|
|
(1_000, "1s_to_5s"),
|
|
(5_000, "5s_to_30s"),
|
|
(30_000, "30s_to_2m"),
|
|
(120_000, "2m_to_10m"),
|
|
(600_000, "gte_10m"),
|
|
],
|
|
)
|
|
def test_duration_bucket_boundaries(duration_ms, expected):
|
|
assert duration_bucket(duration_ms) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("count", "expected"),
|
|
[
|
|
(0, "0"),
|
|
(1, "1"),
|
|
(2, "2"),
|
|
(3, "3_to_5"),
|
|
(6, "6_to_10"),
|
|
(11, "gte_11"),
|
|
],
|
|
)
|
|
def test_count_bucket_boundaries(count, expected):
|
|
assert count_bucket(count) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("event", "expected"),
|
|
[
|
|
(
|
|
{"completed": True, "turn_exit_reason": "text_response(stop)"},
|
|
("success", "completed", "none"),
|
|
),
|
|
(
|
|
{"failed": True, "turn_exit_reason": "all_retries_exhausted_no_response"},
|
|
("failed", "failed", "none"),
|
|
),
|
|
(
|
|
{"interrupted": True, "turn_exit_reason": "interrupted_by_user"},
|
|
("cancelled", "user_cancelled", "user_cancelled"),
|
|
),
|
|
(
|
|
{"turn_exit_reason": "budget_exhausted"},
|
|
("failed", "iteration_limit", "system_aborted"),
|
|
),
|
|
(
|
|
{"turn_exit_reason": "guardrail_halt"},
|
|
("failed", "guardrail_blocked", "system_aborted"),
|
|
),
|
|
(
|
|
{"failed": True, "turn_exit_reason": "provider_timeout"},
|
|
("timed_out", "timed_out", "timed_out"),
|
|
),
|
|
(
|
|
{"failed": True, "turn_exit_reason": "approval_denied"},
|
|
("failed", "approval_denied", "none"),
|
|
),
|
|
],
|
|
)
|
|
def test_task_terminal_state_is_bounded(event, expected):
|
|
assert task_terminal_state(event) == expected
|
|
|
|
|
|
def test_model_outcome_fails_closed_to_a_bounded_value():
|
|
assert model_call_outcome({"outcome": "private"}) == "failed"
|
|
|
|
|
|
def test_unlisted_model_collapses_to_a_bounded_value():
|
|
assert model_family({"model": "private-model-name"}) == "unknown"
|
|
|
|
|
|
def test_subscriber_contract_rejects_unknown_fields_and_dimension_values():
|
|
event = SimpleNamespace(
|
|
kind="scope",
|
|
category="llm",
|
|
category_profile={"model_name": "gpt"},
|
|
name="hermes.model_call",
|
|
scope_category="end",
|
|
metadata={"hermes.metrics.schema_version": "hermes.metrics.event.v1"},
|
|
data={
|
|
"call_role": "primary",
|
|
"locality": "remote",
|
|
"model_family": "gpt",
|
|
"outcome": "success",
|
|
"provider_family": "direct",
|
|
},
|
|
)
|
|
|
|
assert model_call_dimensions(event) == {
|
|
"call_role": "primary",
|
|
"locality": "remote",
|
|
"model_family": "gpt",
|
|
"outcome": "success",
|
|
"provider_family": "direct",
|
|
}
|
|
event.category_profile["model_name"] = "private-model-name"
|
|
assert model_call_dimensions(event) is None
|
|
event.category_profile["model_name"] = "gpt"
|
|
event.data["prompt"] = "must-not-pass"
|
|
assert model_call_dimensions(event) is None
|
|
event.data.pop("prompt")
|
|
event.metadata["prompt"] = "must-not-pass"
|
|
assert model_call_dimensions(event) is None
|
|
event.metadata.pop("prompt")
|
|
event.category_profile["private"] = "must-not-pass"
|
|
assert model_call_dimensions(event) is None
|
|
event.category_profile.pop("private")
|
|
event.category = "function"
|
|
assert model_call_dimensions(event) is None
|
|
|
|
|
|
def test_task_subscriber_contract_accepts_only_bounded_scope_events():
|
|
start = SimpleNamespace(
|
|
kind="scope",
|
|
category="function",
|
|
category_profile=None,
|
|
name="hermes.task_run",
|
|
scope_category="start",
|
|
metadata={"hermes.metrics.schema_version": "hermes.metrics.event.v1"},
|
|
data={"entrypoint": "interactive", "execution_surface": "cli"},
|
|
)
|
|
assert task_counter(start) == (
|
|
"hermes.task_run.started",
|
|
{"entrypoint": "interactive", "execution_surface": "cli"},
|
|
)
|
|
|
|
terminal_fields = task_terminal_fields(
|
|
{
|
|
"platform": "cli",
|
|
"completed": True,
|
|
"turn_exit_reason": "text_response(stop)",
|
|
},
|
|
duration_ms=6_000,
|
|
model_call_count=2,
|
|
tool_call_count=3,
|
|
retry_count=1,
|
|
)
|
|
end = SimpleNamespace(**{
|
|
**start.__dict__,
|
|
"scope_category": "end",
|
|
"data": terminal_fields,
|
|
})
|
|
assert task_counter(end) == (
|
|
"hermes.task_run.finished",
|
|
terminal_fields,
|
|
)
|
|
|
|
end.data["task_id"] = "must-not-pass"
|
|
assert task_counter(end) is None
|
|
end.data.pop("task_id")
|
|
end.data["outcome"] = "private"
|
|
assert task_counter(end) is None
|
|
end.data["outcome"] = "success"
|
|
end.metadata["prompt"] = "must-not-pass"
|
|
assert task_counter(end) is None
|
|
|
|
|
|
def test_store_rejects_an_unsupported_schema_version(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
with sqlite3.connect(database_path) as connection:
|
|
connection.execute(
|
|
"CREATE TABLE telemetry_state (key TEXT PRIMARY KEY, value TEXT NOT NULL)"
|
|
)
|
|
connection.execute(
|
|
"INSERT INTO telemetry_state(key, value) VALUES ('schema_version', '999')"
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="Unsupported shared-metrics store schema"):
|
|
SharedMetricsStore(database_path, tmp_path / "outbox")
|
|
|
|
with sqlite3.connect(database_path) as connection:
|
|
[schema_version] = connection.execute(
|
|
"SELECT value FROM telemetry_state WHERE key = 'schema_version'"
|
|
).fetchone()
|
|
assert schema_version == "999"
|
|
|
|
|
|
def test_pending_metrics_keep_the_version_recorded_at_event_time(tmp_path):
|
|
store = SharedMetricsStore(tmp_path / "metrics.sqlite3", tmp_path / "outbox")
|
|
store.record_model_call(_dimensions(), "version-a")
|
|
store.record_model_call(_dimensions(), "version-b")
|
|
|
|
packages = [
|
|
json.loads(path.read_text(encoding="utf-8"))
|
|
for path in store.create_and_export_package()
|
|
]
|
|
|
|
assert {package["resource"]["hermes_version"] for package in packages} == {
|
|
"version-a",
|
|
"version-b",
|
|
}
|
|
assert all(package["metrics"][0]["value"] == 1 for package in packages)
|
|
|
|
|
|
def test_store_exports_task_started_and_terminal_counters(tmp_path):
|
|
store = SharedMetricsStore(tmp_path / "metrics.sqlite3", tmp_path / "outbox")
|
|
store.record_counter(
|
|
"hermes.task_run.started",
|
|
{"entrypoint": "interactive", "execution_surface": "cli"},
|
|
"test-version",
|
|
)
|
|
terminal = task_terminal_fields(
|
|
{
|
|
"platform": "cli",
|
|
"completed": True,
|
|
"turn_exit_reason": "text_response(stop)",
|
|
},
|
|
duration_ms=2_000,
|
|
model_call_count=1,
|
|
tool_call_count=2,
|
|
retry_count=0,
|
|
)
|
|
store.record_counter("hermes.task_run.finished", terminal, "test-version")
|
|
|
|
[package_path] = store.create_and_export_package()
|
|
package = json.loads(package_path.read_text(encoding="utf-8"))
|
|
_schema_validator().validate(package)
|
|
|
|
assert {metric["name"] for metric in package["metrics"]} == {
|
|
"hermes.task_run.finished",
|
|
"hermes.task_run.started",
|
|
}
|
|
|
|
|
|
def test_package_schema_rejects_unknown_fields(tmp_path):
|
|
store = SharedMetricsStore(tmp_path / "metrics.sqlite3", tmp_path / "outbox")
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
[package_path] = store.create_and_export_package()
|
|
package = json.loads(package_path.read_text(encoding="utf-8"))
|
|
invalid_package = deepcopy(package)
|
|
invalid_package["prompt"] = "must-not-be-accepted"
|
|
|
|
jsonschema = pytest.importorskip("jsonschema")
|
|
with pytest.raises(jsonschema.ValidationError):
|
|
_schema_validator().validate(invalid_package)
|
|
|
|
|
|
def test_store_rejects_dimensions_outside_the_metric_contract(tmp_path):
|
|
store = SharedMetricsStore(tmp_path / "metrics.sqlite3", tmp_path / "outbox")
|
|
|
|
with pytest.raises(ValueError, match="Unsupported dimensions"):
|
|
store.record_counter(
|
|
"hermes.model_call.count",
|
|
{"prompt": "must-not-be-persisted"},
|
|
"test-version",
|
|
)
|
|
|
|
assert store.counter_snapshot() == []
|
|
|
|
|
|
def test_package_builder_rejects_tampered_dimensions(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
with sqlite3.connect(database_path) as connection:
|
|
connection.execute(
|
|
"UPDATE counter_aggregates SET dimensions_json = ?",
|
|
(json.dumps({"prompt": "must-not-be-exported"}),),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Unsupported dimensions"):
|
|
store.create_and_export_package()
|
|
|
|
assert list(outbox_directory.glob("*.json")) == []
|
|
|
|
|
|
def test_pending_package_retry_reuses_the_same_package_and_file(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
[package_path] = store.create_and_export_package()
|
|
original_payload = package_path.read_bytes()
|
|
|
|
with sqlite3.connect(database_path) as connection:
|
|
connection.execute("UPDATE package_outbox SET exported_at = NULL")
|
|
|
|
restarted = SharedMetricsStore(database_path, outbox_directory)
|
|
assert restarted.create_and_export_package() == [package_path]
|
|
assert package_path.read_bytes() == original_payload
|
|
assert list(outbox_directory.glob("*.json")) == [package_path]
|
|
|
|
|
|
def test_retention_prunes_only_expired_exported_history(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
|
|
store.record_model_call(_dimensions(), "expired-version")
|
|
[expired_path] = store.create_and_export_package()
|
|
store.record_model_call(_dimensions(), "current-version")
|
|
[current_path] = store.create_and_export_package()
|
|
store.record_model_call(_dimensions(), "pending-version")
|
|
pending_package = store._create_package()
|
|
assert pending_package is not None
|
|
|
|
with sqlite3.connect(database_path) as connection:
|
|
connection.execute(
|
|
"""
|
|
UPDATE counter_aggregates
|
|
SET period_start = '2026-05-01'
|
|
WHERE hermes_version = 'expired-version'
|
|
"""
|
|
)
|
|
connection.execute(
|
|
"""
|
|
UPDATE package_outbox
|
|
SET period_start = '2026-05-01T00:00:00Z',
|
|
period_end = '2026-05-02T00:00:00Z',
|
|
exported_at = '2026-05-02T00:00:00Z'
|
|
WHERE package_id = ?
|
|
""",
|
|
(expired_path.stem,),
|
|
)
|
|
|
|
store._prune_expired_history(
|
|
now=datetime(2026, 7, 23, tzinfo=timezone.utc)
|
|
)
|
|
|
|
assert not expired_path.exists()
|
|
assert current_path.exists()
|
|
assert not (outbox_directory / f"{pending_package['package_id']}.json").exists()
|
|
with sqlite3.connect(database_path) as connection:
|
|
outbox_rows = connection.execute(
|
|
"SELECT package_id, exported_at FROM package_outbox ORDER BY package_id"
|
|
).fetchall()
|
|
aggregate_versions = {
|
|
row[0]
|
|
for row in connection.execute(
|
|
"SELECT hermes_version FROM counter_aggregates"
|
|
).fetchall()
|
|
}
|
|
assert {row[0] for row in outbox_rows} == {
|
|
current_path.stem,
|
|
pending_package["package_id"],
|
|
}
|
|
assert next(
|
|
row[1] for row in outbox_rows if row[0] == pending_package["package_id"]
|
|
) is None
|
|
assert aggregate_versions == {"current-version", "pending-version"}
|
|
|
|
|
|
def test_retention_failure_does_not_fail_a_committed_export(tmp_path, monkeypatch):
|
|
store = SharedMetricsStore(
|
|
tmp_path / "metrics.sqlite3",
|
|
tmp_path / "outbox",
|
|
)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
|
|
def fail_pruning():
|
|
raise OSError("retention unavailable")
|
|
|
|
monkeypatch.setattr(store, "_prune_expired_history", fail_pruning)
|
|
|
|
[package_path] = store.create_and_export_package()
|
|
|
|
assert package_path.exists()
|
|
assert store.counter_snapshot()[0]["packaged_value"] == 1
|
|
|
|
|
|
def test_file_export_failure_retries_committed_outbox_without_duplicate_delta(
|
|
tmp_path, monkeypatch
|
|
):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
|
|
def fail_write(*_args, **_kwargs):
|
|
raise OSError("simulated atomic export failure")
|
|
|
|
module_globals = SharedMetricsStore._export_pending_packages.__globals__
|
|
original_write = module_globals["atomic_json_write"]
|
|
monkeypatch.setitem(module_globals, "atomic_json_write", fail_write)
|
|
with pytest.raises(OSError, match="simulated atomic export failure"):
|
|
store.create_and_export_package()
|
|
|
|
with sqlite3.connect(database_path) as connection:
|
|
package_id, exported_at = connection.execute(
|
|
"SELECT package_id, exported_at FROM package_outbox"
|
|
).fetchone()
|
|
assert exported_at is None
|
|
assert store.counter_snapshot()[0]["packaged_value"] == 1
|
|
assert list(outbox_directory.glob("*.json")) == []
|
|
|
|
monkeypatch.setitem(module_globals, "atomic_json_write", original_write)
|
|
assert store.create_and_export_package() == [
|
|
outbox_directory / f"{package_id}.json"
|
|
]
|
|
assert len(list(outbox_directory.glob("*.json"))) == 1
|
|
assert store.create_and_export_package() == []
|
|
|
|
|
|
def test_package_export_does_not_chase_concurrent_updates(tmp_path, monkeypatch):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
original_create = store._create_package
|
|
create_calls = 0
|
|
|
|
def create_and_record_another():
|
|
nonlocal create_calls
|
|
create_calls += 1
|
|
package = original_create()
|
|
if create_calls == 1:
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
return package
|
|
|
|
monkeypatch.setattr(store, "_create_package", create_and_record_another)
|
|
first_paths = store.create_and_export_package()
|
|
|
|
assert create_calls == 1
|
|
assert len(first_paths) == 1
|
|
[counter] = store.counter_snapshot()
|
|
assert counter["metric_name"] == "hermes.model_call.count"
|
|
assert counter["dimensions"] == _dimensions()
|
|
assert counter["value"] == 2
|
|
assert counter["packaged_value"] == 1
|
|
|
|
second_paths = store.create_and_export_package()
|
|
assert len(second_paths) == 1
|
|
assert store.counter_snapshot()[0]["packaged_value"] == 2
|
|
|
|
|
|
def test_concurrent_package_builders_commit_one_delta(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
ready = threading.Barrier(2)
|
|
|
|
def export() -> list[Path]:
|
|
worker_store = SharedMetricsStore(database_path, outbox_directory)
|
|
ready.wait(timeout=5)
|
|
return worker_store.create_and_export_package()
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
futures = [executor.submit(export) for _ in range(2)]
|
|
for future in futures:
|
|
future.result()
|
|
|
|
with sqlite3.connect(database_path) as connection:
|
|
[outbox_count] = connection.execute(
|
|
"SELECT COUNT(*) FROM package_outbox"
|
|
).fetchone()
|
|
[package_path] = list(outbox_directory.glob("*.json"))
|
|
package = json.loads(package_path.read_text(encoding="utf-8"))
|
|
|
|
assert outbox_count == 1
|
|
assert package["metrics"][0]["value"] == 1
|
|
assert store.counter_snapshot()[0]["packaged_value"] == 1
|
|
|
|
|
|
def test_concurrent_due_exports_create_one_daily_package(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
ready = threading.Barrier(8)
|
|
|
|
def export() -> None:
|
|
worker_store = SharedMetricsStore(database_path, outbox_directory)
|
|
ready.wait(timeout=5)
|
|
worker_store.create_and_export_package_if_due()
|
|
|
|
with ThreadPoolExecutor(max_workers=8) as executor:
|
|
futures = [executor.submit(export) for _ in range(8)]
|
|
for future in futures:
|
|
future.result()
|
|
|
|
with sqlite3.connect(database_path) as connection:
|
|
[outbox_count] = connection.execute(
|
|
"SELECT COUNT(*) FROM package_outbox"
|
|
).fetchone()
|
|
assert outbox_count == 1
|
|
assert len(list(outbox_directory.glob("*.json"))) == 1
|
|
assert store.counter_snapshot()[0]["packaged_value"] == 1
|
|
|
|
|
|
def test_concurrent_model_call_updates_are_transactional(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
SharedMetricsStore(database_path, outbox_directory)
|
|
|
|
def record_calls(count: int) -> None:
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
for _ in range(count):
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
futures = [executor.submit(record_calls, 10) for _ in range(2)]
|
|
for future in futures:
|
|
future.result()
|
|
|
|
restarted = SharedMetricsStore(database_path, outbox_directory)
|
|
assert restarted.counter_snapshot()[0]["value"] == 20
|
|
|
|
|
|
def test_cross_process_model_call_updates_are_transactional(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
context = mp.get_context("spawn")
|
|
start_barrier = context.Barrier(2)
|
|
processes = [
|
|
context.Process(
|
|
target=_record_model_calls_in_process,
|
|
args=(str(database_path), str(outbox_directory), 10, start_barrier),
|
|
)
|
|
for _ in range(2)
|
|
]
|
|
|
|
for process in processes:
|
|
process.start()
|
|
for process in processes:
|
|
process.join(timeout=15)
|
|
assert not process.is_alive()
|
|
assert process.exitcode == 0
|
|
|
|
restarted = SharedMetricsStore(database_path, outbox_directory)
|
|
assert restarted.counter_snapshot()[0]["value"] == 20
|
|
|
|
|
|
def test_schema_initialization_waits_for_an_existing_writer(tmp_path):
|
|
database_path = tmp_path / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "outbox"
|
|
database_path.touch()
|
|
blocker = sqlite3.connect(database_path)
|
|
blocker.execute("BEGIN IMMEDIATE")
|
|
|
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
future = executor.submit(
|
|
SharedMetricsStore,
|
|
database_path,
|
|
outbox_directory,
|
|
)
|
|
try:
|
|
time.sleep(0.4)
|
|
assert not future.done()
|
|
finally:
|
|
blocker.rollback()
|
|
blocker.close()
|
|
store = future.result(timeout=2)
|
|
|
|
assert store.counter_snapshot() == []
|
|
|
|
|
|
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission modes are unavailable")
|
|
def test_store_and_export_are_owner_only(tmp_path):
|
|
database_path = tmp_path / "private-store" / "metrics.sqlite3"
|
|
outbox_directory = tmp_path / "private-outbox"
|
|
store = SharedMetricsStore(database_path, outbox_directory)
|
|
store.record_model_call(_dimensions(), "test-version")
|
|
[package_path] = store.create_and_export_package()
|
|
|
|
assert stat.S_IMODE(database_path.parent.stat().st_mode) == 0o700
|
|
assert stat.S_IMODE(outbox_directory.stat().st_mode) == 0o700
|
|
assert stat.S_IMODE(database_path.stat().st_mode) == 0o600
|
|
assert stat.S_IMODE(package_path.stat().st_mode) == 0o600
|