151 lines
5.8 KiB
Python
151 lines
5.8 KiB
Python
import asyncio
|
|
import tempfile
|
|
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
from test_conversations import ScriptedClient, World, world
|
|
|
|
from beaver_gateway.core.envelope import HEADER, Envelope, render
|
|
from beaver_gateway.core.watch import Change, VaultWatch, WatchRules
|
|
|
|
__all__ = ["world"]
|
|
|
|
RULES = WatchRules(
|
|
full=("дни/{today}.md",),
|
|
names=("дни/*", "люди/*", "мета/бобер/*"),
|
|
ignore=("чаты/*", "мета/*"),
|
|
)
|
|
|
|
|
|
def vault() -> tuple[Path, VaultWatch]:
|
|
root = Path(tempfile.mkdtemp(prefix="beaver-vault-"))
|
|
for sub in ("дни", "люди", "чаты", "мета/бобер", "мета/чужое"):
|
|
(root / sub).mkdir(parents=True)
|
|
today = datetime.now(UTC).date().isoformat()
|
|
(root / "дни" / f"{today}.md").write_text("# день\n- 09:00 проснулся\n")
|
|
(root / "люди" / "Прохор.md").write_text("# Прохор\nстрока\n")
|
|
(root / "чаты" / "чат.md").write_text("чат\n")
|
|
(root / "мета" / "чужое" / "x.md").write_text("x\n")
|
|
watch = VaultWatch(root, RULES, tz="UTC")
|
|
watch.snapshot()
|
|
return root, watch
|
|
|
|
|
|
def append(path: Path, text: str) -> None:
|
|
with path.open("a", encoding="utf-8") as fh:
|
|
fh.write(text)
|
|
|
|
|
|
def test_rules_classify_paths() -> None:
|
|
today = "2026-08-29"
|
|
assert RULES.kind("дни/2026-08-29.md", today=today) == "full"
|
|
assert RULES.kind("дни/2026-08-28.md", today=today) == "names"
|
|
assert RULES.kind("люди/Прохор.md", today=today) == "names"
|
|
assert RULES.kind("мета/бобер/состояние.md", today=today) == "names"
|
|
assert RULES.kind("мета/чужое/x.md", today=today) is None
|
|
assert RULES.kind("чаты/чат.md", today=today) is None
|
|
assert RULES.kind("люди/фото.png", today=today) is None
|
|
|
|
|
|
def test_watch_reports_only_added_lines_with_mtime() -> None:
|
|
root, watch = vault()
|
|
today = watch.today()
|
|
diary = root / "дни" / f"{today}.md"
|
|
append(diary, "- 12:40 вышел\n- 13:00 кофе\n")
|
|
append(root / "люди" / "Прохор.md", "ещё\n")
|
|
append(root / "чаты" / "чат.md", "ignored\n")
|
|
for path in (diary, root / "люди" / "Прохор.md", root / "чаты" / "чат.md"):
|
|
watch.note(path)
|
|
changes = {c.path: c for c in watch.take()}
|
|
assert set(changes) == {f"дни/{today}.md", "люди/Прохор.md"}
|
|
assert changes[f"дни/{today}.md"].added == ["- 12:40 вышел", "- 13:00 кофе"]
|
|
assert changes[f"дни/{today}.md"].full
|
|
assert changes["люди/Прохор.md"].added_count == 1
|
|
assert changes["люди/Прохор.md"].added == []
|
|
assert abs(
|
|
changes[f"дни/{today}.md"].mtime
|
|
- datetime.fromtimestamp(diary.stat().st_mtime, tz=UTC)
|
|
) < timedelta(seconds=1)
|
|
assert watch.take() == []
|
|
append(diary, "- 14:00 снова\n")
|
|
watch.note(diary)
|
|
(only,) = watch.take()
|
|
assert only.added == ["- 14:00 снова"]
|
|
|
|
|
|
def test_envelope_respects_ceilings_and_names_only_window() -> None:
|
|
root, watch = vault()
|
|
today = watch.today()
|
|
diary = root / "дни" / f"{today}.md"
|
|
append(diary, "".join(f"- строка {i}\n" for i in range(200)))
|
|
for name in ("Петя", "Маша"):
|
|
(root / "люди" / f"{name}.md").write_text("новый\n" * 50)
|
|
watch.note(root / "люди" / f"{name}.md")
|
|
watch.note(diary)
|
|
envelope = Envelope(watch=watch, tz="Europe/Warsaw")
|
|
text = envelope.build()
|
|
lines = text.splitlines()
|
|
assert lines[0] == HEADER
|
|
assert "(Warsaw)" in lines[1]
|
|
assert lines[2].startswith("vault, изменено со старта: ")
|
|
assert f"дни/{today}.md (+200)" in lines[2]
|
|
assert "люди/Петя.md (+50)" in lines[2]
|
|
assert sum(1 for line in lines if line.startswith("+ ")) == 31
|
|
assert "+ … ещё 170" in lines
|
|
assert len(lines) <= 120
|
|
append(diary, "- ещё одна\n")
|
|
watch.note(diary)
|
|
second = envelope.build()
|
|
assert f"дни/{today}.md (+1)" in second
|
|
assert "+ - ещё одна" not in second
|
|
|
|
|
|
def test_render_hits_total_ceiling() -> None:
|
|
now = datetime(2026, 8, 26, 11, 4, tzinfo=UTC)
|
|
changes = [
|
|
Change(
|
|
path=f"дни/{i}.md",
|
|
mtime=now,
|
|
added=[f"l{j}" for j in range(30)],
|
|
added_count=30,
|
|
full=True,
|
|
)
|
|
for i in range(10)
|
|
]
|
|
text = render(
|
|
now=now,
|
|
tz="Europe/Warsaw",
|
|
changes=changes,
|
|
since=now - timedelta(hours=1),
|
|
names_only=False,
|
|
)
|
|
lines = text.splitlines()
|
|
assert len(lines) <= 120
|
|
assert "… (потолок конверта)" in lines
|
|
assert lines[1] == "время: 2026-08-26 13:04 (Warsaw)"
|
|
|
|
|
|
async def test_master_turn_gets_envelope_after_text_and_before_injects(
|
|
world: World,
|
|
) -> None:
|
|
root, watch = vault()
|
|
world.conversations._envelope = Envelope(watch=watch, tz="UTC") # noqa: SLF001
|
|
master = await world.conversations.create(kind="master", agent="a", origin="test")
|
|
append(root / "люди" / "Прохор.md", "новое\n")
|
|
watch.note(root / "люди" / "Прохор.md")
|
|
await world.conversations.inject(master, "tick", urgency="normal", origin="крон")
|
|
await world.conversations.post(master, "hello")
|
|
await world.settle(master, 2)
|
|
prompt = ScriptedClient.instances[0].prompts[0]
|
|
head, _, rest = prompt.partition("\n\n")
|
|
assert head == "hello"
|
|
assert rest.startswith(HEADER)
|
|
assert "люди/Прохор.md (+1)" in rest
|
|
assert rest.index("[инжекты") > rest.index(HEADER)
|
|
branch = await world.conversations.spawn(
|
|
kind="branch", parent=master, seed="brief", text="do X"
|
|
)
|
|
await world.settle(branch, 1)
|
|
assert "[конверт" not in ScriptedClient.instances[-1].prompts[0]
|
|
await asyncio.sleep(0)
|