Files
EverOS/tests/unit/test_memory/test_search/test_dto.py
Elliot Chen 518b8eca85 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.
2026-06-06 07:33:17 +08:00

136 lines
4.2 KiB
Python

"""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"