mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-02-04 09:56:47 +00:00
* brady bunch PRD/tasks * clean dead daily.co code * brady bunch prototype (no-mistakes) * brady bunch prototype (no-mistakes) review * self-review * daily poll time match (no-mistakes) * daily poll self-review (no-mistakes) * daily poll self-review (no-mistakes) * daily co doc * cleanup * cleanup * self-review (no-mistakes) * self-review (no-mistakes) * self-review * self-review * ui typefix * dupe calls error handling proper * daily reflector data model doc * logging style fix * migration merge --------- Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
375 lines
14 KiB
Python
375 lines
14 KiB
Python
"""
|
|
Integration tests for time-based meeting-to-recording matching.
|
|
|
|
Tests the critical path for matching Daily.co recordings to meetings when
|
|
API doesn't return instanceId.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
from reflector.db.meetings import meetings_controller
|
|
from reflector.db.rooms import rooms_controller
|
|
|
|
|
|
@pytest.fixture
|
|
async def test_room():
|
|
"""Create a test room for meetings."""
|
|
room = await rooms_controller.add(
|
|
name="test-room-time",
|
|
user_id="test-user-id",
|
|
zulip_auto_post=False,
|
|
zulip_stream="",
|
|
zulip_topic="",
|
|
is_locked=False,
|
|
room_mode="normal",
|
|
recording_type="cloud",
|
|
recording_trigger="automatic",
|
|
is_shared=False,
|
|
platform="daily",
|
|
)
|
|
return room
|
|
|
|
|
|
@pytest.fixture
|
|
def base_time():
|
|
"""Fixed timestamp for deterministic tests."""
|
|
return datetime(2026, 1, 14, 9, 0, 0, tzinfo=timezone.utc)
|
|
|
|
|
|
class TestTimeBasedMatching:
|
|
"""Test get_by_room_name_and_time() matching logic."""
|
|
|
|
async def test_exact_time_match(self, test_room, base_time):
|
|
"""Recording timestamp exactly matches meeting start_date."""
|
|
meeting = await meetings_controller.create(
|
|
id="meeting-exact",
|
|
room_name="daily-test-20260114090000",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time,
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
result = await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-test-20260114090000",
|
|
recording_start=base_time,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.id == meeting.id
|
|
|
|
async def test_recording_slightly_after_meeting_start(self, test_room, base_time):
|
|
"""Recording started 1 minute after meeting (participants joined late)."""
|
|
meeting = await meetings_controller.create(
|
|
id="meeting-late",
|
|
room_name="daily-test-20260114090100",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time,
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
recording_start = base_time + timedelta(minutes=1)
|
|
|
|
result = await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-test-20260114090100",
|
|
recording_start=recording_start,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.id == meeting.id
|
|
|
|
async def test_duplicate_room_names_picks_closest(self, test_room, base_time):
|
|
"""
|
|
Two meetings with same room_name (duplicate/race condition).
|
|
Should pick closest by timestamp.
|
|
"""
|
|
meeting1 = await meetings_controller.create(
|
|
id="meeting-1-first",
|
|
room_name="daily-duplicate-room",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time,
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
meeting2 = await meetings_controller.create(
|
|
id="meeting-2-second",
|
|
room_name="daily-duplicate-room", # Same room_name!
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time + timedelta(seconds=0.99), # 0.99s later
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
# Recording started 0.5s after meeting1
|
|
# Distance: meeting1 = 0.5s, meeting2 = 0.49s → meeting2 is closer
|
|
recording_start = base_time + timedelta(seconds=0.5)
|
|
|
|
result = await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-duplicate-room",
|
|
recording_start=recording_start,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.id == meeting2.id # meeting2 is closer (0.49s vs 0.5s)
|
|
|
|
async def test_outside_time_window_returns_none(self, test_room, base_time):
|
|
"""Recording outside 1-week window returns None."""
|
|
await meetings_controller.create(
|
|
id="meeting-old",
|
|
room_name="daily-test-old",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time,
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
# Recording 8 days later (outside 7-day window)
|
|
recording_start = base_time + timedelta(days=8)
|
|
|
|
result = await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-test-old",
|
|
recording_start=recording_start,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
assert result is None
|
|
|
|
async def test_tie_breaker_deterministic(self, test_room, base_time):
|
|
"""When time delta identical, tie-breaker by meeting.id is deterministic."""
|
|
meeting_z = await meetings_controller.create(
|
|
id="zzz-last-uuid",
|
|
room_name="daily-test-tie",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time,
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
meeting_a = await meetings_controller.create(
|
|
id="aaa-first-uuid",
|
|
room_name="daily-test-tie",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time, # Exact same start_date
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
result = await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-test-tie",
|
|
recording_start=base_time,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
assert result is not None
|
|
# Tie-breaker: lexicographically first UUID
|
|
assert result.id == "aaa-first-uuid"
|
|
|
|
async def test_timezone_naive_datetime_raises(self, test_room, base_time):
|
|
"""Timezone-naive datetime raises ValueError."""
|
|
await meetings_controller.create(
|
|
id="meeting-tz",
|
|
room_name="daily-test-tz",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time,
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
# Naive datetime (no timezone)
|
|
naive_dt = datetime(2026, 1, 14, 9, 0, 0)
|
|
|
|
with pytest.raises(ValueError, match="timezone-aware"):
|
|
await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-test-tz",
|
|
recording_start=naive_dt,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
async def test_one_week_boundary_after_included(self, test_room, base_time):
|
|
"""Meeting 1-week AFTER recording is included (window_end boundary)."""
|
|
meeting_time = base_time + timedelta(hours=168)
|
|
|
|
await meetings_controller.create(
|
|
id="meeting-boundary-after",
|
|
room_name="daily-test-boundary-after",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=meeting_time,
|
|
end_date=meeting_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
result = await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-test-boundary-after",
|
|
recording_start=base_time,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.id == "meeting-boundary-after"
|
|
|
|
async def test_one_week_boundary_before_included(self, test_room, base_time):
|
|
"""Meeting 1-week BEFORE recording is included (window_start boundary)."""
|
|
meeting_time = base_time - timedelta(hours=168)
|
|
|
|
await meetings_controller.create(
|
|
id="meeting-boundary-before",
|
|
room_name="daily-test-boundary-before",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=meeting_time,
|
|
end_date=meeting_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
result = await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-test-boundary-before",
|
|
recording_start=base_time,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.id == "meeting-boundary-before"
|
|
|
|
async def test_recording_before_meeting_start(self, test_room, base_time):
|
|
"""Recording started before meeting (clock skew or early join)."""
|
|
await meetings_controller.create(
|
|
id="meeting-early",
|
|
room_name="daily-test-early",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time,
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
recording_start = base_time - timedelta(minutes=2)
|
|
|
|
result = await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-test-early",
|
|
recording_start=recording_start,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.id == "meeting-early"
|
|
|
|
async def test_mixed_inside_outside_window(self, test_room, base_time):
|
|
"""Multiple meetings, only one inside window - returns the inside one."""
|
|
await meetings_controller.create(
|
|
id="meeting-old",
|
|
room_name="daily-test-mixed",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time - timedelta(days=10),
|
|
end_date=base_time - timedelta(days=10, hours=-1),
|
|
room=test_room,
|
|
)
|
|
|
|
await meetings_controller.create(
|
|
id="meeting-inside",
|
|
room_name="daily-test-mixed",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time - timedelta(days=2),
|
|
end_date=base_time - timedelta(days=2, hours=-1),
|
|
room=test_room,
|
|
)
|
|
|
|
await meetings_controller.create(
|
|
id="meeting-future",
|
|
room_name="daily-test-mixed",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time + timedelta(days=10),
|
|
end_date=base_time + timedelta(days=10, hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
result = await meetings_controller.get_by_room_name_and_time(
|
|
room_name="daily-test-mixed",
|
|
recording_start=base_time,
|
|
time_window_hours=168,
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.id == "meeting-inside"
|
|
|
|
|
|
class TestAtomicCloudRecordingUpdate:
|
|
"""Test atomic update prevents race conditions."""
|
|
|
|
async def test_first_update_succeeds(self, test_room, base_time):
|
|
"""First call to set_cloud_recording_if_missing succeeds."""
|
|
meeting = await meetings_controller.create(
|
|
id="meeting-atomic-1",
|
|
room_name="daily-test-atomic",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time,
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
success = await meetings_controller.set_cloud_recording_if_missing(
|
|
meeting_id=meeting.id,
|
|
s3_key="first-s3-key",
|
|
duration=100,
|
|
)
|
|
|
|
assert success is True
|
|
|
|
updated = await meetings_controller.get_by_id(meeting.id)
|
|
assert updated.daily_composed_video_s3_key == "first-s3-key"
|
|
assert updated.daily_composed_video_duration == 100
|
|
|
|
async def test_second_update_fails_atomically(self, test_room, base_time):
|
|
"""Second call to update same meeting doesn't overwrite (atomic check)."""
|
|
meeting = await meetings_controller.create(
|
|
id="meeting-atomic-2",
|
|
room_name="daily-test-atomic2",
|
|
room_url="https://example.daily.co/test",
|
|
host_room_url="https://example.daily.co/test?t=host",
|
|
start_date=base_time,
|
|
end_date=base_time + timedelta(hours=1),
|
|
room=test_room,
|
|
)
|
|
|
|
success1 = await meetings_controller.set_cloud_recording_if_missing(
|
|
meeting_id=meeting.id,
|
|
s3_key="first-s3-key",
|
|
duration=100,
|
|
)
|
|
|
|
assert success1 is True
|
|
|
|
after_first = await meetings_controller.get_by_id(meeting.id)
|
|
assert after_first.daily_composed_video_s3_key == "first-s3-key"
|
|
|
|
success2 = await meetings_controller.set_cloud_recording_if_missing(
|
|
meeting_id=meeting.id,
|
|
s3_key="bucket/path/should-not-overwrite",
|
|
duration=200,
|
|
)
|
|
|
|
assert success2 is False
|
|
|
|
final = await meetings_controller.get_by_id(meeting.id)
|
|
assert final.daily_composed_video_s3_key == "first-s3-key"
|
|
assert final.daily_composed_video_duration == 100
|