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

210 lines
6.0 KiB
Python

import json
from pathlib import Path
import pytest
import opensquilla.memory.checkpoint as checkpoint
from opensquilla.memory.checkpoint import (
CheckpointEvent,
append_checkpoint_events,
build_checkpoint_events,
checkpoint_event_hash,
checkpoint_relative_path,
)
from opensquilla.provider import ContentBlockText, Message
def _checkpoint_event(
*,
session_key: str = "agent:main:webchat:abc",
turn_id: str = "turn-1",
) -> CheckpointEvent:
return CheckpointEvent(
schema_version=1,
event_id="evt-1",
session_key=session_key,
session_id="session-1",
turn_id=turn_id,
sequence=1,
timestamp_ms=123,
role="user",
content_type="text",
content="hello",
summary=None,
tool_name=None,
tool_call_id=None,
status="ok",
token_estimate=1,
source="turn_runner",
attachments=[],
content_hash="",
)
def test_checkpoint_event_serializes_required_fields() -> None:
event = CheckpointEvent(
schema_version=1,
event_id="evt-1",
session_key="agent:main:webchat:abc",
session_id="session-1",
turn_id="turn-1",
sequence=1,
timestamp_ms=123,
role="tool_result",
content_type="json",
content='{"ok": true}',
summary="tool succeeded",
tool_name="memory_save",
tool_call_id="call-1",
status="ok",
token_estimate=3,
source="tool_runtime",
attachments=[],
content_hash="",
)
payload = event.to_json_dict()
assert payload["schema_version"] == 1
assert payload["session_key"] == "agent:main:webchat:abc"
assert payload["role"] == "tool_result"
assert payload["tool_name"] == "memory_save"
assert payload["status"] == "ok"
def test_checkpoint_hash_is_stable_for_normalized_content() -> None:
first = checkpoint_event_hash(" user message\n")
second = checkpoint_event_hash("user message")
assert first == second
assert len(first) == 64
def test_build_checkpoint_events_preserves_content_and_reasoning_content() -> None:
message = Message(
role="assistant",
content="visible answer",
reasoning_content="private chain",
)
events = build_checkpoint_events(
session_key="agent:main:webchat:abc",
session_id="session-1",
entries=[message],
source="turn_runner",
turn_id="turn-1",
)
assert len(events) == 1
payload = json.loads(events[0].content)
assert payload == {
"content": "visible answer",
"reasoning_content": "private chain",
}
assert events[0].content_type == "json"
def test_build_checkpoint_events_serializes_provider_content_blocks_as_json() -> None:
message = Message(
role="assistant",
content=[ContentBlockText(text="visible block")],
)
events = build_checkpoint_events(
session_key="agent:main:webchat:abc",
session_id="session-1",
entries=[message],
source="turn_runner",
turn_id="turn-1",
)
assert len(events) == 1
assert json.loads(events[0].content) == [{"type": "text", "text": "visible block"}]
assert events[0].content_type == "json"
async def test_append_checkpoint_events_writes_jsonl_once(tmp_path):
event = _checkpoint_event()
result = append_checkpoint_events(tmp_path, [event])
second = append_checkpoint_events(tmp_path, [event])
assert result.relative_path == "memory/.checkpoints/agent-main-webchat-abc/turn-1.jsonl"
assert result.event_count == 1
assert result.content_hash == second.content_hash
lines = (tmp_path / result.relative_path).read_text(encoding="utf-8").splitlines()
assert len(lines) == 1
@pytest.mark.parametrize(
"event",
[
_checkpoint_event(session_key="agent:main:webchat:other"),
_checkpoint_event(turn_id="turn-2"),
],
)
def test_append_checkpoint_events_rejects_mixed_turn_batches(tmp_path, event):
with pytest.raises(ValueError, match="share session_key and turn_id"):
append_checkpoint_events(tmp_path, [_checkpoint_event(), event])
def test_append_checkpoint_events_cleans_temp_file_when_write_fails(
tmp_path,
monkeypatch,
):
class FailingTempFile:
def __init__(self, *, dir, prefix, suffix, **_kwargs):
self.name = str(Path(dir) / f"{prefix}forced{suffix}")
def __enter__(self):
Path(self.name).write_text("partial", encoding="utf-8")
return self
def __exit__(self, *_args):
return False
def write(self, _body):
raise OSError("simulated write failure")
monkeypatch.setattr(checkpoint.tempfile, "NamedTemporaryFile", FailingTempFile)
with pytest.raises(OSError, match="simulated write failure"):
append_checkpoint_events(tmp_path, [_checkpoint_event()])
checkpoint_dir = tmp_path / "memory/.checkpoints/agent-main-webchat-abc"
assert not any(path.name.endswith(".tmp") for path in checkpoint_dir.iterdir())
def test_checkpoint_relative_path_is_sidecar_only() -> None:
path = checkpoint_relative_path(
session_key="agent:main:webchat:abc",
turn_id="turn-1",
)
assert path == Path("memory/.checkpoints/agent-main-webchat-abc/turn-1.jsonl")
@pytest.mark.parametrize(
("session_key", "expected_component"),
[
("..", "unknown"),
(".", "unknown"),
("..-", "unknown"),
("-..", "unknown"),
("---..---", "unknown"),
(".-", "unknown"),
("-.", "unknown"),
("../abc", "..-abc"),
("a/b", "a-b"),
],
)
def test_checkpoint_relative_path_sanitizes_unsafe_session_components(
session_key: str,
expected_component: str,
) -> None:
path = checkpoint_relative_path(session_key=session_key, turn_id="turn-1")
assert path == Path("memory/.checkpoints") / expected_component / "turn-1.jsonl"
assert ".." not in path.relative_to("memory/.checkpoints").parts
assert "." not in path.relative_to("memory/.checkpoints").parts