Files
learningcircuit--local-deep…/tests/metrics/test_query_utils_truncation_summary.py
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

263 lines
8.6 KiB
Python

"""Tests for get_context_overflow_truncation_summary helper.
The helper unifies truncation aggregation between /metrics/api/metrics
and /metrics/api/context-overflow. These tests pin its semantics so the
two endpoints cannot drift apart.
"""
from datetime import datetime, timedelta, timezone
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from local_deep_research.database.models import Base, TokenUsage
from local_deep_research.metrics.query_utils import (
get_context_overflow_truncation_summary,
)
@pytest.fixture
def engine():
eng = create_engine("sqlite:///:memory:")
Base.metadata.create_all(eng)
yield eng
eng.dispose()
@pytest.fixture
def session(engine):
Session = sessionmaker(bind=engine)
s = Session()
yield s
s.close()
def _make_usage(
*,
minutes_ago: int = 0,
context_limit: int | None = 8192,
context_truncated: bool = False,
tokens_truncated: int | None = None,
prompt_tokens: int = 100,
research_mode: str | None = None,
) -> TokenUsage:
return TokenUsage(
research_id=f"r-{minutes_ago}-{context_truncated}-{research_mode}",
model_provider="openai",
model_name="gpt-4",
prompt_tokens=prompt_tokens,
completion_tokens=50,
total_tokens=prompt_tokens + 50,
timestamp=datetime.now(timezone.utc) - timedelta(minutes=minutes_ago),
context_limit=context_limit,
context_truncated=context_truncated,
tokens_truncated=tokens_truncated,
research_mode=research_mode,
)
class TestEmpty:
def test_empty_db_returns_zeros(self, session):
result = get_context_overflow_truncation_summary(session, "30d")
assert result == {
"total_requests": 0,
"requests_with_context": 0,
"truncated_requests": 0,
"truncation_rate": 0.0,
"avg_tokens_truncated": 0.0,
"total_tokens": 0,
"total_prompt_tokens": 0,
"total_completion_tokens": 0,
"avg_prompt_tokens": 0.0,
"avg_completion_tokens": 0.0,
"max_prompt_tokens": 0,
}
class TestCounts:
def test_mixed_rows_count_correctly(self, session):
# 4 rows: one with no context_limit, two with context but not truncated,
# one truncated.
session.add_all(
[
_make_usage(context_limit=None),
_make_usage(context_limit=4096, context_truncated=False),
_make_usage(context_limit=8192, context_truncated=False),
_make_usage(
context_limit=8192,
context_truncated=True,
tokens_truncated=200,
),
]
)
session.commit()
result = get_context_overflow_truncation_summary(session, "30d")
assert result["total_requests"] == 4
assert result["requests_with_context"] == 3
assert result["truncated_requests"] == 1
# 1/3 of context-aware requests truncated
assert result["truncation_rate"] == pytest.approx(33.333333, rel=1e-3)
assert result["avg_tokens_truncated"] == 200.0
# Token-summary aggregates over the same 4 rows (each: prompt=100,
# completion=50, total=150). These must be asserted directly because
# the test_empty_db_returns_zeros case can't catch a non-zero bug
# (e.g. SUM accidentally returning per-row count instead of sum).
assert result["total_tokens"] == 600
assert result["total_prompt_tokens"] == 400
assert result["total_completion_tokens"] == 200
assert result["avg_prompt_tokens"] == pytest.approx(100.0)
assert result["avg_completion_tokens"] == pytest.approx(50.0)
assert result["max_prompt_tokens"] == 100
class TestPeriodFilter:
def test_7d_excludes_older_rows(self, session):
session.add_all(
[
_make_usage(minutes_ago=60), # 1 hour ago, in window
_make_usage(minutes_ago=10 * 24 * 60), # 10 days ago, out
]
)
session.commit()
result_7d = get_context_overflow_truncation_summary(session, "7d")
result_30d = get_context_overflow_truncation_summary(session, "30d")
assert result_7d["total_requests"] == 1
assert result_30d["total_requests"] == 2
def test_all_period_includes_old_rows(self, session):
# A row 2 years old should be excluded by 1y but included by all.
session.add_all(
[
_make_usage(minutes_ago=60),
_make_usage(minutes_ago=2 * 365 * 24 * 60),
]
)
session.commit()
result_1y = get_context_overflow_truncation_summary(session, "1y")
result_all = get_context_overflow_truncation_summary(session, "all")
assert result_1y["total_requests"] == 1
assert result_all["total_requests"] == 2
class TestUnknownPeriodFallback:
def test_unknown_period_uses_30d_window(self, session):
# Inside 30d, outside 30d.
session.add_all(
[
_make_usage(minutes_ago=60),
_make_usage(minutes_ago=60 * 24 * 60), # 60 days ago
]
)
session.commit()
# "foo" is not a valid period — helper falls back to 30 days, matching
# the route's defensive whitelist behavior end-to-end.
result = get_context_overflow_truncation_summary(session, "foo")
assert result["total_requests"] == 1
class TestNullTokensTruncated:
def test_null_tokens_truncated_yields_zero_not_nan(self, session):
# Truncated row but tokens_truncated=NULL — func.avg ignores NULL,
# the `or 0` guard then prevents NaN/None leaking out.
session.add(
_make_usage(
context_limit=8192,
context_truncated=True,
tokens_truncated=None,
)
)
session.commit()
result = get_context_overflow_truncation_summary(session, "30d")
assert result["truncated_requests"] == 1
assert result["avg_tokens_truncated"] == 0.0
class TestDivisionByZeroGuard:
def test_no_context_data_keeps_rate_zero(self, session):
# Rows exist, but none have a context_limit — truncation_rate must
# still be 0.0 (not NaN) since the denominator is requests_with_context.
session.add_all(
[
_make_usage(context_limit=None),
_make_usage(context_limit=None),
]
)
session.commit()
result = get_context_overflow_truncation_summary(session, "30d")
assert result["total_requests"] == 2
assert result["requests_with_context"] == 0
assert result["truncation_rate"] == 0.0
class TestResearchModeFilter:
def test_default_includes_all_modes(self, session):
session.add_all(
[
_make_usage(research_mode="quick"),
_make_usage(research_mode="detailed"),
_make_usage(research_mode=None),
]
)
session.commit()
# Default research_mode is "all" — no mode filter applied
result = get_context_overflow_truncation_summary(session, "30d")
assert result["total_requests"] == 3
def test_quick_excludes_detailed(self, session):
session.add_all(
[
_make_usage(research_mode="quick"),
_make_usage(research_mode="detailed"),
_make_usage(research_mode="detailed"),
]
)
session.commit()
result = get_context_overflow_truncation_summary(
session, "30d", research_mode="quick"
)
assert result["total_requests"] == 1
def test_detailed_excludes_quick(self, session):
session.add_all(
[
_make_usage(research_mode="quick"),
_make_usage(research_mode="detailed"),
_make_usage(research_mode="detailed"),
]
)
session.commit()
result = get_context_overflow_truncation_summary(
session, "30d", research_mode="detailed"
)
assert result["total_requests"] == 2
def test_all_mode_string_includes_everything(self, session):
session.add_all(
[
_make_usage(research_mode="quick"),
_make_usage(research_mode="detailed"),
]
)
session.commit()
# Explicit "all" matches the default (no filter)
result = get_context_overflow_truncation_summary(
session, "30d", research_mode="all"
)
assert result["total_requests"] == 2