File size: 23,739 Bytes
23f9faf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9765f33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23f9faf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9765f33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23f9faf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505

"""Unit tests for the multi-round balanced assignment + completion-code
feature.

Runs offline (no network / no huggingface_hub calls): assignment.py's core
functions are pure, and the round-lifecycle integration tests patch main.py's
HF-facing I/O functions with an in-memory fake store.

Run with:  python -m unittest backend.test_assignment   (from repo root)
       or: python -m unittest test_assignment            (from backend/)
"""

import json
import os
import random
import sys
import unittest
from datetime import datetime, timedelta, timezone
from unittest import mock

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

import assignment  # noqa: E402


def make_record(user, video_id, created_at, extra=None):
    record = {
        "id": "x",
        "video_id": video_id,
        "user": user,
        "created_at": created_at,
        "annotations": {"responses": {}},
    }
    if extra:
        record.update(extra)
    return record


def iso(dt):
    return dt.isoformat().replace("+00:00", "Z")


class DedupeLatestTests(unittest.TestCase):
    def test_existing_annotations_preserved_and_counted(self):
        records = [
            make_record("Adi", "v1", "2026-01-01T00:00:00Z"),
            make_record("Adi", "v2", "2026-01-01T00:00:00Z"),
            make_record("youngsun", "v1", "2026-01-01T00:00:00Z"),
        ]
        original_len = len(records)

        deduped, skipped = assignment.dedupe_latest(records)

        self.assertEqual(len(records), original_len, "dedupe_latest must not mutate its input")
        self.assertEqual(skipped, 0)
        self.assertEqual(len(deduped), 3)
        by_video = assignment.completed_by_video(deduped)
        self.assertEqual(by_video["v1"], {"adi", "youngsun"})
        self.assertEqual(by_video["v2"], {"adi"})

    def test_same_annotator_same_video_counted_once_latest_kept(self):
        records = [
            make_record("Adi", "v1", "2026-01-01T00:00:00Z", {"annotations": {"marker": "old"}}),
            make_record("Adi", "v1", "2026-01-02T00:00:00Z", {"annotations": {"marker": "new"}}),
            make_record("adi", "v1", "2026-01-03T00:00:00Z", {"annotations": {"marker": "newest"}}),
        ]

        deduped, skipped = assignment.dedupe_latest(records)

        self.assertEqual(skipped, 0)
        self.assertEqual(len(deduped), 1, "same annotator + same video, across case variants, is one record")
        self.assertEqual(deduped[("adi", "v1")]["annotations"]["marker"], "newest")

    def test_malformed_or_partial_records_skipped_not_counted(self):
        records = [
            make_record("Adi", "v1", "2026-01-01T00:00:00Z"),
            {"id": "y", "video_id": "v2", "user": "Adi"},  # missing created_at (autosave/reserve-like)
            {"id": "z", "user": "Adi", "created_at": "2026-01-01T00:00:00Z"},  # missing video_id
            {"id": "w", "video_id": "v3", "user": "", "created_at": "2026-01-01T00:00:00Z"},  # empty user
            {"id": "q", "video_id": "v4", "user": "Adi", "created_at": "not-a-date"},  # unparsable timestamp
        ]

        deduped, skipped = assignment.dedupe_latest(records)

        self.assertEqual(len(deduped), 1)
        self.assertEqual(skipped, 4)


class SavedResponsesForRoundTests(unittest.TestCase):
    def test_returns_responses_only_for_this_annotator_and_this_rounds_videos(self):
        records = [
            make_record("Adi", "v1", "2026-01-01T00:00:00Z", {"annotations": {"responses": {"marker": "adi-v1"}}}),
            make_record("Adi", "v2", "2026-01-01T00:00:00Z", {"annotations": {"responses": {"marker": "adi-v2"}}}),
            make_record(
                "youngsun", "v1", "2026-01-01T00:00:00Z", {"annotations": {"responses": {"marker": "youngsun-v1"}}}
            ),
        ]
        deduped, _skipped = assignment.dedupe_latest(records)

        result = assignment.saved_responses_for_round(deduped, "adi", ["v1", "v2", "v3"])

        self.assertEqual(result, {"v1": {"marker": "adi-v1"}, "v2": {"marker": "adi-v2"}})

    def test_excludes_videos_outside_the_given_round(self):
        records = [
            make_record("Adi", "v9", "2026-01-01T00:00:00Z", {"annotations": {"responses": {"marker": "old-round"}}})
        ]
        deduped, _skipped = assignment.dedupe_latest(records)

        result = assignment.saved_responses_for_round(deduped, "adi", ["v1", "v2"])

        self.assertEqual(result, {}, "a completion from a prior round must not leak into this round's resume state")

    def test_empty_deduped_returns_empty_dict(self):
        self.assertEqual(assignment.saved_responses_for_round({}, "adi", ["v1"]), {})


class PickBalancedTests(unittest.TestCase):
    def test_prefers_lowest_coverage(self):
        coverage = {"v1": 4, "v2": 0, "v3": 2}
        picked = assignment.pick_balanced(list(coverage), coverage.get, 2, random.Random(0), target=5)
        self.assertEqual(set(picked), {"v2", "v3"})

    def test_excludes_at_or_over_target_when_enough_under_target_remain(self):
        coverage = {"v1": 5, "v2": 5, "v3": 1}
        picked = assignment.pick_balanced(list(coverage), coverage.get, 1, random.Random(0), target=5)
        self.assertEqual(picked, ["v3"])

    def test_falls_back_to_at_target_videos_when_not_enough_under_target(self):
        coverage = {"v1": 5, "v2": 5, "v3": 1}
        picked = assignment.pick_balanced(list(coverage), coverage.get, 3, random.Random(0), target=5)
        self.assertEqual(set(picked), {"v1", "v2", "v3"})

    def test_never_returns_more_than_requested_or_duplicates(self):
        coverage = {f"v{i}": i % 3 for i in range(30)}
        picked = assignment.pick_balanced(list(coverage), coverage.get, 7, random.Random(1), target=5)
        self.assertEqual(len(picked), 7)
        self.assertEqual(len(set(picked)), 7)


class CompletionCodeTests(unittest.TestCase):
    def test_format(self):
        code = assignment.generate_completion_code()
        self.assertTrue(code.startswith("T2AV-"))
        suffix = code[len("T2AV-"):]
        self.assertEqual(len(suffix), 12)
        self.assertTrue(all(c in assignment.COMPLETION_CODE_ALPHABET for c in suffix))

    def test_codes_are_not_trivially_repeated(self):
        codes = {assignment.generate_completion_code() for _ in range(200)}
        self.assertEqual(len(codes), 200, "200 draws from a 36^12 space should never collide")


class BuildRoundTests(unittest.TestCase):
    def test_always_excludes_already_completed_never_seeds_with_them(self):
        """Core policy change: a round is exactly N NEW videos, never padded
        with history, regardless of how much history exists."""
        catalog = [f"v{i}" for i in range(1, 201)]  # 200 videos, like production
        already_completed = set(catalog[:51])  # mirrors real production data (Adi: 51 completed)

        round_record = assignment.build_round(
            "adi", "Adi", catalog, already_completed, {}, {}, random.Random(0), round_number=1,
        )

        self.assertEqual(round_record["actual_size"], 20)
        self.assertEqual(len(round_record["video_ids"]), 20)
        self.assertEqual(
            already_completed.intersection(round_record["video_ids"]), set(),
            "none of the 51 previously completed videos may appear in the new round",
        )
        self.assertEqual(len(set(round_record["video_ids"])), 20, "no duplicates within the round")
        self.assertEqual(round_record["round_number"], 1)
        self.assertEqual(round_record["requested_size"], 20)

    def test_one_historical_completion_gets_twenty_new_not_nineteen(self):
        catalog = [f"v{i}" for i in range(1, 201)]
        already_completed = {"v1"}

        round_record = assignment.build_round(
            "youngsun", "youngsun_0715", catalog, already_completed, {}, {}, random.Random(0), round_number=1,
        )

        self.assertEqual(round_record["actual_size"], 20)
        self.assertNotIn("v1", round_record["video_ids"])

    def test_brand_new_annotator_gets_exactly_twenty(self):
        catalog = [f"v{i}" for i in range(1, 201)]
        round_record = assignment.build_round(
            "new", "New", catalog, set(), {}, {}, random.Random(0), round_number=1,
        )
        self.assertEqual(round_record["actual_size"], 20)

    def test_reduced_size_when_not_enough_eligible_remain(self):
        catalog = [f"v{i}" for i in range(1, 26)]  # only 25 videos total
        already_completed = set(catalog[:15])  # 15 done, only 10 eligible left

        round_record = assignment.build_round(
            "x", "X", catalog, already_completed, {}, {}, random.Random(0), round_number=1,
        )

        self.assertEqual(round_record["actual_size"], 10)
        self.assertEqual(round_record["requested_size"], 20)
        self.assertEqual(len(round_record["video_ids"]), 10)
        self.assertEqual(already_completed.intersection(round_record["video_ids"]), set())

    def test_second_round_excludes_first_rounds_completions_too(self):
        """After Adi finishes 51 (history) + 20 (round 1) = 71, round 2 must
        exclude all 71, selecting from the remaining 129."""
        catalog = [f"v{i}" for i in range(1, 201)]
        historical = set(catalog[:51])

        round1 = assignment.build_round(
            "adi", "Adi", catalog, historical, {}, {}, random.Random(0), round_number=1,
        )
        after_round1 = historical | set(round1["video_ids"])
        self.assertEqual(len(after_round1), 71)

        round2 = assignment.build_round(
            "adi", "Adi", catalog, after_round1, {}, {}, random.Random(1), round_number=2,
        )

        self.assertEqual(round2["actual_size"], 20)
        self.assertEqual(after_round1.intersection(round2["video_ids"]), set())
        remaining_pool_size = len(catalog) - len(after_round1)
        self.assertEqual(remaining_pool_size, 129)
        self.assertEqual(round2["round_number"], 2)

    def test_existing_completions_affect_new_round_priority(self):
        catalog = ["v1", "v2", "v3"]
        completed_by_video_map = {"v1": {"p1", "p2", "p3", "p4"}}  # coverage 4, still < target 5

        round_record = assignment.build_round(
            "new", "New", catalog, set(), completed_by_video_map, {}, random.Random(0),
            round_number=1, videos_per_annotator=2,
        )

        self.assertNotIn("v1", round_record["video_ids"], "v2/v3 have lower coverage and must be preferred")
        self.assertEqual(set(round_record["video_ids"]), {"v2", "v3"})


class ReservedByVideoTests(unittest.TestCase):
    def test_counts_open_uncompleted_unexpired_reservations(self):
        now = datetime(2026, 1, 10, tzinfo=timezone.utc)
        assignments = [
            {"annotator_id": "a", "video_ids": ["v1", "v2"], "created_at": "2026-01-09T00:00:00Z"},
            {"annotator_id": "b", "video_ids": ["v1"], "created_at": "2026-01-09T00:00:00Z"},
        ]
        completed_by_annotator = {"a": {"v2"}}

        reserved = assignment.reserved_by_video(assignments, completed_by_annotator, now, ttl_seconds=7 * 24 * 3600)

        self.assertEqual(reserved.get("v1"), 2)
        self.assertNotIn("v2", reserved)

    def test_excludes_expired_reservations_without_needing_to_delete_anything(self):
        now = datetime(2026, 1, 10, tzinfo=timezone.utc)
        assignments = [{"annotator_id": "a", "video_ids": ["v1"], "created_at": "2025-01-01T00:00:00Z"}]

        reserved = assignment.reserved_by_video(assignments, {}, now, ttl_seconds=24 * 3600)

        self.assertEqual(reserved.get("v1", 0), 0)

    def test_excludes_the_requesting_annotator_from_their_own_reservations(self):
        now = datetime(2026, 1, 10, tzinfo=timezone.utc)
        assignments = [{"annotator_id": "a", "video_ids": ["v1"], "created_at": "2026-01-09T00:00:00Z"}]

        reserved = assignment.reserved_by_video(assignments, {}, now, ttl_seconds=24 * 3600, exclude_annotator="a")

        self.assertEqual(reserved.get("v1", 0), 0)


class SequentialBalanceTests(unittest.TestCase):
    def test_sequential_rounds_stay_reasonably_balanced(self):
        catalog = [f"v{i}" for i in range(1, 81)]  # 80 videos
        reserved_by_video_map = {}
        rng = random.Random(42)

        for i in range(10):  # 10 annotators * 20 slots = 200 slots over 80 videos
            round_record = assignment.build_round(
                f"person{i}", f"person{i}", catalog, set(), {}, reserved_by_video_map, rng, round_number=1,
            )
            for video_id in round_record["video_ids"]:
                reserved_by_video_map[video_id] = reserved_by_video_map.get(video_id, 0) + 1

        counts = list(reserved_by_video_map.values())
        self.assertEqual(len(reserved_by_video_map), 80, "every video should have been touched")
        self.assertLessEqual(max(counts) - min(counts), 2, "balancing should keep coverage tight")


class FakeRepoStore:
    """Stands in for the annotations dataset repo's assignments/ and
    completions/ prefixes."""

    def __init__(self):
        self.files = {}

    def read(self, path):
        return self.files.get(path)

    def write_kwargs(self, **kwargs):
        path = kwargs["path_in_repo"]
        content = json.loads(kwargs["path_or_fileobj"].getvalue().decode("utf-8"))
        self.files[path] = content

    def list_active_round_records(self):
        records = []
        for path, pointer in self.files.items():
            if not path.endswith("/current.json"):
                continue
            annotator_id = path.split("/")[1]
            round_path = f"assignments/{annotator_id}/rounds/{pointer['assignment_id']}.json"
            completion_path = f"completions/{annotator_id}/{pointer['assignment_id']}.json"
            round_record = self.files.get(round_path)
            if round_record is None or completion_path in self.files:
                continue
            records.append(round_record)
        return records


class RoundLifecycleIntegrationTests(unittest.TestCase):
    """Exercises main.get_or_create_current_round() with every HF network
    call replaced by an in-memory fake, so this stays fully offline."""

    def setUp(self):
        import main as backend_main

        self.backend_main = backend_main
        self.store = FakeRepoStore()
        self.catalog = [f"v{i}" for i in range(1, 201)]  # 200 videos, like production
        self.completed_records = []  # raw annotation records fed to both list_annotation_records variants

        patches = [
            mock.patch.object(backend_main, "_read_json_from_repo", side_effect=self.store.read),
            # Both the cached and fresh variants must be mocked - the completion
            # check in get_or_create_current_round() deliberately bypasses the
            # cache (fresh=True) so a real annotator sees their code promptly;
            # leaving list_annotation_records() unmocked would silently fall
            # through to a real network call here.
            mock.patch.object(
                backend_main, "list_annotation_records_cached", side_effect=lambda: self.completed_records
            ),
            mock.patch.object(
                backend_main, "list_annotation_records", side_effect=lambda: self.completed_records
            ),
            mock.patch.object(
                backend_main, "list_active_round_records", side_effect=self.store.list_active_round_records
            ),
            mock.patch.object(backend_main, "ensure_dataset_exists", return_value=None),
            mock.patch.object(backend_main, "ensure_dataset_configured", return_value=None),
            mock.patch.object(backend_main, "_current_catalog_video_ids", return_value=self.catalog),
            mock.patch.object(backend_main, "upload_with_retry", side_effect=self.store.write_kwargs),
        ]
        for patcher in patches:
            patcher.start()
            self.addCleanup(patcher.stop)

    def _complete_round(self, annotator_raw, round_record):
        """Simulates the annotator finishing every video in a round by
        appending matching annotation records - exactly what save_annotation
        would have produced."""
        now = datetime.now(timezone.utc)
        for video_id in round_record["video_ids"]:
            self.completed_records.append(make_record(annotator_raw, video_id, iso(now)))

    def test_brand_new_annotator_gets_round_one_of_twenty(self):
        result = self.backend_main.get_or_create_current_round("Scratch Tester")
        self.assertEqual(result["status"], "in_progress")
        self.assertEqual(result["round_number"], 1)
        self.assertEqual(len(result["video_ids"]), 20)
        self.assertEqual(len(set(result["video_ids"])), 20)

    def test_refresh_while_incomplete_returns_identical_round(self):
        first = self.backend_main.get_or_create_current_round("Scratch Tester")
        second = self.backend_main.get_or_create_current_round("Scratch Tester")
        self.assertEqual(first["video_ids"], second["video_ids"])
        self.assertEqual(first["assignment_id"], second["assignment_id"])

    def test_no_completion_code_at_nineteen_of_twenty(self):
        first = self.backend_main.get_or_create_current_round("Scratch Tester")
        for video_id in first["video_ids"][:19]:
            self.completed_records.append(make_record("Scratch Tester", video_id, iso(datetime.now(timezone.utc))))

        result = self.backend_main.get_or_create_current_round("Scratch Tester")

        self.assertEqual(result["status"], "in_progress")
        self.assertNotIn("completion_code", result)

    def test_brand_new_round_has_no_saved_annotations(self):
        result = self.backend_main.get_or_create_current_round("Scratch Tester")
        self.assertEqual(result["saved_annotations"], {})

    def test_resume_returns_saved_responses_for_completed_videos_only(self):
        first = self.backend_main.get_or_create_current_round("Scratch Tester")
        completed_ids = first["video_ids"][:3]
        for video_id in completed_ids:
            self.completed_records.append(
                make_record(
                    "Scratch Tester",
                    video_id,
                    iso(datetime.now(timezone.utc)),
                    {"annotations": {"responses": {"video": {"tcRel": {"label": "PASS", "rationale": ""}}}}},
                )
            )

        result = self.backend_main.get_or_create_current_round("Scratch Tester")

        self.assertEqual(result["status"], "in_progress")
        self.assertEqual(set(result["saved_annotations"].keys()), set(completed_ids))
        for video_id in completed_ids:
            self.assertEqual(result["saved_annotations"][video_id]["video"]["tcRel"]["label"], "PASS")

    def test_completion_code_persisted_once_at_twenty_of_twenty(self):
        first = self.backend_main.get_or_create_current_round("Scratch Tester")
        self._complete_round("Scratch Tester", first)

        result_a = self.backend_main.get_or_create_current_round("Scratch Tester")
        result_b = self.backend_main.get_or_create_current_round("Scratch Tester")

        self.assertEqual(result_a["status"], "completed")
        self.assertTrue(result_a["completion_code"].startswith("T2AV-"))
        self.assertEqual(result_a["completion_code"], result_b["completion_code"], "code must be stable on refresh")
        completion_files = [p for p in self.store.files if p.startswith("completions/")]
        self.assertEqual(len(completion_files), 1, "completion record written exactly once")

    def test_within_grace_period_shows_same_completed_round_not_a_new_one(self):
        first = self.backend_main.get_or_create_current_round("Scratch Tester")
        self._complete_round("Scratch Tester", first)
        self.backend_main.get_or_create_current_round("Scratch Tester")  # triggers completion-record creation

        # Directly age the completion record to just inside the grace window.
        completion_path = f"completions/scratch-tester/{first['assignment_id']}.json"
        recent = datetime.now(timezone.utc) - timedelta(
            seconds=assignment.COMPLETION_GRACE_PERIOD_SECONDS - 30
        )
        self.store.files[completion_path]["completed_at"] = iso(recent)

        result = self.backend_main.get_or_create_current_round("Scratch Tester")

        self.assertEqual(result["status"], "completed")
        self.assertEqual(result["round_number"], 1)

    def test_past_grace_period_auto_creates_next_round_with_no_extra_call(self):
        first = self.backend_main.get_or_create_current_round("Scratch Tester")
        self._complete_round("Scratch Tester", first)
        self.backend_main.get_or_create_current_round("Scratch Tester")  # triggers completion-record creation

        # Directly age the completion record past the grace window - simulates "came back later".
        completion_path = f"completions/scratch-tester/{first['assignment_id']}.json"
        stale = datetime.now(timezone.utc) - timedelta(
            seconds=assignment.COMPLETION_GRACE_PERIOD_SECONDS + 60
        )
        self.store.files[completion_path]["completed_at"] = iso(stale)

        result = self.backend_main.get_or_create_current_round("Scratch Tester")

        self.assertEqual(result["status"], "in_progress")
        self.assertEqual(result["round_number"], 2)
        self.assertEqual(set(result["video_ids"]).intersection(first["video_ids"]), set())

    def test_never_assigns_the_same_video_twice_across_rounds(self):
        first = self.backend_main.get_or_create_current_round("Scratch Tester")
        self._complete_round("Scratch Tester", first)
        self.backend_main.get_or_create_current_round("Scratch Tester")

        completion_path = f"completions/scratch-tester/{first['assignment_id']}.json"
        stale = datetime.now(timezone.utc) - timedelta(seconds=assignment.COMPLETION_GRACE_PERIOD_SECONDS + 60)
        self.store.files[completion_path]["completed_at"] = iso(stale)

        second = self.backend_main.get_or_create_current_round("Scratch Tester")

        all_seen = first["video_ids"] + second["video_ids"]
        self.assertEqual(len(all_seen), len(set(all_seen)))

    def test_different_annotators_each_get_their_own_round(self):
        a = self.backend_main.get_or_create_current_round("Person A")
        b = self.backend_main.get_or_create_current_round("Person B")
        self.assertEqual(len(a["video_ids"]), 20)
        self.assertEqual(len(b["video_ids"]), 20)
        round_files = [p for p in self.store.files if "/rounds/" in p]
        self.assertEqual(len(round_files), 2)

    def test_simultaneous_requests_do_not_create_duplicate_rounds(self):
        """The lock fully serializes get_or_create_current_round, so we
        simulate "simultaneous" by calling it back-to-back and asserting only
        one round/pointer combination was ever written - the real concurrency
        guarantee is exercised live against a scratch HF repo separately."""
        for _ in range(5):
            self.backend_main.get_or_create_current_round("Scratch Tester")
        round_files = [p for p in self.store.files if "/rounds/" in p]
        self.assertEqual(len(round_files), 1)

    def test_partial_records_never_mistakenly_counted_as_completed(self):
        first = self.backend_main.get_or_create_current_round("Scratch Tester")
        # An autosave/reservation-like record missing created_at for every video.
        for video_id in first["video_ids"]:
            self.completed_records.append({"id": "x", "video_id": video_id, "user": "Scratch Tester"})

        result = self.backend_main.get_or_create_current_round("Scratch Tester")

        self.assertEqual(result["status"], "in_progress")


if __name__ == "__main__":
    unittest.main()