chore: initialize EverOS 1.0.0
md-first memory extraction framework for AI agents. Markdown is the single source of truth; SQLite holds state and LanceDB provides the rebuildable vector + BM25 + scalar index. The codebase follows a single-direction DDD layering (entrypoints -> service -> memory -> infra, with component / core / config cross-cutting) enforced by import-linter. Engineering surface: - Coding conventions in .claude/rules/ (path-scoped) and workflows in .claude/skills/ (/commit, /new-branch, /pr). - GitHub Actions CI runs make lint + test + integration; pre-commit mirrors the gates locally (ruff, hygiene hooks, gitlint commit-msg). - Commit messages follow Conventional Commits, enforced by gitlint. - make lint also enforces datetime two-zone discipline and OpenAPI drift.
This commit is contained in:
135
tests/unit/test_memory/test_search/test_dto.py
Normal file
135
tests/unit/test_memory/test_search/test_dto.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""Unit tests for ``memory.search.dto`` validation rules."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from everos.memory.search import (
|
||||
SearchData,
|
||||
SearchMethod,
|
||||
SearchRequest,
|
||||
SearchResponse,
|
||||
)
|
||||
|
||||
|
||||
def _minimal_request_kwargs() -> dict:
|
||||
return {
|
||||
"user_id": "alice",
|
||||
"query": "hello",
|
||||
}
|
||||
|
||||
|
||||
def test_enable_llm_rerank_defaults_to_false() -> None:
|
||||
"""HYBRID should NOT auto-trigger LLM Phase-5 rerank by default.
|
||||
|
||||
The caller opts in explicitly when they want the extra LLM pass;
|
||||
leaving it off keeps a default HYBRID call cheap (no LLM ``chat``).
|
||||
"""
|
||||
req = SearchRequest(**_minimal_request_kwargs())
|
||||
assert req.enable_llm_rerank is False
|
||||
|
||||
|
||||
def test_enable_llm_rerank_accepts_true() -> None:
|
||||
req = SearchRequest(**_minimal_request_kwargs(), enable_llm_rerank=True)
|
||||
assert req.enable_llm_rerank is True
|
||||
|
||||
|
||||
def test_minimal_request_uses_hybrid_default() -> None:
|
||||
req = SearchRequest(**_minimal_request_kwargs())
|
||||
assert req.method == SearchMethod.HYBRID
|
||||
assert req.top_k == -1
|
||||
assert req.include_profile is False
|
||||
assert req.filters is None
|
||||
assert req.radius is None
|
||||
|
||||
|
||||
def test_top_k_zero_rejected() -> None:
|
||||
with pytest.raises(ValidationError) as exc:
|
||||
SearchRequest(**_minimal_request_kwargs(), top_k=0)
|
||||
assert "top_k" in str(exc.value)
|
||||
|
||||
|
||||
def test_top_k_above_100_rejected() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
SearchRequest(**_minimal_request_kwargs(), top_k=101)
|
||||
|
||||
|
||||
def test_top_k_below_minus_one_rejected() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
SearchRequest(**_minimal_request_kwargs(), top_k=-2)
|
||||
|
||||
|
||||
def test_top_k_minus_one_accepted() -> None:
|
||||
req = SearchRequest(**_minimal_request_kwargs(), top_k=-1)
|
||||
assert req.top_k == -1
|
||||
|
||||
|
||||
def test_top_k_in_range_accepted() -> None:
|
||||
req = SearchRequest(**_minimal_request_kwargs(), top_k=50)
|
||||
assert req.top_k == 50
|
||||
|
||||
|
||||
def test_radius_out_of_range_rejected() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
SearchRequest(**_minimal_request_kwargs(), radius=1.5)
|
||||
with pytest.raises(ValidationError):
|
||||
SearchRequest(**_minimal_request_kwargs(), radius=-0.1)
|
||||
|
||||
|
||||
def test_neither_user_id_nor_agent_id_rejected() -> None:
|
||||
"""The xor validator requires exactly one of user_id / agent_id."""
|
||||
with pytest.raises(ValidationError, match="exactly one of"):
|
||||
SearchRequest(query="hello") # neither set
|
||||
|
||||
|
||||
def test_both_user_id_and_agent_id_rejected() -> None:
|
||||
"""The xor validator rejects ambiguous owner identity."""
|
||||
with pytest.raises(ValidationError, match="exactly one of"):
|
||||
SearchRequest(user_id="alice", agent_id="agent_x", query="hello")
|
||||
|
||||
|
||||
def test_empty_query_rejected() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
SearchRequest(user_id="alice", query="")
|
||||
|
||||
|
||||
def test_empty_user_id_rejected() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
SearchRequest(user_id="", query="hello")
|
||||
|
||||
|
||||
def test_extra_top_level_field_rejected() -> None:
|
||||
"""``extra='forbid'`` keeps the contract tight."""
|
||||
with pytest.raises(ValidationError):
|
||||
SearchRequest(
|
||||
**_minimal_request_kwargs(),
|
||||
unexpected_field="x", # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
|
||||
def test_filters_extra_keys_allowed() -> None:
|
||||
"""FilterNode is open-shape; safety is enforced in the compiler."""
|
||||
req = SearchRequest(
|
||||
**_minimal_request_kwargs(),
|
||||
filters={"session_id": "sess_a", "AND": [{"timestamp": {"gte": 1}}]},
|
||||
)
|
||||
assert req.filters is not None
|
||||
dumped = req.filters.model_dump(exclude_none=True)
|
||||
assert dumped["session_id"] == "sess_a"
|
||||
assert dumped["AND"][0]["timestamp"]["gte"] == 1
|
||||
|
||||
|
||||
def test_response_default_arrays_present() -> None:
|
||||
"""Every ``data.*`` array must exist so callers can iterate unconditionally."""
|
||||
resp = SearchResponse(request_id="0" * 32, data=SearchData())
|
||||
assert resp.data.episodes == []
|
||||
assert resp.data.profiles == []
|
||||
assert resp.data.agent_cases == []
|
||||
assert resp.data.agent_skills == []
|
||||
|
||||
|
||||
def test_method_enum_serialises_to_lowercase() -> None:
|
||||
req = SearchRequest(**_minimal_request_kwargs(), method="agentic") # type: ignore[arg-type]
|
||||
assert req.method == SearchMethod.AGENTIC
|
||||
assert req.method.value == "agentic"
|
||||
Reference in New Issue
Block a user