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:
196
tests/unit/test_memory/test_models.py
Normal file
196
tests/unit/test_memory/test_models.py
Normal file
@ -0,0 +1,196 @@
|
||||
"""Unit tests for memory domain models — focused on ``from_algo`` factories.
|
||||
|
||||
The factories carry the load-bearing contract: algo's emitted business
|
||||
fields survive, everos's engineering metadata (session_id / sender_ids
|
||||
/ parent_id) gets injected, and any algo-side ``parent_id`` (smuggled
|
||||
through ``extra='allow'``) is dropped in favour of the caller's value.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from everalgo.types import (
|
||||
AgentCase as AlgoAgentCase,
|
||||
)
|
||||
from everalgo.types import (
|
||||
AtomicFact as AlgoAtomicFact,
|
||||
)
|
||||
from everalgo.types import (
|
||||
Episode as AlgoEpisode,
|
||||
)
|
||||
from everalgo.types import (
|
||||
Foresight as AlgoForesight,
|
||||
)
|
||||
|
||||
from everos.memory.models import AgentCase, AtomicFact, Episode, Foresight
|
||||
|
||||
|
||||
def test_atomic_fact_from_algo_carries_business_fields_and_metadata() -> None:
|
||||
algo = AlgoAtomicFact(
|
||||
owner_id="u_alice",
|
||||
content="alice likes hiking",
|
||||
timestamp=1_700_000_000_000,
|
||||
)
|
||||
fact = AtomicFact.from_algo(
|
||||
algo,
|
||||
owner_id="u_alice",
|
||||
session_id="s_42",
|
||||
parent_id="mc_abc",
|
||||
)
|
||||
assert fact.owner_id == "u_alice"
|
||||
assert fact.fact == "alice likes hiking"
|
||||
assert fact.timestamp == 1_700_000_000_000
|
||||
assert fact.session_id == "s_42"
|
||||
assert fact.parent_id == "mc_abc"
|
||||
assert not hasattr(fact, "sender_ids")
|
||||
|
||||
|
||||
def test_atomic_fact_from_algo_drops_algo_side_parent_id() -> None:
|
||||
# Smuggle a parent_id through extra='allow' on the algo side.
|
||||
algo = AlgoAtomicFact.model_validate(
|
||||
{
|
||||
"owner_id": "u_alice",
|
||||
"content": "x",
|
||||
"timestamp": 1_700_000_000_000,
|
||||
"parent_id": "ALGO_STALE",
|
||||
}
|
||||
)
|
||||
fact = AtomicFact.from_algo(
|
||||
algo, owner_id="u_alice", session_id="s1", parent_id="mc_real"
|
||||
)
|
||||
# Caller-supplied parent_id wins; algo-side value is discarded.
|
||||
assert fact.parent_id == "mc_real"
|
||||
|
||||
|
||||
def test_atomic_fact_from_algo_owner_id_override_for_fan_out() -> None:
|
||||
"""One LLM template fans out to many owners — caller's owner_id wins."""
|
||||
algo = AlgoAtomicFact(
|
||||
owner_id="PLACEHOLDER", # subject-agnostic prompt placeholder
|
||||
content="likes hiking",
|
||||
timestamp=1_700_000_000_000,
|
||||
)
|
||||
fact_alice = AtomicFact.from_algo(
|
||||
algo, owner_id="u_alice", session_id="s1", parent_id="mc_a"
|
||||
)
|
||||
fact_bob = AtomicFact.from_algo(
|
||||
algo, owner_id="u_bob", session_id="s1", parent_id="mc_a"
|
||||
)
|
||||
assert fact_alice.owner_id == "u_alice"
|
||||
assert fact_bob.owner_id == "u_bob"
|
||||
# Same source template body survives the fan-out.
|
||||
assert fact_alice.fact == fact_bob.fact == "likes hiking"
|
||||
|
||||
|
||||
def test_foresight_from_algo_preserves_optional_time_window() -> None:
|
||||
algo = AlgoForesight(
|
||||
owner_id="u_alice",
|
||||
foresight="plans trip to tokyo",
|
||||
evidence="said so",
|
||||
timestamp=1_700_000_000_000,
|
||||
start_time="2026-06-01",
|
||||
duration_days=7,
|
||||
)
|
||||
fs = Foresight.from_algo(algo, session_id="s1", parent_id="mc_a")
|
||||
assert fs.foresight == "plans trip to tokyo"
|
||||
assert fs.evidence == "said so"
|
||||
assert fs.start_time == "2026-06-01"
|
||||
assert fs.duration_days == 7
|
||||
assert fs.end_time is None
|
||||
assert fs.parent_id == "mc_a"
|
||||
assert not hasattr(fs, "sender_ids")
|
||||
|
||||
|
||||
def test_foresight_from_algo_drops_algo_side_parent_id() -> None:
|
||||
algo = AlgoForesight.model_validate(
|
||||
{
|
||||
"owner_id": "u_alice",
|
||||
"foresight": "x",
|
||||
"evidence": "y",
|
||||
"timestamp": 1_700_000_000_000,
|
||||
"parent_id": "ALGO_STALE",
|
||||
}
|
||||
)
|
||||
fs = Foresight.from_algo(algo, session_id="s1", parent_id="mc_real")
|
||||
assert fs.parent_id == "mc_real"
|
||||
|
||||
|
||||
def test_foresight_from_algo_preserves_algo_owner_id() -> None:
|
||||
"""Per-sender extraction: algo emits the correct owner_id."""
|
||||
algo = AlgoForesight(
|
||||
owner_id="u_bob",
|
||||
foresight="trip to tokyo",
|
||||
evidence="said so",
|
||||
timestamp=1_700_000_000_000,
|
||||
)
|
||||
fs = Foresight.from_algo(algo, session_id="s1", parent_id="mc_a")
|
||||
assert fs.owner_id == "u_bob"
|
||||
|
||||
|
||||
def test_agent_case_from_algo_injects_owner_and_drops_algo_id() -> None:
|
||||
"""Algo emits a uuid `id` + no owner; everos injects agent_id, drops uuid."""
|
||||
algo = AlgoAgentCase(
|
||||
id=uuid.uuid4().hex,
|
||||
timestamp=1_700_000_000_000,
|
||||
task_intent="summarise doc",
|
||||
approach="read + condense",
|
||||
quality_score=0.75,
|
||||
key_insight="batch-then-summarise",
|
||||
)
|
||||
case = AgentCase.from_algo(
|
||||
algo, owner_id="agent_42", session_id="s1", parent_id="mc_a"
|
||||
)
|
||||
assert case.owner_id == "agent_42"
|
||||
assert case.task_intent == "summarise doc"
|
||||
assert case.approach == "read + condense"
|
||||
assert case.quality_score == 0.75
|
||||
assert case.key_insight == "batch-then-summarise"
|
||||
assert case.session_id == "s1"
|
||||
assert case.parent_id == "mc_a"
|
||||
# algo's uuid `id` is not surfaced on the domain model.
|
||||
assert not hasattr(case, "id") or case.id != algo.id # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_agent_case_from_algo_normalises_empty_key_insight_to_none() -> None:
|
||||
"""algo emits `""` when there's nothing to insight; domain normalises to None."""
|
||||
algo = AlgoAgentCase(
|
||||
id=uuid.uuid4().hex,
|
||||
timestamp=1_700_000_000_000,
|
||||
task_intent="ti",
|
||||
approach="ap",
|
||||
quality_score=0.5,
|
||||
key_insight="",
|
||||
)
|
||||
case = AgentCase.from_algo(
|
||||
algo, owner_id="agent_42", session_id="s1", parent_id="mc_a"
|
||||
)
|
||||
assert case.key_insight is None
|
||||
|
||||
|
||||
def test_episode_from_algo_owner_id_caller_supplied() -> None:
|
||||
"""Caller supplies ``owner_id``; algo's value (None or otherwise) is dropped.
|
||||
|
||||
The pipeline runs the algo once with ``sender_id=None`` (generic
|
||||
EPISODE_GENERATION_PROMPT) and then fans the same algo Episode out
|
||||
to one domain Episode per user sender, each rooted at its own owner.
|
||||
"""
|
||||
algo = AlgoEpisode(owner_id=None, episode="hello", timestamp=1_700_000_000_000)
|
||||
ep_alice = Episode.from_algo(
|
||||
algo,
|
||||
owner_id="u_alice",
|
||||
session_id="s1",
|
||||
sender_ids=["u_alice", "u_bob"],
|
||||
parent_id="mc_a",
|
||||
)
|
||||
ep_bob = Episode.from_algo(
|
||||
algo,
|
||||
owner_id="u_bob",
|
||||
session_id="s1",
|
||||
sender_ids=["u_alice", "u_bob"],
|
||||
parent_id="mc_a",
|
||||
)
|
||||
assert ep_alice.owner_id == "u_alice"
|
||||
assert ep_bob.owner_id == "u_bob"
|
||||
assert ep_alice.episode == ep_bob.episode == "hello"
|
||||
assert ep_alice.parent_id == ep_bob.parent_id == "mc_a"
|
||||
assert ep_alice.session_id == ep_bob.session_id == "s1"
|
||||
Reference in New Issue
Block a user