Files
wehub-resource-sync 0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:20 +08:00

182 lines
6.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import builtins
from dataclasses import dataclass
from types import SimpleNamespace
import pytest
import headroom.relevance as relevance_mod
from headroom.relevance import (
BM25Scorer,
EmbeddingScorer,
HybridScorer,
create_scorer,
embedding,
hybrid,
)
from headroom.relevance.base import RelevanceScore, RelevanceScorer, default_batch_score
@dataclass
class DummyRelevanceScorer(RelevanceScorer):
def score(self, item: str, context: str) -> RelevanceScore:
return RelevanceScore(score=0.4, reason=f"{item}:{context}")
def score_batch(self, items: list[str], context: str) -> list[RelevanceScore]:
return [RelevanceScore(score=0.2, reason=context) for _ in items]
def test_base_default_batch_and_abstract_methods() -> None:
scorer = DummyRelevanceScorer()
batch = default_batch_score(scorer, ["a", "b"], "ctx")
assert [item.reason for item in batch] == ["a:ctx", "b:ctx"]
assert RelevanceScorer.score(scorer, "a", "ctx") is None
assert RelevanceScorer.score_batch(scorer, ["a"], "ctx") is None
assert RelevanceScorer.is_available() is True
def test_create_scorer_embedding_unavailable_branch(monkeypatch) -> None:
monkeypatch.setattr(
relevance_mod.EmbeddingScorer, "is_available", classmethod(lambda cls: False)
)
with pytest.raises(RuntimeError, match="sentence-transformers"):
create_scorer("embedding")
def test_bm25_internal_paths_and_non_normalized_mode() -> None:
scorer = BM25Scorer(normalize_score=False)
assert scorer._tokenize("") == []
assert scorer._compute_idf("x", doc_count=1, doc_freq=0) == 0.0
assert scorer._compute_idf("x", doc_count=1, doc_freq=1) > 0
assert scorer._bm25_score([], ["a"]) == (0.0, [])
assert scorer._bm25_score(["a"], []) == (0.0, [])
no_match = scorer.score("hello world", "missing")
assert no_match.reason == "BM25: no term matches"
one_match = scorer.score("find alice", "alice")
assert one_match.reason == "BM25: matched 'alice'"
assert one_match.score > 0
many_match = scorer.score("alpha beta gamma delta", "alpha beta gamma delta")
assert many_match.reason.startswith("BM25: matched 4 terms")
batch = scorer.score_batch(["alpha", "alpha beta"], "alpha beta")
assert [item.reason for item in batch] == ["BM25: 1 terms", "BM25: 2 terms"]
def test_embedding_numpy_and_model_error_paths(monkeypatch) -> None:
embedding._numpy = None
real_import = builtins.__import__
def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == "numpy":
raise ImportError("missing")
return real_import(name, globals, locals, fromlist, level)
monkeypatch.setattr(builtins, "__import__", fake_import)
with pytest.raises(ImportError, match="numpy is required"):
embedding._get_numpy()
monkeypatch.setattr(builtins, "__import__", real_import)
fake_np = SimpleNamespace(
linalg=SimpleNamespace(norm=lambda value: 0 if value == [0, 0] else 1),
dot=lambda a, b: -1,
)
monkeypatch.setattr(embedding, "_numpy", fake_np)
assert embedding._cosine_similarity([0, 0], [1, 0]) == 0.0
assert embedding._cosine_similarity([1, 0], [0, 1]) == 0.0
monkeypatch.setattr(EmbeddingScorer, "is_available", classmethod(lambda cls: False))
with pytest.raises(RuntimeError, match="requires fastembed"):
EmbeddingScorer()._get_model()
def test_embedding_score_empty_and_batch_shortcuts() -> None:
scorer = EmbeddingScorer()
assert scorer.score("", "ctx").reason == "Embedding: empty input"
assert scorer.score("item", "").reason == "Embedding: empty input"
assert scorer.score_batch([], "ctx") == []
assert scorer.score_batch(["item"], "")[0].reason == "Embedding: empty context"
def test_embedding_score_and_batch_with_fake_model(monkeypatch) -> None:
scorer = EmbeddingScorer()
monkeypatch.setattr(
scorer,
"_encode",
lambda texts: (
[[1.0, 0.0], [0.5, 0.5]] if len(texts) == 2 else [[1.0, 0.0], [0.0, 1.0], [1.0, 0.0]]
),
)
monkeypatch.setattr(
embedding, "_cosine_similarity", lambda a, b: 0.75 if a == [1.0, 0.0] else 0.25
)
single = scorer.score("item", "ctx")
assert single.score == 0.75
assert single.reason == "Embedding: semantic similarity 0.75"
batch = scorer.score_batch(["first", "second"], "ctx")
assert [item.score for item in batch] == [0.75, 0.25]
assert [item.reason for item in batch] == ["Embedding: 0.75", "Embedding: 0.25"]
def test_hybrid_constructor_alpha_variants_and_single_score_paths(monkeypatch) -> None:
bm25_result = RelevanceScore(score=0.1, reason="bm25", matched_terms=["term"])
emb_result = RelevanceScore(score=0.9, reason="emb", matched_terms=[])
class FakeBM25:
def score(self, item: str, context: str) -> RelevanceScore:
return bm25_result
def score_batch(self, items: list[str], context: str) -> list[RelevanceScore]:
return [bm25_result for _ in items]
class FakeEmbedding:
def score(self, item: str, context: str) -> RelevanceScore:
return emb_result
def score_batch(self, items: list[str], context: str) -> list[RelevanceScore]:
return [emb_result for _ in items]
scorer = HybridScorer(
alpha=0.4, adaptive=True, bm25_scorer=FakeBM25(), embedding_scorer=FakeEmbedding()
)
assert scorer.has_embedding_support() is True
assert scorer._compute_alpha("find id 1234") == 0.65
assert scorer._compute_alpha("find host api.example.com") == 0.6
assert scorer._compute_alpha("find email test@example.com") == 0.6
single = scorer.score("item", "show me errors")
assert single.score == pytest.approx(0.58)
assert "Hybrid (α=0.40): BM25=0.10, Semantic=0.90" == single.reason
batch = scorer.score_batch(["a", "b"], "show me errors")
assert len(batch) == 2
assert batch[0].reason == "Hybrid (α=0.40): BM25=0.10, Emb=0.90"
def test_hybrid_fallback_and_empty_batch(monkeypatch) -> None:
scorer = HybridScorer(bm25_scorer=BM25Scorer())
scorer._embedding_available = False
scorer.embedding = None
empty = scorer.score_batch([], "ctx")
assert empty == []
boosted = scorer.score('{"id":"123","name":"alice"}', "alice")
assert boosted.score >= 0.3
assert "BM25 only, boosted" in boosted.reason
boosted_batch = scorer.score_batch(['{"id":"123"}', '{"id":"456"}'], "123 456")
assert all("BM25 only, boosted" in item.reason for item in boosted_batch)
def test_hybrid_auto_fallback_when_embeddings_unavailable(monkeypatch) -> None:
monkeypatch.setattr(hybrid.EmbeddingScorer, "is_available", classmethod(lambda cls: False))
scorer = HybridScorer()
assert scorer.has_embedding_support() is False