150 lines
5.0 KiB
Python
150 lines
5.0 KiB
Python
import json
|
|
from copy import deepcopy
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from ai_platform.presentation.http.schemas import RunRequest
|
|
|
|
|
|
def test_snapshot_preserves_known_omni_metadata_and_array_order(valid_run_payload):
|
|
request = RunRequest.model_validate(valid_run_payload)
|
|
snapshot = request.snapshot
|
|
|
|
assert snapshot.version == "1.0.0"
|
|
assert snapshot.scope == "linked_contact_history"
|
|
assert snapshot.identity_context is not None
|
|
assert (
|
|
snapshot.identity_context.model_dump() == valid_run_payload["snapshot"]["identity_context"]
|
|
)
|
|
assert snapshot.trigger_metadata is not None
|
|
assert (
|
|
snapshot.trigger_metadata.model_dump() == valid_run_payload["snapshot"]["trigger_metadata"]
|
|
)
|
|
assert snapshot.conversations[0].channel == "webchat"
|
|
assert [message.message_id for message in snapshot.conversations[0].messages] == [
|
|
"message-2",
|
|
"message-1",
|
|
]
|
|
|
|
|
|
def test_message_order_remains_significant_to_canonical_fingerprint(valid_run_payload):
|
|
original = RunRequest.model_validate(valid_run_payload).model_dump(mode="json")
|
|
reordered_payload = deepcopy(valid_run_payload)
|
|
reordered_payload["snapshot"]["conversations"][0]["messages"].reverse()
|
|
reordered = RunRequest.model_validate(reordered_payload).model_dump(mode="json")
|
|
|
|
assert json.dumps(original, sort_keys=True, separators=(",", ":")) != json.dumps(
|
|
reordered, sort_keys=True, separators=(",", ":")
|
|
)
|
|
|
|
|
|
def test_conversation_order_is_preserved(valid_run_payload):
|
|
conversations = valid_run_payload["snapshot"]["conversations"]
|
|
conversations.append(
|
|
{
|
|
"conversation_id": "conversation-1",
|
|
"messages": [
|
|
{
|
|
"message_id": "older-message",
|
|
"sender_type": "contact",
|
|
"content": "Earlier in time, later in the array.",
|
|
"sent_at": "2025-12-30T10:00:00Z",
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
snapshot = RunRequest.model_validate(valid_run_payload).snapshot
|
|
|
|
assert [conversation.conversation_id for conversation in snapshot.conversations] == [
|
|
"conversation-2",
|
|
"conversation-1",
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("change_snapshot", "error"),
|
|
[
|
|
(
|
|
lambda snapshot: snapshot.update(
|
|
conversations=[
|
|
{"conversation_id": f"conversation-{index}", "messages": []}
|
|
for index in range(11)
|
|
]
|
|
),
|
|
"10",
|
|
),
|
|
(
|
|
lambda snapshot: snapshot["conversations"][0].update(
|
|
messages=[
|
|
{
|
|
"message_id": f"message-{index}",
|
|
"sender_type": "contact",
|
|
"content": "x",
|
|
"sent_at": "2025-12-31T10:00:00Z",
|
|
}
|
|
for index in range(51)
|
|
]
|
|
),
|
|
"50",
|
|
),
|
|
(
|
|
lambda snapshot: snapshot["conversations"][0]["messages"][0].update(
|
|
content="x" * 20_001
|
|
),
|
|
"20000",
|
|
),
|
|
(
|
|
lambda snapshot: snapshot["conversations"][0]["messages"][0].update(
|
|
sent_at="2025-12-31T10:00:00"
|
|
),
|
|
"timezone-aware",
|
|
),
|
|
(
|
|
lambda snapshot: snapshot["conversations"][0]["messages"][0].update(
|
|
sent_at="2025-09-30T12:00:00Z"
|
|
),
|
|
"90-day",
|
|
),
|
|
],
|
|
)
|
|
def test_snapshot_rejects_invalid_evidence(valid_run_payload, change_snapshot, error):
|
|
data = deepcopy(valid_run_payload)
|
|
change_snapshot(data["snapshot"])
|
|
|
|
with pytest.raises(ValidationError, match=error):
|
|
RunRequest.model_validate(data)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("target", "extra"),
|
|
[
|
|
("snapshot", "unsupported_snapshot_field"),
|
|
("lead_profile", "unsupported_profile_field"),
|
|
("conversation", "unsupported_conversation_field"),
|
|
("message", "unsupported_message_field"),
|
|
],
|
|
)
|
|
def test_snapshot_rejects_unmodeled_fields(valid_run_payload, target, extra):
|
|
snapshot = deepcopy(valid_run_payload)["snapshot"]
|
|
if target == "snapshot":
|
|
snapshot[extra] = "not silently discarded"
|
|
elif target == "lead_profile":
|
|
snapshot["lead_profile"][extra] = "not silently discarded"
|
|
elif target == "conversation":
|
|
snapshot["conversations"][0][extra] = "not silently discarded"
|
|
else:
|
|
snapshot["conversations"][0]["messages"][0][extra] = "not silently discarded"
|
|
|
|
with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
|
|
RunRequest.model_validate({**valid_run_payload, "snapshot": snapshot})
|
|
|
|
|
|
def test_snapshot_rejects_naive_capture_timestamp(valid_run_payload):
|
|
data = deepcopy(valid_run_payload)
|
|
data["snapshot"]["captured_at"] = "2026-01-01T12:00:00"
|
|
|
|
with pytest.raises(ValidationError, match="timezone-aware"):
|
|
RunRequest.model_validate(data)
|