refactor(usage): address round-4 review on PR #871
- hooks: document why user_id in metadata is safe — it stays tenant-local (the CP rollup aggregates GROUP BY (day, metric) into usage_daily, which has no metadata column, so it never reaches Stripe) and is retained to keep Deck #67's future per-user attribution derivable from the app DB. - migration: instantiate the SQLite-side column types (sa.Text() etc.) for visual parity with the instantiated Postgres types. - tests: assert the WARNING contract in the unserializable-metadata test too; add an autouse fixture that resets UsageEventStore._shared_instance so a stray shared() call can't leak across tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3a8ea893c6
commit
9c2f9fac46
@@ -28,6 +28,19 @@ from nextcloud_mcp_server.usage.store import UsageEventStore
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_shared_usage_store():
|
||||
"""Keep the process-wide ``shared()`` cache from leaking across tests.
|
||||
|
||||
These tests construct ``UsageEventStore(storage)`` directly, but a stray
|
||||
``shared()`` call (here or in a smoke test sharing the process) would
|
||||
otherwise poison later tests with a stale storage handle.
|
||||
"""
|
||||
UsageEventStore._shared_instance = None
|
||||
yield
|
||||
UsageEventStore._shared_instance = None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def storage(storage_backend):
|
||||
"""Initialized RefreshTokenStorage backed by SQLite or Postgres."""
|
||||
@@ -231,7 +244,9 @@ async def test_best_effort_swallows_db_errors(storage, monkeypatch, caplog):
|
||||
)
|
||||
|
||||
|
||||
async def test_best_effort_swallows_unserializable_metadata(storage, monkeypatch):
|
||||
async def test_best_effort_swallows_unserializable_metadata(
|
||||
storage, monkeypatch, caplog
|
||||
):
|
||||
"""Non-serializable metadata is swallowed, not raised into the caller.
|
||||
|
||||
json.dumps runs inside the best-effort try, so a metadata value the JSON
|
||||
@@ -245,9 +260,16 @@ async def test_best_effort_swallows_unserializable_metadata(storage, monkeypatch
|
||||
bad_metadata = {"obj": object()}
|
||||
|
||||
# Must not raise.
|
||||
await store.record_usage_event(
|
||||
metric="pages_chunks", value=1, metadata=bad_metadata
|
||||
)
|
||||
with caplog.at_level(logging.WARNING, logger="nextcloud_mcp_server.usage.store"):
|
||||
await store.record_usage_event(
|
||||
metric="pages_chunks", value=1, metadata=bad_metadata
|
||||
)
|
||||
|
||||
# Nothing was written — the encode failed before the insert.
|
||||
assert await _count(storage) == 0
|
||||
# Silent data loss would be a footgun once metering is on: same WARNING
|
||||
# contract as the DB-error path.
|
||||
assert any(
|
||||
r.levelno == logging.WARNING and "usage metering write dropped" in r.message
|
||||
for r in caplog.records
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user