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