Files
2026-07-13 13:12:33 +08:00

421 lines
16 KiB
Python

import threading
from types import SimpleNamespace
from typing import Any
import pytest
from opensquilla.memory.checkpoint import checkpoint_coverage_hash, checkpoint_turn_id
from opensquilla.memory.session_flush import FlushReceipt, SessionFlushService
from opensquilla.provider import Message
from opensquilla.session.manager import SessionManager
from opensquilla.session.models import MemoryDurableReceipt
from opensquilla.session.storage import SessionStorage
from opensquilla.tool_boundary import ToolCall, ToolResult
async def test_memory_durable_receipt_upsert_is_idempotent(tmp_path):
storage = await SessionStorage.open(tmp_path / "sessions.db")
try:
receipt = MemoryDurableReceipt(
receipt_id="r1",
session_key="agent:main:webchat:abc",
session_id="session-1",
turn_id="turn-1",
scope="checkpoint",
source_path="memory/.checkpoints/agent-main-webchat-abc/turn-1.jsonl",
target_path=None,
content_hash="h1",
idempotency_key="checkpoint:agent:main:webchat:abc:turn-1:h1",
status="checkpoint_saved",
reason=None,
attempt_count=0,
next_retry_at_ms=None,
)
await storage.upsert_memory_durable_receipt(receipt)
await storage.upsert_memory_durable_receipt(receipt)
rows = await storage.list_memory_durable_receipts(
session_key="agent:main:webchat:abc"
)
assert len(rows) == 1
assert rows[0].status == "checkpoint_saved"
finally:
await storage.close()
async def test_memory_durable_receipt_filters_and_update(tmp_path):
storage = await SessionStorage.open(tmp_path / "sessions.db")
try:
receipt = MemoryDurableReceipt(
receipt_id="r2",
session_key="agent:main:webchat:abc",
session_id="session-1",
turn_id="turn-2",
scope="checkpoint",
source_path="memory/.checkpoints/agent-main-webchat-abc/turn-2.jsonl",
target_path=None,
content_hash="h2",
idempotency_key="checkpoint:agent:main:webchat:abc:turn-2:h2",
status="checkpoint_failed",
reason="write failed",
attempt_count=1,
next_retry_at_ms=None,
)
saved = await storage.upsert_memory_durable_receipt(receipt)
updated = await storage.update_memory_durable_receipt(
saved.receipt_id,
status="checkpoint_saved",
reason=None,
attempt_count=2,
next_retry_at_ms=123,
)
assert updated.status == "checkpoint_saved"
assert updated.reason is None
assert updated.attempt_count == 2
assert updated.next_retry_at_ms == 123
by_status = await storage.list_memory_durable_receipts(
session_key="agent:main:webchat:abc",
status="checkpoint_saved",
)
by_idempotency = await storage.list_memory_durable_receipts(
idempotency_key="checkpoint:agent:main:webchat:abc:turn-2:h2",
)
assert [row.receipt_id for row in by_status] == ["r2"]
assert [row.receipt_id for row in by_idempotency] == ["r2"]
with pytest.raises(ValueError):
await storage.update_memory_durable_receipt(saved.receipt_id, unknown=True)
with pytest.raises(KeyError):
await storage.update_memory_durable_receipt("missing", status="failed")
finally:
await storage.close()
async def test_memory_durable_receipt_update_canonicalizes_session_key(tmp_path):
storage = await SessionStorage.open(tmp_path / "sessions.db")
try:
saved = await storage.upsert_memory_durable_receipt(
MemoryDurableReceipt(
receipt_id="r3",
session_key="agent:main:webchat:abc",
session_id="session-1",
turn_id="turn-3",
scope="checkpoint",
source_path="memory/.checkpoints/agent-main-webchat-abc/turn-3.jsonl",
target_path=None,
content_hash="h3",
idempotency_key="checkpoint:agent:main:webchat:abc:turn-3:h3",
status="checkpoint_saved",
reason=None,
attempt_count=0,
next_retry_at_ms=None,
)
)
updated = await storage.update_memory_durable_receipt(
saved.receipt_id,
session_key="webchat:default",
)
rows = await storage.list_memory_durable_receipts(
session_key="webchat:default"
)
assert updated.session_key == "agent:main:webchat:default"
assert [row.receipt_id for row in rows] == ["r3"]
finally:
await storage.close()
async def test_memory_durable_receipt_conflict_updates_mutable_fields(tmp_path):
storage = await SessionStorage.open(tmp_path / "sessions.db")
try:
first = await storage.upsert_memory_durable_receipt(
MemoryDurableReceipt(
receipt_id="r4-original",
session_key="agent:main:webchat:abc",
session_id="session-1",
turn_id="turn-4",
scope="checkpoint",
source_path="memory/.checkpoints/agent-main-webchat-abc/turn-4.jsonl",
target_path=None,
content_hash="h4",
idempotency_key="checkpoint:agent:main:webchat:abc:turn-4:h4",
status="checkpoint_failed",
reason="initial failure",
attempt_count=1,
next_retry_at_ms=100,
created_at=1000,
updated_at=1000,
)
)
second = await storage.upsert_memory_durable_receipt(
MemoryDurableReceipt(
receipt_id="r4-conflict",
session_key="agent:main:webchat:abc",
session_id="session-1",
turn_id="turn-4",
scope="checkpoint",
source_path="memory/.checkpoints/agent-main-webchat-abc/turn-4.jsonl",
target_path=None,
content_hash="h4",
idempotency_key="checkpoint:agent:main:webchat:abc:turn-4:h4",
status="checkpoint_saved",
reason=None,
attempt_count=2,
next_retry_at_ms=200,
)
)
rows = await storage.list_memory_durable_receipts(
idempotency_key="checkpoint:agent:main:webchat:abc:turn-4:h4"
)
assert len(rows) == 1
assert second.receipt_id == "r4-original"
assert rows[0].receipt_id == "r4-original"
assert rows[0].created_at == first.created_at
assert rows[0].status == "checkpoint_saved"
assert rows[0].reason is None
assert rows[0].attempt_count == 2
assert rows[0].next_retry_at_ms == 200
assert rows[0].updated_at >= first.updated_at
finally:
await storage.close()
async def test_record_memory_checkpoint_preserves_operation_and_coverage_ids(tmp_path):
storage = await SessionStorage.open(tmp_path / "sessions.db")
manager = SessionManager(storage, checkpoint_workspace_dir=tmp_path / "workspace")
try:
key = "agent:main:webchat:abc"
session = await manager.create(key)
await manager.append_message(key, role="user", content="checkpoint body")
entries = await storage.get_transcript(session.session_id)
receipt = await manager.record_memory_checkpoint(key, turn_id="cmp_123")
assert receipt.turn_id == "cmp_123"
assert receipt.coverage_turn_id == checkpoint_turn_id(entries)
assert receipt.coverage_hash == checkpoint_coverage_hash(entries)
assert receipt.coverage_entry_count == len(entries)
rows = await storage.list_memory_durable_receipts(
session_key=key,
session_id=session.session_id,
scope="checkpoint",
status="checkpoint_saved",
coverage_turn_id=checkpoint_turn_id(entries),
coverage_hash=checkpoint_coverage_hash(entries),
coverage_entry_count=len(entries),
limit=1,
)
assert [row.receipt_id for row in rows] == [receipt.receipt_id]
finally:
await storage.close()
async def test_memory_durable_receipt_coverage_lookup_is_targeted(tmp_path):
storage = await SessionStorage.open(tmp_path / "sessions.db")
try:
for idx in range(105):
await storage.upsert_memory_durable_receipt(
MemoryDurableReceipt(
receipt_id=f"old-{idx}",
session_key="agent:main:webchat:abc",
session_id="session-1",
turn_id=f"cmp-old-{idx}",
scope="checkpoint",
source_path=f"memory/.checkpoints/old-{idx}.jsonl",
content_hash=f"h-old-{idx}",
coverage_turn_id=f"through-{idx}",
coverage_hash=f"coverage-old-{idx}",
coverage_entry_count=1,
idempotency_key=f"checkpoint:old:{idx}",
status="checkpoint_saved",
)
)
target = await storage.upsert_memory_durable_receipt(
MemoryDurableReceipt(
receipt_id="target",
session_key="agent:main:webchat:abc",
session_id="session-1",
turn_id="cmp-target",
scope="checkpoint",
source_path="memory/.checkpoints/target.jsonl",
content_hash="h-target",
coverage_turn_id="through-999",
coverage_hash="coverage-target",
coverage_entry_count=2,
idempotency_key="checkpoint:target",
status="checkpoint_saved",
)
)
rows = await storage.list_memory_durable_receipts(
session_key="agent:main:webchat:abc",
session_id="session-1",
scope="checkpoint",
status="checkpoint_saved",
coverage_turn_id="through-999",
coverage_hash="coverage-target",
coverage_entry_count=2,
limit=1,
)
assert [row.receipt_id for row in rows] == [target.receipt_id]
finally:
await storage.close()
async def test_record_memory_checkpoint_preserves_distinct_failure_receipts(
tmp_path, monkeypatch
):
import opensquilla.memory.checkpoint as checkpoint
storage = await SessionStorage.open(tmp_path / "sessions.db")
manager = SessionManager(storage, checkpoint_workspace_dir=tmp_path / "workspace")
try:
key = "agent:main:webchat:abc"
await manager.create(key)
await manager.append_message(key, role="user", content="same checkpoint body")
errors = iter([RuntimeError("disk full"), RuntimeError("permission denied")])
def _fail_append(*args, **kwargs):
raise next(errors)
monkeypatch.setattr(checkpoint, "append_checkpoint_events", _fail_append)
with pytest.raises(RuntimeError, match="disk full"):
await manager.record_memory_checkpoint(key, turn_id="turn-failed")
with pytest.raises(RuntimeError, match="permission denied"):
await manager.record_memory_checkpoint(key, turn_id="turn-failed")
rows = await storage.list_memory_durable_receipts(
session_key=key,
status="checkpoint_failed",
)
assert len(rows) == 2
assert {row.reason for row in rows} == {"disk full", "permission denied"}
assert len({row.idempotency_key for row in rows}) == 2
finally:
await storage.close()
async def test_record_memory_checkpoint_requires_explicit_checkpoint_workspace(tmp_path):
storage = await SessionStorage.open(tmp_path / "sessions.db")
manager = SessionManager(storage)
manager.workspace_dir = tmp_path / "dynamic-attribute"
try:
key = "agent:main:webchat:abc"
await manager.create(key)
await manager.append_message(key, role="user", content="checkpoint body")
with pytest.raises(RuntimeError, match="checkpoint workspace_dir is not configured"):
await manager.record_memory_checkpoint(key, turn_id="turn-no-workspace")
rows = await storage.list_memory_durable_receipts(
session_key=key,
status="checkpoint_failed",
)
assert len(rows) == 1
assert rows[0].reason == "checkpoint workspace_dir is not configured"
assert not (tmp_path / "dynamic-attribute").exists()
finally:
await storage.close()
async def test_record_memory_checkpoint_writes_checkpoint_off_event_loop(
tmp_path, monkeypatch
):
import opensquilla.memory.checkpoint as checkpoint
storage = await SessionStorage.open(tmp_path / "sessions.db")
manager = SessionManager(storage, checkpoint_workspace_dir=tmp_path / "workspace")
event_loop_thread = threading.get_ident()
writer_thread_ids: list[int] = []
original_append = checkpoint.append_checkpoint_events
try:
key = "agent:main:webchat:abc"
await manager.create(key)
await manager.append_message(key, role="user", content="checkpoint body")
def _spy_append(*args, **kwargs):
writer_thread_ids.append(threading.get_ident())
return original_append(*args, **kwargs)
monkeypatch.setattr(checkpoint, "append_checkpoint_events", _spy_append)
await manager.record_memory_checkpoint(key, turn_id="turn-threaded")
assert writer_thread_ids
assert writer_thread_ids[0] != event_loop_thread
finally:
await storage.close()
async def test_session_flush_repair_receipt_writer_records_raw_fallback_in_ledger(
tmp_path,
):
class InvalidJsonProvider:
async def complete(self, **_kwargs: Any) -> SimpleNamespace:
return SimpleNamespace(content='{"candidates": [')
storage = await SessionStorage.open(tmp_path / "sessions.db")
try:
session_key = "agent:main:webchat:abc"
session_id = "session-1"
async def handler(call: ToolCall) -> ToolResult:
raise AssertionError(f"raw fallback must not call {call.tool_name}")
async def receipt_writer(receipt: FlushReceipt, **row: Any) -> None:
await storage.upsert_memory_durable_receipt(
MemoryDurableReceipt(
session_key=row["session_key"],
session_id=session_id,
scope=row["scope"],
target_path=row["target_path"],
idempotency_key=(
f"{row['scope']}:{row['session_key']}:{row['status']}:"
f"{row['target_path']}"
),
status=row["status"],
reason=row["reason"],
attempt_count=1,
)
)
service = SessionFlushService(
provider_selector=lambda _agent_id: InvalidJsonProvider(),
tool_registry=SimpleNamespace(
to_tool_definitions=lambda: [SimpleNamespace(name="memory_save")]
),
tool_handler=handler,
receipt_writer=receipt_writer,
archive_workspace_resolver=lambda _agent_id: tmp_path,
)
receipt = await service.execute(
[Message(role="user", content="temporary transcript")],
session_key,
agent_id="main",
)
rows = await storage.list_memory_durable_receipts(session_key=session_key)
assert receipt.result_status == "parse_failed_archived"
assert len(rows) == 2
assert rows[0].scope == "preimage"
assert rows[1].scope == "repair"
assert rows[1].status == "repair_pending"
assert rows[1].reason == "parse_failed_archived"
assert rows[1].target_path == receipt.flushed_paths[0]
finally:
await storage.close()