121 lines
6.0 KiB
Python
121 lines
6.0 KiB
Python
"""Executable completeness contract for the exact-ID message ledger."""
|
|
from __future__ import annotations
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
import sqlite3
|
|
import pytest
|
|
from aios_v2r1.message_ledger import MessageLedger
|
|
from aios_v2r1.tenant_context import CanonicalMessage, ConflictError, ScopeError
|
|
|
|
def message(**changes: object) -> CanonicalMessage:
|
|
value = CanonicalMessage(
|
|
tenant_id="tenant-a", cell_id="cell-a", principal_id="user-a",
|
|
principal_type="authenticated_user", origin_type="channel_user",
|
|
channel="weixin", channel_account_id="account-a", peer_id="peer-a",
|
|
conversation_id="conversation-a", session_id="session-a", task_id=None,
|
|
event_id="event-a", message_id="message-a", reply_to_message_id=None,
|
|
direction="inbound", command_capable=True,
|
|
occurred_at="2026-07-13T00:00:00+00:00",
|
|
received_at="2026-07-13T00:00:01+00:00", body="hello",
|
|
quoted_body=None, adapter_name="weixin", adapter_version="v2")
|
|
return replace(value, **changes)
|
|
|
|
@pytest.fixture
|
|
def ledger(tmp_path: Path) -> MessageLedger:
|
|
return MessageLedger(tmp_path / "ledger.sqlite3")
|
|
|
|
def test_append_writes_directly_without_using_remember(ledger: MessageLedger) -> None:
|
|
ledger.remember = lambda _: (_ for _ in ()).throw(AssertionError("remember called")) # type: ignore[method-assign]
|
|
assert ledger.append(message()).message_id == "message-a"
|
|
assert ledger.count() == 1
|
|
|
|
def test_append_rejects_same_message_id_with_different_content(ledger: MessageLedger) -> None:
|
|
ledger.append(message(body="first"))
|
|
with pytest.raises(ConflictError):
|
|
ledger.append(message(event_id="event-b", body="second"))
|
|
assert ledger.count() == 1
|
|
|
|
def test_append_is_idempotent_when_only_event_id_changes(ledger: MessageLedger) -> None:
|
|
stored = ledger.append(message(event_id="event-a", body="same body"))
|
|
replay = ledger.append(message(event_id="event-b", body="same body"))
|
|
assert replay == stored
|
|
assert replay.event_id == "event-a"
|
|
assert ledger.count() == 1
|
|
|
|
@pytest.mark.parametrize("candidate", ["message", "message-a-extra", "MESSAGE-A"])
|
|
def test_quote_lookup_has_no_prefix_similarity_or_case_fallback(
|
|
ledger: MessageLedger, candidate: str
|
|
) -> None:
|
|
ledger.append(message(message_id="message-a"))
|
|
with pytest.raises(ScopeError, match="quoted_text_unavailable"):
|
|
ledger.resolve_quote(message(event_id="reply-event", message_id="reply-message",
|
|
reply_to_message_id=candidate))
|
|
|
|
def test_explicit_quoted_body_never_falls_back_to_storage(ledger: MessageLedger) -> None:
|
|
assert ledger.resolve_quote(message(quoted_body="exact transport quote")) == "exact transport quote"
|
|
assert ledger.count() == 0
|
|
|
|
@pytest.mark.parametrize("scope_change", [{"tenant_id": "tenant-b"}, {"cell_id": "cell-b"}])
|
|
def test_quote_lookup_is_tenant_and_cell_isolated(
|
|
ledger: MessageLedger, scope_change: dict[str, str]
|
|
) -> None:
|
|
ledger.append(message())
|
|
with pytest.raises(ScopeError, match="quoted_text_unavailable"):
|
|
ledger.resolve_quote(message(**scope_change, event_id="reply-event",
|
|
message_id="reply-message",
|
|
reply_to_message_id="message-a"))
|
|
|
|
def test_same_id_across_directions_is_ambiguous_not_guessed(ledger: MessageLedger) -> None:
|
|
ledger.append(message(direction="inbound", body="in"))
|
|
ledger.append(message(direction="outbound", event_id="event-b", body="out"))
|
|
with pytest.raises(ConflictError, match="ambiguous"):
|
|
ledger.resolve_quote(message(event_id="event-c", message_id="message-c",
|
|
reply_to_message_id="message-a"))
|
|
|
|
def test_expired_record_is_not_resolved(ledger: MessageLedger) -> None:
|
|
ledger.append(message())
|
|
with sqlite3.connect(ledger.database) as db:
|
|
db.execute("UPDATE message_ledger SET expires_at='2000-01-01T00:00:00+00:00'")
|
|
with pytest.raises(ScopeError, match="quoted_text_unavailable"):
|
|
ledger.resolve_quote(message(event_id="event-b", message_id="message-b",
|
|
reply_to_message_id="message-a"))
|
|
|
|
def test_purge_removes_only_expired_records(ledger: MessageLedger) -> None:
|
|
ledger.append(message())
|
|
ledger.append(message(event_id="event-b", message_id="message-b"))
|
|
with sqlite3.connect(ledger.database) as db:
|
|
db.execute("UPDATE message_ledger SET expires_at='2000-01-01T00:00:00+00:00' "
|
|
"WHERE message_id='message-a'")
|
|
assert ledger.purge_expired() == 1
|
|
assert ledger.count() == 1
|
|
|
|
def test_restart_preserves_exact_resolution(tmp_path: Path) -> None:
|
|
database = tmp_path / "ledger.sqlite3"
|
|
MessageLedger(database).append(message())
|
|
reopened = MessageLedger(database)
|
|
assert reopened.resolve_quote(message(event_id="event-b", message_id="message-b",
|
|
reply_to_message_id="message-a")) == "hello"
|
|
|
|
def test_concurrent_identical_append_is_one_record(ledger: MessageLedger) -> None:
|
|
with ThreadPoolExecutor(max_workers=16) as executor:
|
|
records = list(executor.map(lambda _: ledger.append(message()), range(16)))
|
|
assert ledger.count() == 1
|
|
assert {record.message_id for record in records} == {"message-a"}
|
|
|
|
def test_concurrent_distinct_appends_are_not_lost(ledger: MessageLedger) -> None:
|
|
def append(index: int) -> None:
|
|
ledger.append(message(event_id=f"event-{index}", message_id=f"message-{index}"))
|
|
with ThreadPoolExecutor(max_workers=16) as executor:
|
|
list(executor.map(append, range(16)))
|
|
assert ledger.count() == 16
|
|
|
|
def test_sqlite_failures_are_sanitized(ledger: MessageLedger) -> None:
|
|
ledger.database.write_bytes(b"not-a-sqlite-database")
|
|
with pytest.raises(ConflictError, match="ledger storage unavailable") as raised:
|
|
ledger.count()
|
|
assert not isinstance(raised.value, sqlite3.Error)
|
|
assert "sqlite" not in str(raised.value).lower()
|
|
assert raised.value.__cause__ is None
|
|
assert raised.value.__context__ is None
|