项目文件夹

文件
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

331 行
13 KiB
Python

"""Provider-level coverage tests for token_counter.py.
Targets specific uncovered lines:
- on_llm_start: model extraction fallbacks (kwargs direct, serialized kwargs model_name)
- on_llm_start: provider extraction for unrecognized _type with provider kwarg
- on_llm_end: token extraction from llm_output 'usage' key (alternative to 'token_usage')
- on_llm_end: Ollama context overflow with zero original_prompt_estimate guard
- _save_to_db: background thread success path with full token_data dict verification
"""
import threading
from unittest.mock import MagicMock, patch
from langchain_core.outputs import LLMResult
from local_deep_research.metrics.token_counter import TokenCountingCallback
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_callback(research_context=None, research_id="rid-test", **overrides):
"""Build a TokenCountingCallback with controllable state."""
ctx = research_context if research_context is not None else {}
cb = TokenCountingCallback(research_id=research_id, research_context=ctx)
for k, v in overrides.items():
setattr(cb, k, v)
return cb
def _make_llm_result(llm_output=None, generations=None):
"""Build a minimal mock LLMResult."""
result = MagicMock(spec=LLMResult)
result.llm_output = llm_output
result.generations = generations or []
return result
def _make_generation(usage_metadata=None, response_metadata=None):
"""Build a mock generation with a message carrying metadata."""
gen = MagicMock()
msg = MagicMock()
msg.usage_metadata = usage_metadata
msg.response_metadata = (
response_metadata if response_metadata is not None else {}
)
gen.message = msg
return gen
def _setup_model_counts(cb, model_name="test-model", provider="openai"):
"""Register model in cb.counts so on_llm_end can update them."""
cb.current_model = model_name
cb.current_provider = provider
cb.counts["by_model"][model_name] = {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
"calls": 1,
"provider": provider,
}
def _patch_worker_thread():
t = MagicMock()
t.name = "WorkerThread-1"
return patch.object(threading, "current_thread", return_value=t)
# ---------------------------------------------------------------------------
# 1. on_llm_start – model extraction fallback: kwargs["model"] when
# invocation_params has no model key (line 163)
# ---------------------------------------------------------------------------
class TestModelFromKwargsModel:
"""Model extracted from kwargs['model'] when invocation_params is empty."""
def test_model_from_kwargs_model_key(self):
"""kwargs['model'] is used when invocation_params has no model."""
cb = _make_callback()
cb.on_llm_start(
{},
["hello"],
invocation_params={},
model="gemma-2-27b",
)
assert cb.current_model == "gemma-2-27b"
def test_model_from_kwargs_model_name_key(self):
"""kwargs['model_name'] is used when invocation_params and kwargs['model'] are absent."""
cb = _make_callback()
cb.on_llm_start({}, ["hello"], model_name="codellama-34b")
assert cb.current_model == "codellama-34b"
# ---------------------------------------------------------------------------
# 2. on_llm_start – model from serialized["kwargs"]["model_name"]
# (line 167-169, specifically the model_name branch)
# ---------------------------------------------------------------------------
class TestModelFromSerializedKwargsModelName:
"""Model extracted from serialized['kwargs']['model_name'] when 'model' absent."""
def test_serialized_kwargs_model_name(self):
"""serialized['kwargs']['model_name'] provides the model when 'model' key missing."""
cb = _make_callback()
cb.on_llm_start(
{"kwargs": {"model_name": "deepseek-coder"}},
["prompt"],
)
assert cb.current_model == "deepseek-coder"
# ---------------------------------------------------------------------------
# 3. on_llm_start – provider from kwargs when _type is unrecognized
# (line 210-211: else branch inside the if "_type" in serialized block)
# ---------------------------------------------------------------------------
class TestProviderFromKwargsWithUnrecognizedType:
"""When _type exists but is not ChatOllama/ChatOpenAI/ChatAnthropic,
provider falls back to kwargs.get('provider', 'unknown')."""
def test_unrecognized_type_with_provider_kwarg(self):
"""Unrecognized _type uses provider kwarg."""
cb = _make_callback()
cb.on_llm_start(
{"_type": "ChatGoogleGenAI", "kwargs": {"model": "gemini-pro"}},
["prompt"],
provider="google",
)
assert cb.current_provider == "google"
def test_unrecognized_type_without_provider_kwarg(self):
"""Unrecognized _type without provider kwarg defaults to 'unknown'."""
cb = _make_callback()
cb.on_llm_start(
{"_type": "ChatGoogleGenAI", "kwargs": {"model": "gemini-pro"}},
["prompt"],
)
assert cb.current_provider == "unknown"
# ---------------------------------------------------------------------------
# 4. on_llm_end – token extraction from llm_output 'usage' key
# (line 240-241: the or branch: response.llm_output.get("usage", {}))
# ---------------------------------------------------------------------------
class TestTokenExtractionFromUsageKey:
"""Tokens extracted from llm_output['usage'] when 'token_usage' absent."""
def test_usage_key_provides_tokens(self):
"""llm_output['usage'] provides prompt_tokens/completion_tokens."""
cb = _make_callback()
_setup_model_counts(cb, "gpt-4o", "openai")
result = _make_llm_result(
llm_output={
"usage": {
"prompt_tokens": 45,
"completion_tokens": 30,
"total_tokens": 75,
}
},
)
cb.on_llm_end(result)
assert cb.counts["total_prompt_tokens"] == 45
assert cb.counts["total_completion_tokens"] == 30
assert cb.counts["total_tokens"] == 75
# ---------------------------------------------------------------------------
# 5. on_llm_end – Ollama context overflow: original_prompt_estimate == 0
# guards against division-by-zero (line 312-314)
# ---------------------------------------------------------------------------
class TestContextOverflowZeroPromptEstimate:
"""When original_prompt_estimate is 0 the truncation_ratio is 0, not a ZeroDivisionError."""
def test_zero_prompt_estimate_no_division_error(self):
"""truncation_ratio stays 0 when original_prompt_estimate is 0."""
cb = _make_callback(research_context={"context_limit": 100})
_setup_model_counts(cb, "llama3", "ollama")
# context_limit is set during on_llm_start; set it directly here
cb.context_limit = 100
cb.original_prompt_estimate = 0
gen = _make_generation(
usage_metadata=None,
response_metadata={
"prompt_eval_count": 96, # >= 95 (95% of 100)
"eval_count": 20,
},
)
result = _make_llm_result(generations=[[gen]])
cb.on_llm_end(result)
assert cb.context_truncated is True
# original_prompt_estimate (0) <= prompt_eval_count (96), so
# the inner if branch is not entered; tokens_truncated stays 0
assert cb.tokens_truncated == 0
assert cb.truncation_ratio == 0.0
def test_context_overflow_calculates_truncation(self):
"""When prompt estimate > actual, truncation fields are populated."""
cb = _make_callback(research_context={"context_limit": 1000})
_setup_model_counts(cb, "llama3", "ollama")
# context_limit is set during on_llm_start; set it directly here
cb.context_limit = 1000
cb.original_prompt_estimate = 1200
gen = _make_generation(
usage_metadata=None,
response_metadata={
"prompt_eval_count": 960, # >= 950 (95% of 1000)
"eval_count": 50,
},
)
result = _make_llm_result(generations=[[gen]])
cb.on_llm_end(result)
assert cb.context_truncated is True
assert cb.tokens_truncated == 240 # 1200 - 960
expected_ratio = 240 / 1200
assert abs(cb.truncation_ratio - expected_ratio) < 1e-9
# ---------------------------------------------------------------------------
# 6. _save_to_db – background thread success path
# (lines 408-482): verifies metrics_writer is called with correct data
# ---------------------------------------------------------------------------
class TestSaveToDbBackgroundThread:
"""_save_to_db in a background thread writes via metrics_writer."""
def test_metrics_writer_called_with_token_data(self):
"""metrics_writer.write_token_metrics receives correct token_data dict."""
cb = _make_callback(
research_context={
"username": "alice",
"user_password": "secret",
"research_query": "quantum computing",
"research_mode": "detailed",
"research_phase": "search",
"search_iteration": 2,
"search_engines_planned": ["google", "bing"],
"search_engine_selected": "google",
},
research_id="rid-42",
)
cb.current_model = "gpt-4o"
cb.current_provider = "openai"
cb.response_time_ms = 350
cb.success_status = "success"
cb.error_type = None
cb.calling_file = "runner.py"
cb.calling_function = "execute"
cb.call_stack = "runner.py:execute:10"
with _patch_worker_thread():
with patch(
"local_deep_research.database.thread_metrics.metrics_writer"
) as mock_writer:
cb._save_to_db(prompt_tokens=100, completion_tokens=50)
mock_writer.set_user_password.assert_called_once_with(
"alice", "secret"
)
mock_writer.write_token_metrics.assert_called_once()
call_args = mock_writer.write_token_metrics.call_args
assert call_args[0][0] == "alice"
assert call_args[0][1] == "rid-42"
token_data = call_args[0][2]
assert token_data["model_name"] == "gpt-4o"
assert token_data["provider"] == "openai"
assert token_data["prompt_tokens"] == 100
assert token_data["completion_tokens"] == 50
assert token_data["research_query"] == "quantum computing"
assert token_data["research_mode"] == "detailed"
assert token_data["research_phase"] == "search"
assert token_data["search_iteration"] == 2
# search_engines_planned list should be JSON-serialized
assert (
token_data["search_engines_planned"] == '["google", "bing"]'
)
assert token_data["search_engine_selected"] == "google"
assert token_data["response_time_ms"] == 350
assert token_data["calling_file"] == "runner.py"
def test_no_username_logs_warning_and_returns(self):
"""Missing username in research_context causes early return."""
cb = _make_callback(
research_context={"user_password": "secret"},
research_id="rid-99",
)
cb.current_model = "gpt-4"
cb.current_provider = "openai"
with _patch_worker_thread():
with patch(
"local_deep_research.database.thread_metrics.metrics_writer"
) as mock_writer:
cb._save_to_db(prompt_tokens=10, completion_tokens=5)
mock_writer.write_token_metrics.assert_not_called()
def test_no_password_logs_warning_and_returns(self):
"""Missing password in research_context causes early return after username check."""
cb = _make_callback(
research_context={"username": "bob"},
research_id="rid-88",
)
cb.current_model = "gpt-4"
cb.current_provider = "openai"
with _patch_worker_thread():
with patch(
"local_deep_research.database.thread_metrics.metrics_writer"
) as mock_writer:
cb._save_to_db(prompt_tokens=10, completion_tokens=5)
mock_writer.write_token_metrics.assert_not_called()