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:
0
tests/unit/test_infra/test_markdown/__init__.py
Normal file
0
tests/unit/test_infra/test_markdown/__init__.py
Normal file
104
tests/unit/test_infra/test_markdown/test_mds/test_agent_skill.py
Normal file
104
tests/unit/test_infra/test_markdown/test_mds/test_agent_skill.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""Tests for :class:`AgentSkillFrontmatter` — the AgentSkill schema.
|
||||
|
||||
Lives under ``test_infra`` because :class:`AgentSkillFrontmatter` itself
|
||||
lives under ``infra/.../mds`` (it carries business fields + the
|
||||
directory-shape ClassVars). The schema-agnostic chassis tests live
|
||||
under ``test_core/test_persistence/test_markdown/``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from everos.infra.persistence.markdown import AgentSkillFrontmatter
|
||||
|
||||
|
||||
def _kwargs(**overrides: object) -> dict[str, object]:
|
||||
"""Minimal valid kwargs for AgentSkillFrontmatter."""
|
||||
base: dict[str, object] = {
|
||||
"id": "skill_contract_risk_scan",
|
||||
"agent_id": "agent_zhang_legal",
|
||||
"name": "contract_risk_scan",
|
||||
"description": "Scan a contract draft for risk clauses.",
|
||||
"confidence": 0.5,
|
||||
"maturity_score": 0.5,
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
def test_skill_inherits_agent_scope() -> None:
|
||||
"""Skills always live under ``agents/`` — track + SCOPE_DIR confirm."""
|
||||
assert AgentSkillFrontmatter.SCOPE_DIR == "agents"
|
||||
fm = AgentSkillFrontmatter(**_kwargs()) # type: ignore[arg-type]
|
||||
assert fm.track == "agent"
|
||||
assert fm.type == "agent_skill"
|
||||
|
||||
|
||||
def test_skill_requires_name_and_description() -> None:
|
||||
"""Tier-1 prompt injection demands both fields — schema enforces."""
|
||||
bad = _kwargs()
|
||||
del bad["name"]
|
||||
with pytest.raises(ValidationError):
|
||||
AgentSkillFrontmatter(**bad) # type: ignore[arg-type]
|
||||
|
||||
bad = _kwargs()
|
||||
del bad["description"]
|
||||
with pytest.raises(ValidationError):
|
||||
AgentSkillFrontmatter(**bad) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_skill_requires_confidence_and_maturity_score() -> None:
|
||||
"""LLM-emitted score fields are required (no default)."""
|
||||
bad = _kwargs()
|
||||
del bad["confidence"]
|
||||
with pytest.raises(ValidationError):
|
||||
AgentSkillFrontmatter(**bad) # type: ignore[arg-type]
|
||||
|
||||
bad = _kwargs()
|
||||
del bad["maturity_score"]
|
||||
with pytest.raises(ValidationError):
|
||||
AgentSkillFrontmatter(**bad) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_skill_optional_fields_default() -> None:
|
||||
"""``source_case_ids`` defaults to empty list; ``cluster_id`` to None."""
|
||||
fm = AgentSkillFrontmatter(**_kwargs()) # type: ignore[arg-type]
|
||||
assert fm.source_case_ids == []
|
||||
assert fm.cluster_id is None
|
||||
|
||||
|
||||
def test_skill_lineage_fields_round_trip() -> None:
|
||||
"""``source_case_ids`` + ``cluster_id`` round-trip through model_dump."""
|
||||
fm = AgentSkillFrontmatter(
|
||||
**_kwargs(
|
||||
source_case_ids=["case_a", "case_b"],
|
||||
cluster_id="cl_x",
|
||||
), # type: ignore[arg-type]
|
||||
)
|
||||
dumped = fm.model_dump()
|
||||
assert dumped["source_case_ids"] == ["case_a", "case_b"]
|
||||
assert dumped["cluster_id"] == "cl_x"
|
||||
|
||||
|
||||
def test_skill_extra_fields_still_allowed() -> None:
|
||||
"""L2 system metadata (md_sha256 / last_indexed_at) rides along."""
|
||||
fm = AgentSkillFrontmatter(
|
||||
**_kwargs(
|
||||
md_sha256="deadbeef",
|
||||
last_indexed_at="2026-05-07T08:00:00Z",
|
||||
), # type: ignore[arg-type]
|
||||
)
|
||||
dumped = fm.model_dump()
|
||||
assert dumped["md_sha256"] == "deadbeef"
|
||||
assert dumped["last_indexed_at"] == "2026-05-07T08:00:00Z"
|
||||
|
||||
|
||||
def test_skill_directory_shape_classvars() -> None:
|
||||
"""Path-shape ClassVars pin the wiki layout for the writer/reader pair."""
|
||||
assert AgentSkillFrontmatter.SKILLS_CONTAINER_NAME == "skills"
|
||||
assert AgentSkillFrontmatter.SKILL_DIR_PREFIX == "skill_"
|
||||
assert AgentSkillFrontmatter.SKILL_MAIN_FILENAME == "SKILL.md"
|
||||
assert AgentSkillFrontmatter.SKILL_REFERENCES_DIR_NAME == "references"
|
||||
assert AgentSkillFrontmatter.SKILL_SCRIPTS_DIR_NAME == "scripts"
|
||||
@ -0,0 +1,30 @@
|
||||
"""Tests that every business frontmatter class reports the expected
|
||||
``path_glob()`` — the cascade scanner reads these to enumerate eligible
|
||||
files, so a wrong glob silently drops a whole kind from cascade.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from everos.infra.persistence.markdown import (
|
||||
AgentCaseDailyFrontmatter,
|
||||
AgentSkillFrontmatter,
|
||||
AtomicFactDailyFrontmatter,
|
||||
EpisodeDailyFrontmatter,
|
||||
ForesightDailyFrontmatter,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "expected"),
|
||||
[
|
||||
(EpisodeDailyFrontmatter, "*/*/users/*/episodes/episode-*.md"),
|
||||
(AtomicFactDailyFrontmatter, "*/*/users/*/.atomic_facts/atomic_fact-*.md"),
|
||||
(ForesightDailyFrontmatter, "*/*/users/*/.foresights/foresight-*.md"),
|
||||
(AgentCaseDailyFrontmatter, "*/*/agents/*/.cases/agent_case-*.md"),
|
||||
(AgentSkillFrontmatter, "*/*/agents/*/skills/skill_*/SKILL.md"),
|
||||
],
|
||||
)
|
||||
def test_path_glob(schema: type, expected: str) -> None:
|
||||
assert schema.path_glob() == expected
|
||||
71
tests/unit/test_infra/test_markdown/test_mds/test_profile.py
Normal file
71
tests/unit/test_infra/test_markdown/test_mds/test_profile.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""Tests for the profile frontmatter duck-typed shape.
|
||||
|
||||
Profile schemas have no shared base class — they only need a
|
||||
``PROFILE_FILENAME`` ClassVar plus inheritance from a scope mixin. This
|
||||
test exercises that contract via a local fixture class.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from everos.core.persistence.markdown import UserScopedFrontmatter
|
||||
|
||||
|
||||
class _SampleUserProfileFM(UserScopedFrontmatter):
|
||||
"""Local fixture: a user-track profile schema."""
|
||||
|
||||
PROFILE_FILENAME: ClassVar[str] = "user.md"
|
||||
|
||||
type: Literal["sample_user_profile"] = "sample_user_profile"
|
||||
display_name: str
|
||||
bio: str
|
||||
interests: list[str] = []
|
||||
|
||||
|
||||
def test_schema_inherits_user_scope() -> None:
|
||||
fm = _SampleUserProfileFM(
|
||||
id="sample_user_profile_u_jason",
|
||||
type="sample_user_profile",
|
||||
user_id="u_jason",
|
||||
display_name="Jason",
|
||||
bio="hiker.",
|
||||
)
|
||||
assert fm.track == "user"
|
||||
assert fm.SCOPE_DIR == "users"
|
||||
|
||||
|
||||
def test_profile_filename_classvar() -> None:
|
||||
"""Path-shape ClassVar is duck-typed onto the schema directly."""
|
||||
assert _SampleUserProfileFM.PROFILE_FILENAME == "user.md"
|
||||
|
||||
|
||||
def test_requires_display_name_and_bio() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
_SampleUserProfileFM( # type: ignore[call-arg]
|
||||
id="x",
|
||||
type="sample_user_profile",
|
||||
user_id="u_jason",
|
||||
bio="missing display_name",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
_SampleUserProfileFM( # type: ignore[call-arg]
|
||||
id="x",
|
||||
type="sample_user_profile",
|
||||
user_id="u_jason",
|
||||
display_name="missing bio",
|
||||
)
|
||||
|
||||
|
||||
def test_interests_default_empty() -> None:
|
||||
fm = _SampleUserProfileFM(
|
||||
id="x",
|
||||
type="sample_user_profile",
|
||||
user_id="u_jason",
|
||||
display_name="Jason",
|
||||
bio="hiker.",
|
||||
)
|
||||
assert fm.interests == []
|
||||
@ -0,0 +1,129 @@
|
||||
"""Tests for :class:`AgentSkillReader` — typed read for the skill directory layout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from everos.core.persistence import MemoryRoot
|
||||
from everos.infra.persistence.markdown import (
|
||||
AgentSkillFrontmatter,
|
||||
AgentSkillReader,
|
||||
AgentSkillWriter,
|
||||
)
|
||||
|
||||
|
||||
def _make_fm(**overrides: object) -> AgentSkillFrontmatter:
|
||||
base: dict[str, object] = {
|
||||
"id": "agent_x_skill_alpha",
|
||||
"agent_id": "agent_x",
|
||||
"name": "alpha",
|
||||
"description": "A test skill.",
|
||||
"confidence": 0.5,
|
||||
"maturity_score": 0.5,
|
||||
}
|
||||
base.update(overrides)
|
||||
return AgentSkillFrontmatter(**base) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root(tmp_path: Path) -> MemoryRoot:
|
||||
return MemoryRoot(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def writer(root: MemoryRoot) -> AgentSkillWriter:
|
||||
return AgentSkillWriter(root)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reader(root: MemoryRoot) -> AgentSkillReader:
|
||||
return AgentSkillReader(root)
|
||||
|
||||
|
||||
async def test_read_main_returns_typed_frontmatter_and_body(
|
||||
writer: AgentSkillWriter, reader: AgentSkillReader
|
||||
) -> None:
|
||||
fm_in = _make_fm(
|
||||
description="Contract risk scan.",
|
||||
confidence=0.88,
|
||||
maturity_score=0.82,
|
||||
source_case_ids=["case_a", "case_b"],
|
||||
)
|
||||
await writer.write_main("agent_x", "alpha", frontmatter=fm_in, body="The body.")
|
||||
|
||||
out = await reader.read_main("agent_x", "alpha", schema=AgentSkillFrontmatter)
|
||||
assert out is not None
|
||||
fm_out, body = out
|
||||
assert isinstance(fm_out, AgentSkillFrontmatter)
|
||||
assert fm_out.name == "alpha"
|
||||
assert fm_out.source_case_ids == ["case_a", "case_b"]
|
||||
assert fm_out.confidence == 0.88
|
||||
assert fm_out.maturity_score == 0.82
|
||||
assert body == "The body."
|
||||
|
||||
|
||||
async def test_read_main_returns_none_when_missing(reader: AgentSkillReader) -> None:
|
||||
assert (
|
||||
await reader.read_main("agent_x", "ghost", schema=AgentSkillFrontmatter) is None
|
||||
)
|
||||
|
||||
|
||||
async def test_read_main_round_trip_through_extra_fields(
|
||||
writer: AgentSkillWriter, reader: AgentSkillReader
|
||||
) -> None:
|
||||
"""L2 / L4 ride-along fields survive a write+read cycle (extra="allow")."""
|
||||
fm_in = _make_fm(md_sha256="abc", custom_label="ride-along")
|
||||
await writer.write_main("agent_x", "alpha", frontmatter=fm_in, body="b")
|
||||
out = await reader.read_main("agent_x", "alpha", schema=AgentSkillFrontmatter)
|
||||
assert out is not None
|
||||
fm_out, _ = out
|
||||
dumped = fm_out.model_dump()
|
||||
assert dumped["md_sha256"] == "abc"
|
||||
assert dumped["custom_label"] == "ride-along"
|
||||
|
||||
|
||||
async def test_read_main_validates_against_supplied_schema(
|
||||
writer: AgentSkillWriter, reader: AgentSkillReader
|
||||
) -> None:
|
||||
"""A stricter schema rejects loose existing data — proves typed parsing."""
|
||||
|
||||
class _StricterSkillFM(AgentSkillFrontmatter):
|
||||
# Required field with no default — written file lacks it.
|
||||
priority: int
|
||||
|
||||
fm_in = _make_fm()
|
||||
await writer.write_main("agent_x", "alpha", frontmatter=fm_in, body="b")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
await reader.read_main("agent_x", "alpha", schema=_StricterSkillFM)
|
||||
|
||||
|
||||
async def test_read_reference_round_trip(
|
||||
writer: AgentSkillWriter, reader: AgentSkillReader
|
||||
) -> None:
|
||||
await writer.write_reference(
|
||||
"agent_x", "alpha", "termination", "## term clauses\n..."
|
||||
)
|
||||
content = await reader.read_reference("agent_x", "alpha", "termination")
|
||||
assert content == "## term clauses\n..."
|
||||
|
||||
|
||||
async def test_read_reference_returns_none_when_missing(
|
||||
reader: AgentSkillReader,
|
||||
) -> None:
|
||||
assert await reader.read_reference("agent_x", "alpha", "ghost") is None
|
||||
|
||||
|
||||
async def test_read_script_round_trip(
|
||||
writer: AgentSkillWriter, reader: AgentSkillReader
|
||||
) -> None:
|
||||
await writer.write_script("agent_x", "alpha", "redline.py", "print('hi')\n")
|
||||
content = await reader.read_script("agent_x", "alpha", "redline.py")
|
||||
assert content == "print('hi')"
|
||||
|
||||
|
||||
async def test_read_script_returns_none_when_missing(reader: AgentSkillReader) -> None:
|
||||
assert await reader.read_script("agent_x", "alpha", "ghost.py") is None
|
||||
182
tests/unit/test_infra/test_markdown/test_readers/test_base.py
Normal file
182
tests/unit/test_infra/test_markdown/test_readers/test_base.py
Normal file
@ -0,0 +1,182 @@
|
||||
"""Tests for ``BaseDailyReader`` chassis.
|
||||
|
||||
Symmetric to ``test_writers/test_base.py`` — exercises path resolution
|
||||
+ entry locating + structured-entry upgrading on a dummy schema.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
import pytest
|
||||
|
||||
from everos.core.persistence import (
|
||||
EntryId,
|
||||
MemoryRoot,
|
||||
StructuredEntry,
|
||||
UserScopedFrontmatter,
|
||||
render_structured_entry,
|
||||
)
|
||||
from everos.infra.persistence.markdown.readers import BaseDailyReader
|
||||
from everos.infra.persistence.markdown.writers import BaseDailyWriter
|
||||
|
||||
|
||||
class _DemoFrontmatter(UserScopedFrontmatter):
|
||||
ENTRY_ID_PREFIX: ClassVar[str] = "demo"
|
||||
DIR_NAME: ClassVar[str] = "demos"
|
||||
FILE_PREFIX: ClassVar[str] = "demo"
|
||||
type: Literal["user_demo"] = "user_demo"
|
||||
|
||||
|
||||
class _DemoWriter(BaseDailyWriter):
|
||||
schema = _DemoFrontmatter
|
||||
|
||||
|
||||
class _DemoReader(BaseDailyReader):
|
||||
schema = _DemoFrontmatter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root(tmp_path: Path) -> MemoryRoot:
|
||||
return MemoryRoot(tmp_path)
|
||||
|
||||
|
||||
# ── construction ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_reader_rejects_missing_schema(root: MemoryRoot) -> None:
|
||||
class _NoSchemaReader(BaseDailyReader):
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError, match="schema"):
|
||||
_NoSchemaReader(root)
|
||||
|
||||
|
||||
def test_reader_rejects_schema_missing_classvars(root: MemoryRoot) -> None:
|
||||
class _IncompleteFrontmatter(UserScopedFrontmatter):
|
||||
# Missing DIR_NAME / FILE_PREFIX.
|
||||
type: Literal["incomplete"] = "incomplete"
|
||||
|
||||
class _IncompleteReader(BaseDailyReader):
|
||||
schema = _IncompleteFrontmatter
|
||||
|
||||
with pytest.raises(TypeError, match="missing ClassVar"):
|
||||
_IncompleteReader(root)
|
||||
|
||||
|
||||
# ── read_for ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_read_for_returns_none_when_file_missing(root: MemoryRoot) -> None:
|
||||
reader = _DemoReader(root)
|
||||
assert await reader.read_for("u_jason", dt.date(2026, 4, 22)) is None
|
||||
|
||||
|
||||
async def test_read_for_returns_parsed_when_file_exists(
|
||||
tmp_path: Path, root: MemoryRoot
|
||||
) -> None:
|
||||
writer = _DemoWriter(root)
|
||||
await writer.append("u_jason", "first body", date=dt.date(2026, 4, 22))
|
||||
|
||||
reader = _DemoReader(root)
|
||||
parsed = await reader.read_for("u_jason", dt.date(2026, 4, 22))
|
||||
assert parsed is not None
|
||||
assert len(parsed.entries) == 1
|
||||
assert parsed.entries[0].body == "first body"
|
||||
|
||||
|
||||
async def test_read_for_today_default(root: MemoryRoot) -> None:
|
||||
"""Omitting ``date`` falls back to today_with_timezone()."""
|
||||
writer = _DemoWriter(root)
|
||||
await writer.append("u_jason", "today body")
|
||||
|
||||
reader = _DemoReader(root)
|
||||
parsed = await reader.read_for("u_jason")
|
||||
assert parsed is not None
|
||||
assert parsed.entries[0].body == "today body"
|
||||
|
||||
|
||||
# ── find_entry ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_find_entry_resolves_file_from_entry_id(root: MemoryRoot) -> None:
|
||||
writer = _DemoWriter(root)
|
||||
await writer.append("u_jason", "alpha", date=dt.date(2026, 4, 22))
|
||||
await writer.append("u_jason", "beta", date=dt.date(2026, 4, 22))
|
||||
|
||||
reader = _DemoReader(root)
|
||||
e = await reader.find_entry("u_jason", "demo_20260422_00000002")
|
||||
assert e is not None
|
||||
assert e.id == "demo_20260422_00000002"
|
||||
assert e.body == "beta"
|
||||
|
||||
|
||||
async def test_find_entry_returns_none_when_file_missing(root: MemoryRoot) -> None:
|
||||
reader = _DemoReader(root)
|
||||
assert await reader.find_entry("u_jason", "demo_20260422_00000001") is None
|
||||
|
||||
|
||||
async def test_find_entry_returns_none_when_entry_missing(root: MemoryRoot) -> None:
|
||||
writer = _DemoWriter(root)
|
||||
await writer.append("u_jason", "only", date=dt.date(2026, 4, 22))
|
||||
|
||||
reader = _DemoReader(root)
|
||||
assert await reader.find_entry("u_jason", "demo_20260422_00000099") is None
|
||||
|
||||
|
||||
async def test_find_entry_accepts_entryid_object(root: MemoryRoot) -> None:
|
||||
writer = _DemoWriter(root)
|
||||
await writer.append("u_jason", "alpha", date=dt.date(2026, 4, 22))
|
||||
|
||||
reader = _DemoReader(root)
|
||||
eid = EntryId(prefix="demo", date=dt.date(2026, 4, 22), seq=1)
|
||||
e = await reader.find_entry("u_jason", eid)
|
||||
assert e is not None
|
||||
assert e.body == "alpha"
|
||||
|
||||
|
||||
# ── find_structured ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_find_structured_parses_audit_form(root: MemoryRoot) -> None:
|
||||
writer = _DemoWriter(root)
|
||||
body = render_structured_entry(
|
||||
header="demo_20260422_00000001",
|
||||
inline={"type": "demo", "user_id": "u_jason"},
|
||||
sections={"Body": "the body"},
|
||||
)
|
||||
await writer.append("u_jason", body, date=dt.date(2026, 4, 22))
|
||||
|
||||
reader = _DemoReader(root)
|
||||
structured = await reader.find_structured("u_jason", "demo_20260422_00000001")
|
||||
assert structured is not None
|
||||
assert isinstance(structured, StructuredEntry)
|
||||
assert structured.id == "demo_20260422_00000001"
|
||||
assert structured.header == "demo_20260422_00000001"
|
||||
assert structured.inline == {"type": "demo", "user_id": "u_jason"}
|
||||
assert structured.sections == {"Body": "the body"}
|
||||
|
||||
|
||||
async def test_find_structured_returns_none_when_missing(root: MemoryRoot) -> None:
|
||||
reader = _DemoReader(root)
|
||||
assert await reader.find_structured("u_jason", "demo_20260422_00000001") is None
|
||||
|
||||
|
||||
# ── path_for ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_path_for_matches_writer(tmp_path: Path, root: MemoryRoot) -> None:
|
||||
"""Reader and writer resolve to the same path for the same schema."""
|
||||
reader = _DemoReader(root)
|
||||
writer = _DemoWriter(root)
|
||||
d = dt.date(2026, 4, 22)
|
||||
assert reader.path_for("u_jason", d) == writer.path_for("u_jason", d)
|
||||
|
||||
|
||||
def test_path_for_does_not_create_files(tmp_path: Path, root: MemoryRoot) -> None:
|
||||
reader = _DemoReader(root)
|
||||
p = reader.path_for("u_jason", dt.date(2026, 4, 22))
|
||||
assert not p.exists()
|
||||
assert not (tmp_path / "users").exists()
|
||||
@ -0,0 +1,121 @@
|
||||
"""Tests for :class:`ProfileReader` — typed read for profile files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from everos.core.persistence import MemoryRoot, UserScopedFrontmatter
|
||||
from everos.infra.persistence.markdown.readers import ProfileReader
|
||||
from everos.infra.persistence.markdown.writers import ProfileWriter
|
||||
|
||||
|
||||
class _UserProfileFM(UserScopedFrontmatter):
|
||||
PROFILE_FILENAME: ClassVar[str] = "user.md"
|
||||
type: Literal["demo_user_profile"] = "demo_user_profile"
|
||||
display_name: str = ""
|
||||
bio: str = ""
|
||||
interests: list[str] = []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root(tmp_path: Path) -> MemoryRoot:
|
||||
return MemoryRoot(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def writer(root: MemoryRoot) -> ProfileWriter:
|
||||
return ProfileWriter(root)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reader(root: MemoryRoot) -> ProfileReader:
|
||||
return ProfileReader(root)
|
||||
|
||||
|
||||
async def test_read_returns_typed_frontmatter_and_body(
|
||||
writer: ProfileWriter, reader: ProfileReader
|
||||
) -> None:
|
||||
fm_in = _UserProfileFM(
|
||||
id="demo_user_profile_u_jason",
|
||||
type="demo_user_profile",
|
||||
user_id="u_jason",
|
||||
display_name="Jason",
|
||||
bio="weekend hiker.",
|
||||
interests=["hiking", "coffee"],
|
||||
)
|
||||
await writer.write("u_jason", frontmatter=fm_in, body="The body.")
|
||||
|
||||
out = await reader.read("u_jason", schema=_UserProfileFM)
|
||||
assert out is not None
|
||||
fm_out, body = out
|
||||
assert isinstance(fm_out, _UserProfileFM)
|
||||
assert fm_out.display_name == "Jason"
|
||||
assert fm_out.interests == ["hiking", "coffee"]
|
||||
assert body == "The body."
|
||||
|
||||
|
||||
async def test_read_returns_none_when_missing(reader: ProfileReader) -> None:
|
||||
assert await reader.read("u_ghost", schema=_UserProfileFM) is None
|
||||
|
||||
|
||||
async def test_read_round_trip_through_extra_fields(
|
||||
writer: ProfileWriter, reader: ProfileReader
|
||||
) -> None:
|
||||
"""L2 / L4 ride-along fields survive a write+read cycle."""
|
||||
fm_in = _UserProfileFM(
|
||||
id="demo_user_profile_u_jason",
|
||||
type="demo_user_profile",
|
||||
user_id="u_jason",
|
||||
md_sha256="abc", # extra
|
||||
custom_label="ride-along", # extra
|
||||
)
|
||||
await writer.write("u_jason", frontmatter=fm_in, body="b")
|
||||
out = await reader.read("u_jason", schema=_UserProfileFM)
|
||||
assert out is not None
|
||||
fm_out, _ = out
|
||||
dumped = fm_out.model_dump()
|
||||
assert dumped["md_sha256"] == "abc"
|
||||
assert dumped["custom_label"] == "ride-along"
|
||||
|
||||
|
||||
async def test_read_validates_against_supplied_schema(
|
||||
writer: ProfileWriter, reader: ProfileReader
|
||||
) -> None:
|
||||
"""A stricter schema rejects loose existing data — proves typed parsing."""
|
||||
|
||||
class _StricterFM(UserScopedFrontmatter):
|
||||
PROFILE_FILENAME: ClassVar[str] = "user.md"
|
||||
type: Literal["demo_user_profile"] = "demo_user_profile"
|
||||
# Required field with no default — written file lacks it.
|
||||
priority: int
|
||||
|
||||
fm_in = _UserProfileFM(
|
||||
id="demo_user_profile_u_jason",
|
||||
type="demo_user_profile",
|
||||
user_id="u_jason",
|
||||
)
|
||||
await writer.write("u_jason", frontmatter=fm_in, body="b")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
await reader.read("u_jason", schema=_StricterFM)
|
||||
|
||||
|
||||
def test_path_for_matches_writer(
|
||||
tmp_path: Path,
|
||||
writer: ProfileWriter,
|
||||
reader: ProfileReader,
|
||||
) -> None:
|
||||
"""Reader and writer resolve to the same path for the same schema."""
|
||||
assert reader.path_for("u_jason", schema=_UserProfileFM) == writer.path_for(
|
||||
"u_jason", schema=_UserProfileFM
|
||||
)
|
||||
|
||||
|
||||
def test_path_for_does_not_create_files(tmp_path: Path, reader: ProfileReader) -> None:
|
||||
p = reader.path_for("u_jason", schema=_UserProfileFM)
|
||||
assert not p.exists()
|
||||
assert not (tmp_path / "users").exists()
|
||||
@ -0,0 +1,147 @@
|
||||
"""Tests for :class:`AgentSkillWriter` — directory + progressive disclosure."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from everos.core.persistence import MarkdownReader, MemoryRoot
|
||||
from everos.infra.persistence.markdown import (
|
||||
AgentSkillFrontmatter,
|
||||
AgentSkillWriter,
|
||||
)
|
||||
|
||||
|
||||
def _make_fm(**overrides: object) -> AgentSkillFrontmatter:
|
||||
"""Build an AgentSkillFrontmatter with sensible defaults for tests."""
|
||||
base: dict[str, object] = {
|
||||
"id": "agent_x_skill_alpha",
|
||||
"agent_id": "agent_x",
|
||||
"name": "alpha",
|
||||
"description": "A test skill.",
|
||||
"confidence": 0.5,
|
||||
"maturity_score": 0.5,
|
||||
}
|
||||
base.update(overrides)
|
||||
return AgentSkillFrontmatter(**base) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root(tmp_path: Path) -> MemoryRoot:
|
||||
return MemoryRoot(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def writer(root: MemoryRoot) -> AgentSkillWriter:
|
||||
return AgentSkillWriter(root)
|
||||
|
||||
|
||||
async def test_write_main_creates_directory_layout(
|
||||
root: MemoryRoot, writer: AgentSkillWriter
|
||||
) -> None:
|
||||
fm = _make_fm()
|
||||
path = await writer.write_main(
|
||||
"agent_x", "alpha", frontmatter=fm, body="Step 1: do thing."
|
||||
)
|
||||
expected = root.agents_dir() / "agent_x" / "skills" / "skill_alpha" / "SKILL.md"
|
||||
assert path == expected
|
||||
assert expected.is_file()
|
||||
|
||||
|
||||
async def test_write_main_writes_frontmatter_and_body(
|
||||
root: MemoryRoot, writer: AgentSkillWriter
|
||||
) -> None:
|
||||
fm = _make_fm(
|
||||
description="Contract risk scan.",
|
||||
confidence=0.88,
|
||||
maturity_score=0.82,
|
||||
source_case_ids=["case_a", "case_b"],
|
||||
cluster_id="cl_x",
|
||||
)
|
||||
await writer.write_main("agent_x", "alpha", frontmatter=fm, body="The body.")
|
||||
parsed = await MarkdownReader.read(
|
||||
root.agents_dir() / "agent_x" / "skills" / "skill_alpha" / "SKILL.md"
|
||||
)
|
||||
assert parsed.frontmatter["name"] == "alpha"
|
||||
assert parsed.frontmatter["description"] == "Contract risk scan."
|
||||
assert parsed.frontmatter["confidence"] == 0.88
|
||||
assert parsed.frontmatter["maturity_score"] == 0.82
|
||||
assert parsed.frontmatter["source_case_ids"] == ["case_a", "case_b"]
|
||||
assert parsed.frontmatter["cluster_id"] == "cl_x"
|
||||
assert parsed.body.rstrip("\n") == "The body."
|
||||
|
||||
|
||||
async def test_write_main_is_upsert_full_replace(
|
||||
root: MemoryRoot, writer: AgentSkillWriter
|
||||
) -> None:
|
||||
"""Second call overwrites both frontmatter and body — no append."""
|
||||
fm1 = _make_fm(description="v1", maturity_score=0.4)
|
||||
await writer.write_main("agent_x", "alpha", frontmatter=fm1, body="body v1")
|
||||
|
||||
fm2 = _make_fm(description="v2", maturity_score=0.7)
|
||||
await writer.write_main("agent_x", "alpha", frontmatter=fm2, body="body v2")
|
||||
|
||||
parsed = await MarkdownReader.read(
|
||||
root.agents_dir() / "agent_x" / "skills" / "skill_alpha" / "SKILL.md"
|
||||
)
|
||||
assert parsed.frontmatter["description"] == "v2"
|
||||
assert parsed.frontmatter["maturity_score"] == 0.7
|
||||
assert parsed.body.rstrip("\n") == "body v2"
|
||||
# No "body v1" residue from the previous version.
|
||||
assert "body v1" not in parsed.body
|
||||
|
||||
|
||||
async def test_write_reference_uses_md_extension(
|
||||
root: MemoryRoot, writer: AgentSkillWriter
|
||||
) -> None:
|
||||
path = await writer.write_reference(
|
||||
"agent_x", "alpha", "termination_clauses", "## Termination\n..."
|
||||
)
|
||||
expected = (
|
||||
root.agents_dir()
|
||||
/ "agent_x"
|
||||
/ "skills"
|
||||
/ "skill_alpha"
|
||||
/ "references"
|
||||
/ "termination_clauses.md"
|
||||
)
|
||||
assert path == expected
|
||||
assert path.read_text(encoding="utf-8").startswith("## Termination")
|
||||
|
||||
|
||||
async def test_write_script_keeps_full_filename(
|
||||
root: MemoryRoot, writer: AgentSkillWriter
|
||||
) -> None:
|
||||
path = await writer.write_script("agent_x", "alpha", "redline.py", "print('hi')\n")
|
||||
expected = (
|
||||
root.agents_dir()
|
||||
/ "agent_x"
|
||||
/ "skills"
|
||||
/ "skill_alpha"
|
||||
/ "scripts"
|
||||
/ "redline.py"
|
||||
)
|
||||
assert path == expected
|
||||
assert path.read_text(encoding="utf-8") == "print('hi')\n"
|
||||
|
||||
|
||||
def test_main_path_does_not_create_anything(
|
||||
root: MemoryRoot, writer: AgentSkillWriter
|
||||
) -> None:
|
||||
"""``main_path`` is a pure path resolver — no IO."""
|
||||
p = writer.main_path("agent_x", "alpha")
|
||||
assert p.name == "SKILL.md"
|
||||
assert not root.agents_dir().exists()
|
||||
|
||||
|
||||
async def test_write_main_normalises_trailing_newline(
|
||||
root: MemoryRoot, writer: AgentSkillWriter
|
||||
) -> None:
|
||||
"""Body without a trailing newline still ends in exactly one newline."""
|
||||
fm = _make_fm()
|
||||
await writer.write_main("agent_x", "alpha", frontmatter=fm, body="no-newline-end")
|
||||
text = (
|
||||
root.agents_dir() / "agent_x" / "skills" / "skill_alpha" / "SKILL.md"
|
||||
).read_text(encoding="utf-8")
|
||||
assert text.endswith("no-newline-end\n")
|
||||
182
tests/unit/test_infra/test_markdown/test_writers/test_base.py
Normal file
182
tests/unit/test_infra/test_markdown/test_writers/test_base.py
Normal file
@ -0,0 +1,182 @@
|
||||
"""Tests for ``BaseDailyWriter`` skeleton.
|
||||
|
||||
Uses a dummy ``UserScopedFrontmatter`` subclass to exercise the path
|
||||
resolution + entry-id construction + today-by-default logic without
|
||||
pulling in any concrete business schema.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
import pytest
|
||||
|
||||
from everos.component.utils.datetime import today_with_timezone
|
||||
from everos.core.persistence import (
|
||||
AgentScopedFrontmatter,
|
||||
MarkdownReader,
|
||||
MemoryRoot,
|
||||
UserScopedFrontmatter,
|
||||
)
|
||||
from everos.infra.persistence.markdown.writers import BaseDailyWriter
|
||||
|
||||
|
||||
class _UserDemoFrontmatter(UserScopedFrontmatter):
|
||||
ENTRY_ID_PREFIX: ClassVar[str] = "demo"
|
||||
DIR_NAME: ClassVar[str] = "demos"
|
||||
FILE_PREFIX: ClassVar[str] = "demo"
|
||||
type: Literal["user_demo"] = "user_demo"
|
||||
|
||||
|
||||
class _AgentDemoFrontmatter(AgentScopedFrontmatter):
|
||||
ENTRY_ID_PREFIX: ClassVar[str] = "ademo"
|
||||
DIR_NAME: ClassVar[str] = "demos"
|
||||
FILE_PREFIX: ClassVar[str] = "demo"
|
||||
type: Literal["agent_demo"] = "agent_demo"
|
||||
|
||||
|
||||
class _UserDemoWriter(BaseDailyWriter):
|
||||
schema = _UserDemoFrontmatter
|
||||
|
||||
|
||||
class _AgentDemoWriter(BaseDailyWriter):
|
||||
schema = _AgentDemoFrontmatter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root(tmp_path: Path) -> MemoryRoot:
|
||||
return MemoryRoot(tmp_path)
|
||||
|
||||
|
||||
def test_constructor_rejects_missing_schema(root: MemoryRoot) -> None:
|
||||
class _NoSchema(BaseDailyWriter):
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError, match="schema"):
|
||||
_NoSchema(root)
|
||||
|
||||
|
||||
def test_constructor_rejects_schema_missing_classvars(root: MemoryRoot) -> None:
|
||||
class _IncompleteFrontmatter(UserScopedFrontmatter):
|
||||
# Missing ENTRY_ID_PREFIX / DIR_NAME / FILE_PREFIX.
|
||||
type: Literal["incomplete"] = "incomplete"
|
||||
|
||||
class _IncompleteWriter(BaseDailyWriter):
|
||||
schema = _IncompleteFrontmatter
|
||||
|
||||
with pytest.raises(TypeError, match="ENTRY_ID_PREFIX"):
|
||||
_IncompleteWriter(root)
|
||||
|
||||
|
||||
async def test_append_writes_to_user_track(root: MemoryRoot) -> None:
|
||||
writer = _UserDemoWriter(root)
|
||||
eid = await writer.append("u_jason", "first", date=dt.date(2026, 4, 22))
|
||||
assert eid.prefix == "demo"
|
||||
assert eid.date == dt.date(2026, 4, 22)
|
||||
assert eid.seq == 1
|
||||
expected = root.users_dir() / "u_jason" / "demos" / "demo-2026-04-22.md"
|
||||
assert expected.exists()
|
||||
parsed = await MarkdownReader.read(expected)
|
||||
assert parsed.entries[0].id == "demo_20260422_00000001"
|
||||
assert parsed.entries[0].body == "first"
|
||||
|
||||
|
||||
async def test_append_writes_to_agent_track(root: MemoryRoot) -> None:
|
||||
writer = _AgentDemoWriter(root)
|
||||
eid = await writer.append("agent_zhangsan", "trace", date=dt.date(2026, 4, 22))
|
||||
assert eid.prefix == "ademo"
|
||||
expected = root.agents_dir() / "agent_zhangsan" / "demos" / "demo-2026-04-22.md"
|
||||
assert expected.exists()
|
||||
|
||||
|
||||
async def test_append_increments_seq_across_calls(root: MemoryRoot) -> None:
|
||||
writer = _UserDemoWriter(root)
|
||||
eids = [
|
||||
await writer.append("u_jason", f"body {i}", date=dt.date(2026, 4, 22))
|
||||
for i in range(3)
|
||||
]
|
||||
assert [e.seq for e in eids] == [1, 2, 3]
|
||||
|
||||
|
||||
async def test_append_date_defaults_to_today(root: MemoryRoot) -> None:
|
||||
"""Omitting ``date`` falls back to today_with_timezone()."""
|
||||
writer = _UserDemoWriter(root)
|
||||
eid = await writer.append("u_jason", "body")
|
||||
today = today_with_timezone()
|
||||
assert eid.date == today
|
||||
expected = root.users_dir() / "u_jason" / "demos" / f"demo-{today.isoformat()}.md"
|
||||
assert expected.exists()
|
||||
|
||||
|
||||
async def test_append_passes_frontmatter_updates(root: MemoryRoot) -> None:
|
||||
writer = _UserDemoWriter(root)
|
||||
await writer.append(
|
||||
"u_jason",
|
||||
"body",
|
||||
date=dt.date(2026, 4, 22),
|
||||
frontmatter_updates={"file_type": "user_demo_daily", "entry_count": 1},
|
||||
)
|
||||
path = root.users_dir() / "u_jason" / "demos" / "demo-2026-04-22.md"
|
||||
parsed = await MarkdownReader.read(path)
|
||||
assert parsed.frontmatter["file_type"] == "user_demo_daily"
|
||||
assert parsed.frontmatter["entry_count"] == 1
|
||||
|
||||
|
||||
async def test_current_count_hook_can_be_overridden(root: MemoryRoot) -> None:
|
||||
"""Subclass override of ``_current_count`` controls seq."""
|
||||
|
||||
class _ConstantCount(BaseDailyWriter):
|
||||
schema = _UserDemoFrontmatter
|
||||
|
||||
async def _current_count(self, path): # noqa: ANN001
|
||||
return 41 # always claim 41 existing entries
|
||||
|
||||
writer = _ConstantCount(root)
|
||||
eid = await writer.append("u_jason", "body", date=dt.date(2026, 4, 22))
|
||||
assert eid.seq == 42 # 41 + 1
|
||||
|
||||
|
||||
async def test_frontmatter_updates_hook_supplies_defaults(root: MemoryRoot) -> None:
|
||||
"""Subclass override of ``_frontmatter_updates`` populates frontmatter."""
|
||||
|
||||
class _WithDefaults(BaseDailyWriter):
|
||||
schema = _UserDemoFrontmatter
|
||||
|
||||
def _frontmatter_updates(self, scope_id, date, *, next_count): # noqa: ANN001
|
||||
return {
|
||||
"user_id": scope_id,
|
||||
"entry_count": next_count,
|
||||
"marker": "from-hook",
|
||||
}
|
||||
|
||||
writer = _WithDefaults(root)
|
||||
await writer.append("u_jason", "body", date=dt.date(2026, 4, 22))
|
||||
|
||||
path = root.users_dir() / "u_jason" / "demos" / "demo-2026-04-22.md"
|
||||
parsed = await MarkdownReader.read(path)
|
||||
assert parsed.frontmatter["marker"] == "from-hook"
|
||||
assert parsed.frontmatter["entry_count"] == 1
|
||||
assert parsed.frontmatter["user_id"] == "u_jason"
|
||||
|
||||
|
||||
async def test_explicit_frontmatter_updates_skip_hook(root: MemoryRoot) -> None:
|
||||
"""Caller-supplied ``frontmatter_updates`` overrides the hook entirely."""
|
||||
|
||||
class _WithDefaults(BaseDailyWriter):
|
||||
schema = _UserDemoFrontmatter
|
||||
|
||||
def _frontmatter_updates(self, scope_id, date, *, next_count): # noqa: ANN001
|
||||
return {"marker": "from-hook"}
|
||||
|
||||
writer = _WithDefaults(root)
|
||||
await writer.append(
|
||||
"u_jason",
|
||||
"body",
|
||||
date=dt.date(2026, 4, 22),
|
||||
frontmatter_updates={"marker": "explicit"},
|
||||
)
|
||||
path = root.users_dir() / "u_jason" / "demos" / "demo-2026-04-22.md"
|
||||
parsed = await MarkdownReader.read(path)
|
||||
assert parsed.frontmatter["marker"] == "explicit"
|
||||
@ -0,0 +1,344 @@
|
||||
"""Tests for AtomicFact / Foresight / AgentCase daily-log writers.
|
||||
|
||||
The 4 daily-log kinds (episode + these 3) all share ``BaseDailyWriter``
|
||||
plumbing — exhaustive chassis tests live in ``test_base.py`` and
|
||||
``test_episode_writer.py`` indirectly via the e2e flows. Here we focus
|
||||
on the per-kind path resolution + frontmatter shape that each
|
||||
subclass owns: ``schema``, ``_frontmatter_updates``, and the
|
||||
writer ↔ reader round-trip on a fresh tmp memory_root.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as _dt
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from everos.core.persistence import MarkdownReader, MemoryRoot
|
||||
from everos.infra.persistence.markdown import (
|
||||
AgentCaseReader,
|
||||
AgentCaseWriter,
|
||||
AtomicFactReader,
|
||||
AtomicFactWriter,
|
||||
ForesightReader,
|
||||
ForesightWriter,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def memory_root(tmp_path: Path) -> MemoryRoot:
|
||||
mr = MemoryRoot(tmp_path)
|
||||
mr.ensure()
|
||||
return mr
|
||||
|
||||
|
||||
# ── AtomicFact ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_atomic_fact_writer_round_trip(memory_root: MemoryRoot) -> None:
|
||||
writer = AtomicFactWriter(memory_root)
|
||||
today = _dt.date(2026, 5, 15)
|
||||
eid = await writer.append_entry(
|
||||
"u1",
|
||||
inline={
|
||||
"owner_id": "u1",
|
||||
"session_id": "s1",
|
||||
"timestamp": "2026-05-15T10:00:00+00:00",
|
||||
"parent_id": "mc_1",
|
||||
"sender_ids": ["u1"],
|
||||
},
|
||||
sections={"Fact": "Alice prefers Italian."},
|
||||
date=today,
|
||||
)
|
||||
path = (
|
||||
memory_root.users_dir() / "u1" / ".atomic_facts" / "atomic_fact-2026-05-15.md"
|
||||
)
|
||||
parsed = await MarkdownReader.read(path)
|
||||
|
||||
# frontmatter
|
||||
fm = parsed.frontmatter
|
||||
assert fm["id"] == "atomic_fact_log_u1_2026-05-15"
|
||||
assert fm["type"] == "atomic_fact_daily"
|
||||
assert fm["file_type"] == "atomic_fact_daily"
|
||||
assert fm["user_id"] == "u1"
|
||||
assert fm["track"] == "user"
|
||||
assert fm["date"] == "2026-05-15"
|
||||
assert fm["entry_count"] == 1
|
||||
|
||||
# entry body
|
||||
assert len(parsed.entries) == 1
|
||||
entry = parsed.entries[0]
|
||||
assert entry.id == eid.format()
|
||||
structured = entry.as_structured()
|
||||
assert structured.inline["owner_id"] == "u1"
|
||||
assert structured.inline["parent_id"] == "mc_1"
|
||||
assert structured.sections["Fact"] == "Alice prefers Italian."
|
||||
|
||||
# reader is symmetric
|
||||
reader = AtomicFactReader(memory_root)
|
||||
assert reader.path_for("u1", today) == path
|
||||
found = await reader.find_structured("u1", eid)
|
||||
assert found is not None
|
||||
assert found.sections["Fact"] == "Alice prefers Italian."
|
||||
|
||||
|
||||
async def test_atomic_fact_writer_appends_multiple(memory_root: MemoryRoot) -> None:
|
||||
writer = AtomicFactWriter(memory_root)
|
||||
today = _dt.date(2026, 5, 15)
|
||||
eid1 = await writer.append_entry(
|
||||
"u1",
|
||||
inline={
|
||||
"owner_id": "u1",
|
||||
"session_id": "s1",
|
||||
"timestamp": "2026-05-15T10:00:00+00:00",
|
||||
"parent_id": "mc_1",
|
||||
},
|
||||
sections={"Fact": "fact 1"},
|
||||
date=today,
|
||||
)
|
||||
eid2 = await writer.append_entry(
|
||||
"u1",
|
||||
inline={
|
||||
"owner_id": "u1",
|
||||
"session_id": "s1",
|
||||
"timestamp": "2026-05-15T11:00:00+00:00",
|
||||
"parent_id": "mc_2",
|
||||
},
|
||||
sections={"Fact": "fact 2"},
|
||||
date=today,
|
||||
)
|
||||
assert eid1.format() != eid2.format()
|
||||
assert eid2.format().endswith("0002")
|
||||
|
||||
|
||||
# ── Foresight ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_foresight_writer_round_trip(memory_root: MemoryRoot) -> None:
|
||||
writer = ForesightWriter(memory_root)
|
||||
today = _dt.date(2026, 5, 15)
|
||||
eid = await writer.append_entry(
|
||||
"u1",
|
||||
inline={
|
||||
"owner_id": "u1",
|
||||
"session_id": "s1",
|
||||
"timestamp": "2026-05-15T10:00:00+00:00",
|
||||
"parent_id": "mc_1",
|
||||
"start_time": "2026-05-15T12:00:00+00:00",
|
||||
"end_time": "2026-05-15T13:00:00+00:00",
|
||||
"duration_days": 1,
|
||||
},
|
||||
sections={
|
||||
"Foresight": "User will book lunch at noon.",
|
||||
"Evidence": "Past calendar pattern.",
|
||||
},
|
||||
date=today,
|
||||
)
|
||||
path = memory_root.users_dir() / "u1" / ".foresights" / "foresight-2026-05-15.md"
|
||||
parsed = await MarkdownReader.read(path)
|
||||
fm = parsed.frontmatter
|
||||
assert fm["id"] == "foresight_log_u1_2026-05-15"
|
||||
assert fm["type"] == "foresight_daily"
|
||||
|
||||
structured = parsed.entries[0].as_structured()
|
||||
assert structured.sections["Foresight"] == "User will book lunch at noon."
|
||||
assert structured.sections["Evidence"] == "Past calendar pattern."
|
||||
assert structured.inline["duration_days"] == "1"
|
||||
assert structured.inline["start_time"].startswith("2026-05-15T12:00:00")
|
||||
|
||||
reader = ForesightReader(memory_root)
|
||||
found = await reader.find_structured("u1", eid)
|
||||
assert found is not None
|
||||
assert found.sections["Evidence"] == "Past calendar pattern."
|
||||
|
||||
|
||||
# ── AgentCase ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_agent_case_writer_round_trip(memory_root: MemoryRoot) -> None:
|
||||
writer = AgentCaseWriter(memory_root)
|
||||
today = _dt.date(2026, 5, 15)
|
||||
eid = await writer.append_entry(
|
||||
"a1",
|
||||
inline={
|
||||
"owner_id": "a1",
|
||||
"session_id": "s1",
|
||||
"timestamp": "2026-05-15T10:00:00+00:00",
|
||||
"parent_id": "mc_agent",
|
||||
"quality_score": 0.87,
|
||||
},
|
||||
sections={
|
||||
"TaskIntent": "Scan contract for indemnity gaps.",
|
||||
"Approach": "1. read sections;\n2. flag clauses;\n3. cross-check cap.",
|
||||
"KeyInsight": "Indemnity cap missing in section 4.",
|
||||
},
|
||||
date=today,
|
||||
)
|
||||
path = memory_root.agents_dir() / "a1" / ".cases" / "agent_case-2026-05-15.md"
|
||||
parsed = await MarkdownReader.read(path)
|
||||
fm = parsed.frontmatter
|
||||
assert fm["id"] == "agent_case_log_a1_2026-05-15"
|
||||
assert fm["type"] == "agent_case_daily"
|
||||
assert fm["agent_id"] == "a1"
|
||||
assert fm["track"] == "agent"
|
||||
|
||||
structured = parsed.entries[0].as_structured()
|
||||
assert structured.inline["quality_score"] == "0.87"
|
||||
assert structured.sections["TaskIntent"].startswith("Scan contract")
|
||||
assert structured.sections["Approach"].startswith("1. read sections")
|
||||
assert structured.sections["KeyInsight"].startswith("Indemnity cap missing")
|
||||
|
||||
reader = AgentCaseReader(memory_root)
|
||||
assert reader.path_for("a1", today) == path
|
||||
found = await reader.find_structured("a1", eid)
|
||||
assert found is not None
|
||||
assert found.sections["TaskIntent"].startswith("Scan contract")
|
||||
|
||||
|
||||
# ── round-trip with cascade handler (md → LanceDB row mapping) ─────────────
|
||||
|
||||
|
||||
async def test_atomic_fact_writer_output_feeds_handler(
|
||||
memory_root: MemoryRoot,
|
||||
) -> None:
|
||||
"""The writer's md is exactly what AtomicFactHandler expects to read."""
|
||||
from everos.component.embedding import EmbeddingProvider
|
||||
from everos.component.tokenizer import Tokenizer
|
||||
from everos.memory.cascade.handlers import AtomicFactHandler, HandlerDeps
|
||||
from everos.memory.cascade.handlers._daily_log_base import ParsedEntry
|
||||
|
||||
class _T(Tokenizer):
|
||||
def tokenize(self, t): # type: ignore[no-untyped-def]
|
||||
return [x for x in t.split() if x]
|
||||
|
||||
def tokenize_batch(self, ts): # type: ignore[no-untyped-def]
|
||||
return [self.tokenize(x) for x in ts]
|
||||
|
||||
class _E(EmbeddingProvider):
|
||||
dim = 1024
|
||||
|
||||
async def embed(self, t): # type: ignore[no-untyped-def]
|
||||
return [0.0] * self.dim
|
||||
|
||||
async def embed_batch(self, ts): # type: ignore[no-untyped-def]
|
||||
return [await self.embed(x) for x in ts]
|
||||
|
||||
today = _dt.date(2026, 5, 15)
|
||||
eid = await AtomicFactWriter(memory_root).append_entry(
|
||||
"u1",
|
||||
inline={
|
||||
"owner_id": "u1",
|
||||
"session_id": "s1",
|
||||
"timestamp": "2026-05-15T10:00:00+00:00",
|
||||
"parent_id": "mc_1",
|
||||
"sender_ids": ["u1"],
|
||||
},
|
||||
sections={"Fact": "Alice prefers Italian."},
|
||||
date=today,
|
||||
)
|
||||
path = (
|
||||
memory_root.users_dir() / "u1" / ".atomic_facts" / "atomic_fact-2026-05-15.md"
|
||||
)
|
||||
rel = path.relative_to(memory_root.root).as_posix()
|
||||
parsed = await MarkdownReader.read(path)
|
||||
entry = parsed.entries[0]
|
||||
handler = AtomicFactHandler(
|
||||
HandlerDeps(memory_root=memory_root, embedder=_E(), tokenizer=_T())
|
||||
)
|
||||
structured = entry.as_structured()
|
||||
pe = ParsedEntry(entry.id, structured, handler._content_sha256(structured))
|
||||
row = await handler._build_row(
|
||||
owner_id="u1", owner_type="user", md_path=rel, entry=pe
|
||||
)
|
||||
assert row.id == f"u1_{eid.format()}"
|
||||
assert row.fact == "Alice prefers Italian."
|
||||
assert row.parent_id == "mc_1"
|
||||
assert row.sender_ids == ["u1"]
|
||||
assert len(row.vector) == 1024
|
||||
|
||||
|
||||
# ── Display-tz contract for frontmatter timestamps (Gap #5) ────────────
|
||||
|
||||
|
||||
async def test_atomic_fact_frontmatter_last_appended_at_carries_display_tz_offset(
|
||||
memory_root: MemoryRoot,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""``last_appended_at`` in markdown frontmatter renders in the display tz.
|
||||
|
||||
Markdown frontmatter is a display-side artefact (users read the file
|
||||
directly), so ``last_appended_at`` must use
|
||||
:func:`get_now_with_timezone` not :func:`get_utc_now`. Pins that
|
||||
contract end-to-end: configure ``EVEROS_MEMORY__TIMEZONE=Asia/Shanghai``,
|
||||
write an entry, read the .md file, assert the literal string ends
|
||||
with ``+08:00``.
|
||||
|
||||
Repeats the same check for ``ForesightWriter`` and
|
||||
``AgentCaseWriter`` — they share ``BaseDailyWriter`` plumbing so a
|
||||
regression on one would likely affect all three, but pinning each
|
||||
rules out per-subclass shadowing of ``_frontmatter_updates``.
|
||||
"""
|
||||
from everos.component.utils import datetime as _dt_module
|
||||
from everos.config import load_settings
|
||||
|
||||
monkeypatch.setenv("EVEROS_MEMORY__TIMEZONE", "Asia/Shanghai")
|
||||
load_settings.cache_clear()
|
||||
_dt_module._display_tz.cache_clear()
|
||||
|
||||
today = _dt.date(2026, 5, 15)
|
||||
|
||||
# AtomicFact
|
||||
af_writer = AtomicFactWriter(memory_root)
|
||||
await af_writer.append_entry(
|
||||
"u1",
|
||||
inline={
|
||||
"owner_id": "u1",
|
||||
"session_id": "s1",
|
||||
"timestamp": "2026-05-15T10:00:00+00:00",
|
||||
"parent_id": "mc_1",
|
||||
"sender_ids": ["u1"],
|
||||
},
|
||||
sections={"Fact": "x"},
|
||||
date=today,
|
||||
)
|
||||
af_path = (
|
||||
memory_root.users_dir() / "u1" / ".atomic_facts" / "atomic_fact-2026-05-15.md"
|
||||
)
|
||||
af_fm = (await MarkdownReader.read(af_path)).frontmatter
|
||||
assert af_fm["last_appended_at"].endswith("+08:00"), af_fm["last_appended_at"]
|
||||
|
||||
# Foresight
|
||||
fs_writer = ForesightWriter(memory_root)
|
||||
await fs_writer.append_entry(
|
||||
"u1",
|
||||
inline={
|
||||
"owner_id": "u1",
|
||||
"session_id": "s1",
|
||||
"timestamp": "2026-05-15T10:00:00+00:00",
|
||||
"scope": "today",
|
||||
"horizon_days": 1,
|
||||
},
|
||||
sections={"Foresight": "x"},
|
||||
date=today,
|
||||
)
|
||||
fs_path = memory_root.users_dir() / "u1" / ".foresights" / "foresight-2026-05-15.md"
|
||||
fs_fm = (await MarkdownReader.read(fs_path)).frontmatter
|
||||
assert fs_fm["last_appended_at"].endswith("+08:00"), fs_fm["last_appended_at"]
|
||||
|
||||
# AgentCase
|
||||
ac_writer = AgentCaseWriter(memory_root)
|
||||
await ac_writer.append_entry(
|
||||
"a1",
|
||||
inline={
|
||||
"owner_id": "a1",
|
||||
"session_id": "s1",
|
||||
"timestamp": "2026-05-15T10:00:00+00:00",
|
||||
"quality_score": 0.9,
|
||||
},
|
||||
sections={"Task intent": "x", "Approach": "y"},
|
||||
date=today,
|
||||
)
|
||||
ac_path = memory_root.agents_dir() / "a1" / ".cases" / "agent_case-2026-05-15.md"
|
||||
ac_fm = (await MarkdownReader.read(ac_path)).frontmatter
|
||||
assert ac_fm["last_appended_at"].endswith("+08:00"), ac_fm["last_appended_at"]
|
||||
@ -0,0 +1,166 @@
|
||||
"""Tests for :class:`ProfileWriter` — single-file rewrite layout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
import pytest
|
||||
|
||||
from everos.core.persistence import (
|
||||
AgentScopedFrontmatter,
|
||||
BaseFrontmatter,
|
||||
MarkdownReader,
|
||||
MemoryRoot,
|
||||
UserScopedFrontmatter,
|
||||
)
|
||||
from everos.infra.persistence.markdown.writers import ProfileWriter
|
||||
|
||||
|
||||
class _UserProfileFM(UserScopedFrontmatter):
|
||||
PROFILE_FILENAME: ClassVar[str] = "user.md"
|
||||
type: Literal["demo_user_profile"] = "demo_user_profile"
|
||||
display_name: str = ""
|
||||
bio: str = ""
|
||||
|
||||
|
||||
class _AgentProfileFM(AgentScopedFrontmatter):
|
||||
PROFILE_FILENAME: ClassVar[str] = "agent.md"
|
||||
type: Literal["demo_agent_profile"] = "demo_agent_profile"
|
||||
name: str = ""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root(tmp_path: Path) -> MemoryRoot:
|
||||
return MemoryRoot(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def writer(root: MemoryRoot) -> ProfileWriter:
|
||||
return ProfileWriter(root)
|
||||
|
||||
|
||||
async def test_write_creates_user_profile(
|
||||
root: MemoryRoot, writer: ProfileWriter
|
||||
) -> None:
|
||||
fm = _UserProfileFM(
|
||||
id="demo_user_profile_u_jason",
|
||||
type="demo_user_profile",
|
||||
user_id="u_jason",
|
||||
display_name="Jason",
|
||||
bio="hiker.",
|
||||
)
|
||||
path = await writer.write("u_jason", frontmatter=fm, body="Long-form profile.")
|
||||
expected = root.users_dir() / "u_jason" / "user.md"
|
||||
assert path == expected
|
||||
assert expected.is_file()
|
||||
|
||||
|
||||
async def test_write_creates_agent_profile(
|
||||
root: MemoryRoot, writer: ProfileWriter
|
||||
) -> None:
|
||||
fm = _AgentProfileFM(
|
||||
id="demo_agent_profile_agent_x",
|
||||
type="demo_agent_profile",
|
||||
agent_id="agent_x",
|
||||
name="zhang_legal",
|
||||
)
|
||||
path = await writer.write("agent_x", frontmatter=fm, body="Agent playbook.")
|
||||
expected = root.agents_dir() / "agent_x" / "agent.md"
|
||||
assert path == expected
|
||||
assert expected.is_file()
|
||||
|
||||
|
||||
async def test_write_writes_frontmatter_and_body(
|
||||
root: MemoryRoot, writer: ProfileWriter
|
||||
) -> None:
|
||||
fm = _UserProfileFM(
|
||||
id="demo_user_profile_u_jason",
|
||||
type="demo_user_profile",
|
||||
user_id="u_jason",
|
||||
display_name="Jason",
|
||||
bio="weekend hiker.",
|
||||
)
|
||||
await writer.write("u_jason", frontmatter=fm, body="The body.")
|
||||
|
||||
parsed = await MarkdownReader.read(root.users_dir() / "u_jason" / "user.md")
|
||||
assert parsed.frontmatter["display_name"] == "Jason"
|
||||
assert parsed.frontmatter["bio"] == "weekend hiker."
|
||||
assert parsed.body.rstrip("\n") == "The body."
|
||||
|
||||
|
||||
async def test_write_is_upsert_full_replace(
|
||||
root: MemoryRoot, writer: ProfileWriter
|
||||
) -> None:
|
||||
"""Second call overwrites both frontmatter and body — no append."""
|
||||
fm1 = _UserProfileFM(
|
||||
id="demo_user_profile_u_jason",
|
||||
type="demo_user_profile",
|
||||
user_id="u_jason",
|
||||
display_name="Jason v1",
|
||||
bio="v1",
|
||||
)
|
||||
await writer.write("u_jason", frontmatter=fm1, body="body v1")
|
||||
|
||||
fm2 = _UserProfileFM(
|
||||
id="demo_user_profile_u_jason",
|
||||
type="demo_user_profile",
|
||||
user_id="u_jason",
|
||||
display_name="Jason v2",
|
||||
bio="v2",
|
||||
)
|
||||
await writer.write("u_jason", frontmatter=fm2, body="body v2")
|
||||
|
||||
parsed = await MarkdownReader.read(root.users_dir() / "u_jason" / "user.md")
|
||||
assert parsed.frontmatter["display_name"] == "Jason v2"
|
||||
assert parsed.frontmatter["bio"] == "v2"
|
||||
assert parsed.body.rstrip("\n") == "body v2"
|
||||
assert "v1" not in parsed.body
|
||||
|
||||
|
||||
def test_path_for_does_not_create_files(
|
||||
root: MemoryRoot, writer: ProfileWriter
|
||||
) -> None:
|
||||
"""``path_for`` is a pure path resolver — no IO."""
|
||||
p = writer.path_for("u_jason", schema=_UserProfileFM)
|
||||
assert p == root.users_dir() / "u_jason" / "user.md"
|
||||
assert not p.exists()
|
||||
assert not root.users_dir().exists()
|
||||
|
||||
|
||||
async def test_write_normalises_trailing_newline(
|
||||
root: MemoryRoot, writer: ProfileWriter
|
||||
) -> None:
|
||||
fm = _UserProfileFM(
|
||||
id="demo_user_profile_u_jason",
|
||||
type="demo_user_profile",
|
||||
user_id="u_jason",
|
||||
)
|
||||
await writer.write("u_jason", frontmatter=fm, body="no-newline-end")
|
||||
text = (root.users_dir() / "u_jason" / "user.md").read_text(encoding="utf-8")
|
||||
assert text.endswith("no-newline-end\n")
|
||||
|
||||
|
||||
async def test_write_rejects_schema_missing_profile_filename(
|
||||
writer: ProfileWriter,
|
||||
) -> None:
|
||||
"""Schema without ``PROFILE_FILENAME`` ClassVar raises a clear error."""
|
||||
|
||||
class _BadSchema(UserScopedFrontmatter):
|
||||
type: Literal["bad"] = "bad"
|
||||
|
||||
fm = _BadSchema(id="x", type="bad", user_id="u_jason")
|
||||
with pytest.raises(TypeError, match="PROFILE_FILENAME"):
|
||||
await writer.write("u_jason", frontmatter=fm, body="body")
|
||||
|
||||
|
||||
async def test_write_rejects_schema_missing_scope_dir(writer: ProfileWriter) -> None:
|
||||
"""Schema without scope mixin (empty ``SCOPE_DIR``) raises a clear error."""
|
||||
|
||||
class _ScopelessSchema(BaseFrontmatter):
|
||||
PROFILE_FILENAME: ClassVar[str] = "profile.md"
|
||||
type: Literal["scopeless"] = "scopeless"
|
||||
|
||||
fm = _ScopelessSchema(id="x", type="scopeless")
|
||||
with pytest.raises(TypeError, match="SCOPE_DIR"):
|
||||
await writer.write("x", frontmatter=fm, body="body")
|
||||
Reference in New Issue
Block a user