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:
137
tests/unit/test_memory/test_cascade/test_reconciler.py
Normal file
137
tests/unit/test_memory/test_cascade/test_reconciler.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""Tests for :func:`reconcile` — pure scan vs state diff.
|
||||
|
||||
The reconciler is pure (no IO), so each scenario is just a few
|
||||
dataclass instances in / decisions out. Covers the 4 cases:
|
||||
``added`` / ``modified`` / ``deleted`` / ``no-op``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from everos.memory.cascade.reconciler import PriorState, reconcile
|
||||
from everos.memory.cascade.types import ScanInput
|
||||
|
||||
|
||||
def _scan(path: str, mtime: float = 1.0, kind: str = "episode") -> ScanInput:
|
||||
return ScanInput(md_path=path, mtime=mtime, kind=kind)
|
||||
|
||||
|
||||
def _state(
|
||||
path: str,
|
||||
*,
|
||||
mtime: float = 1.0,
|
||||
kind: str = "episode",
|
||||
status: str = "done",
|
||||
change_type: str = "modified",
|
||||
) -> PriorState:
|
||||
return PriorState(
|
||||
md_path=path,
|
||||
kind=kind,
|
||||
mtime=mtime,
|
||||
status=status,
|
||||
change_type=change_type,
|
||||
)
|
||||
|
||||
|
||||
def test_added_path_emits_added_decision() -> None:
|
||||
decisions = reconcile([_scan("a.md")], state={})
|
||||
assert [(d.md_path, d.change_type) for d in decisions] == [("a.md", "added")]
|
||||
|
||||
|
||||
def test_modified_mtime_emits_modified_decision() -> None:
|
||||
decisions = reconcile(
|
||||
[_scan("a.md", mtime=2.0)],
|
||||
state={"a.md": _state("a.md", mtime=1.0)},
|
||||
)
|
||||
assert [(d.md_path, d.change_type) for d in decisions] == [("a.md", "modified")]
|
||||
|
||||
|
||||
def test_done_state_with_matching_mtime_is_skipped() -> None:
|
||||
"""Quiet sweeps must stay quiet — no upsert churn."""
|
||||
decisions = reconcile(
|
||||
[_scan("a.md", mtime=1.0)],
|
||||
state={"a.md": _state("a.md", mtime=1.0, status="done")},
|
||||
)
|
||||
assert decisions == []
|
||||
|
||||
|
||||
def test_pending_state_with_matching_mtime_still_emits_modified() -> None:
|
||||
"""Pending / failed states are NOT terminal — re-emit so worker re-runs."""
|
||||
decisions = reconcile(
|
||||
[_scan("a.md", mtime=1.0)],
|
||||
state={"a.md": _state("a.md", mtime=1.0, status="pending")},
|
||||
)
|
||||
assert [(d.md_path, d.change_type) for d in decisions] == [("a.md", "modified")]
|
||||
|
||||
|
||||
def test_deleted_path_emits_deleted_decision() -> None:
|
||||
decisions = reconcile(
|
||||
[],
|
||||
state={"a.md": _state("a.md", status="pending")},
|
||||
)
|
||||
assert [(d.md_path, d.change_type) for d in decisions] == [("a.md", "deleted")]
|
||||
|
||||
|
||||
def test_deleted_path_already_done_as_delete_is_skipped() -> None:
|
||||
"""A done row that is itself a successful delete cycle — don't re-emit."""
|
||||
decisions = reconcile(
|
||||
[],
|
||||
state={
|
||||
"a.md": _state("a.md", status="done", change_type="deleted"),
|
||||
},
|
||||
)
|
||||
assert decisions == []
|
||||
|
||||
|
||||
def test_done_added_row_with_missing_path_is_recovered_as_deleted() -> None:
|
||||
"""Watcher missed an unlink (e.g. fseventsd drop / daemon restart).
|
||||
|
||||
The state row is ``status='done'`` from the previous add cycle, but
|
||||
the file is gone from disk. The scanner MUST re-emit a 'deleted'
|
||||
decision — otherwise LanceDB keeps stale rows for the orphan path
|
||||
until something else triggers an enqueue.
|
||||
"""
|
||||
decisions = reconcile(
|
||||
[],
|
||||
state={
|
||||
"a.md": _state("a.md", status="done", change_type="added"),
|
||||
},
|
||||
)
|
||||
assert [(d.md_path, d.change_type) for d in decisions] == [("a.md", "deleted")]
|
||||
|
||||
|
||||
def test_done_modified_row_with_missing_path_is_recovered_as_deleted() -> None:
|
||||
"""Same as the added variant, but the prior cycle was a modification."""
|
||||
decisions = reconcile(
|
||||
[],
|
||||
state={
|
||||
"a.md": _state("a.md", status="done", change_type="modified"),
|
||||
},
|
||||
)
|
||||
assert [(d.md_path, d.change_type) for d in decisions] == [("a.md", "deleted")]
|
||||
|
||||
|
||||
def test_mixed_scenario_preserves_order() -> None:
|
||||
decisions = reconcile(
|
||||
[
|
||||
_scan("new.md"),
|
||||
_scan("changed.md", mtime=2.0),
|
||||
_scan("unchanged.md", mtime=1.0),
|
||||
],
|
||||
state={
|
||||
"changed.md": _state(
|
||||
"changed.md", mtime=1.0, status="done", change_type="modified"
|
||||
),
|
||||
"unchanged.md": _state(
|
||||
"unchanged.md", mtime=1.0, status="done", change_type="modified"
|
||||
),
|
||||
"gone.md": _state("gone.md", status="pending", change_type="modified"),
|
||||
},
|
||||
)
|
||||
by_path = {d.md_path: d.change_type for d in decisions}
|
||||
assert by_path == {
|
||||
"new.md": "added",
|
||||
"changed.md": "modified",
|
||||
"gone.md": "deleted",
|
||||
}
|
||||
# Order: added/modified in scan order, deleted at the tail.
|
||||
assert decisions[-1].md_path == "gone.md"
|
||||
Reference in New Issue
Block a user