mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 13:26:48 +00:00
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
156 lines
5.8 KiB
Python
156 lines
5.8 KiB
Python
"""Tests for the knowledge_search tool."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from openjarvis.connectors.store import KnowledgeStore
|
|
from openjarvis.core.registry import ToolRegistry
|
|
from openjarvis.tools.knowledge_search import KnowledgeSearchTool
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixture
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture()
|
|
def store(tmp_path):
|
|
"""Return a KnowledgeStore pre-loaded with 3 diverse items."""
|
|
s = KnowledgeStore(db_path=tmp_path / "test_knowledge.db")
|
|
|
|
# Item 1 — gmail email from alice
|
|
s.store(
|
|
"Meeting about Kubernetes migration scheduled for next Tuesday.",
|
|
source="gmail",
|
|
doc_type="email",
|
|
title="Re: K8s migration",
|
|
author="alice@example.com",
|
|
url="https://mail.google.com/mail/u/0/#inbox/abc123",
|
|
timestamp="2026-01-15T10:00:00Z",
|
|
)
|
|
|
|
# Item 2 — slack message from bob
|
|
s.store(
|
|
"Discussion about K8s costs — we should consider spot instances.",
|
|
source="slack",
|
|
doc_type="message",
|
|
title="#infrastructure",
|
|
author="bob@example.com",
|
|
url="slack://thread/def456",
|
|
timestamp="2026-01-20T14:30:00Z",
|
|
)
|
|
|
|
# Item 3 — obsidian document from sarah
|
|
s.store(
|
|
"Research notes on large language model fine-tuning strategies.",
|
|
source="obsidian",
|
|
doc_type="document",
|
|
title="LLM Fine-tuning Notes",
|
|
author="sarah@example.com",
|
|
url="obsidian://vault/llm-notes",
|
|
timestamp="2026-02-01T09:00:00Z",
|
|
)
|
|
|
|
yield s
|
|
s.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestKnowledgeSearchTool:
|
|
def test_basic_search(self, store):
|
|
"""Search finds the matching document from the 3-item store."""
|
|
tool = KnowledgeSearchTool(store=store)
|
|
result = tool.execute(query="Kubernetes migration")
|
|
assert result.success is True
|
|
assert "Kubernetes" in result.content
|
|
assert result.metadata["num_results"] >= 1
|
|
|
|
def test_filter_by_source(self, store):
|
|
"""source filter restricts results to gmail only."""
|
|
tool = KnowledgeSearchTool(store=store)
|
|
result = tool.execute(query="Kubernetes", source="gmail")
|
|
assert result.success is True
|
|
assert "gmail" in result.content
|
|
# Every returned result must come from gmail
|
|
assert "slack" not in result.content
|
|
|
|
def test_filter_by_author(self, store):
|
|
"""author filter restricts results to sarah only."""
|
|
tool = KnowledgeSearchTool(store=store)
|
|
# Use "language model" — FTS5 does not tokenise hyphenated terms like
|
|
# "fine-tuning" as a single token, so we search for a phrase that works.
|
|
result = tool.execute(query="language model", author="sarah@example.com")
|
|
assert result.success is True
|
|
assert "sarah@example.com" in result.content
|
|
assert result.metadata["num_results"] >= 1
|
|
|
|
def test_no_results(self, store):
|
|
"""Query matching nothing returns success=True with 'No relevant results'."""
|
|
tool = KnowledgeSearchTool(store=store)
|
|
result = tool.execute(query="zzz_nonexistent_xyzzy_12345")
|
|
assert result.success is True
|
|
assert "No relevant results" in result.content
|
|
assert result.metadata["num_results"] == 0
|
|
|
|
def test_empty_query(self, store):
|
|
"""Empty query string returns success=False."""
|
|
tool = KnowledgeSearchTool(store=store)
|
|
result = tool.execute(query="")
|
|
assert result.success is False
|
|
assert "No query provided" in result.content
|
|
|
|
def test_no_store(self):
|
|
"""Missing store returns success=False."""
|
|
tool = KnowledgeSearchTool()
|
|
result = tool.execute(query="kubernetes")
|
|
assert result.success is False
|
|
assert "No knowledge store configured" in result.content
|
|
|
|
def test_spec_has_filter_params(self):
|
|
"""ToolSpec.parameters includes all required and optional filter fields."""
|
|
tool = KnowledgeSearchTool()
|
|
props = tool.spec.parameters.get("properties", {})
|
|
for field in ("query", "source", "doc_type", "author", "since", "top_k"):
|
|
assert field in props, f"Missing parameter: {field}"
|
|
assert "query" in tool.spec.parameters.get("required", [])
|
|
assert tool.spec.category == "knowledge"
|
|
|
|
def test_registry(self):
|
|
"""ToolRegistry contains 'knowledge_search' after module import.
|
|
|
|
The autouse ``_clean_registries`` fixture clears all registries before
|
|
each test. Since the module is already cached in ``sys.modules`` a
|
|
plain import won't re-execute the ``@ToolRegistry.register`` decorator,
|
|
so we explicitly reload the module.
|
|
"""
|
|
mod_name = "openjarvis.tools.knowledge_search"
|
|
if mod_name in sys.modules:
|
|
importlib.reload(sys.modules[mod_name])
|
|
else:
|
|
importlib.import_module(mod_name)
|
|
|
|
assert ToolRegistry.contains("knowledge_search")
|
|
|
|
|
|
def test_tool_uses_two_stage_retriever(tmp_path: Path) -> None:
|
|
"""KnowledgeSearchTool delegates to TwoStageRetriever when supplied."""
|
|
from openjarvis.connectors.retriever import TwoStageRetriever
|
|
|
|
store = KnowledgeStore(db_path=str(tmp_path / "ts_test.db"))
|
|
store.store(
|
|
content="Deep learning research paper", source="gdrive", doc_type="document"
|
|
)
|
|
retriever = TwoStageRetriever(store=store)
|
|
tool = KnowledgeSearchTool(store=store, retriever=retriever)
|
|
result = tool.execute(query="deep learning")
|
|
assert result.success
|
|
assert result.metadata["num_results"] > 0
|