nte-exporter/tests/test_uid_compatibility.py

174 lines
8.2 KiB
Python

from tests.support import * # noqa: F401,F403
class UidCompatibilityTests(unittest.TestCase):
def test_monopoly_page_split_timestamp_variants_complete_ten_pull(self):
rows = [self._synthetic_row(1, "aa", "dice") for _ in range(5)]
rows += [self._synthetic_row(2, "bb", "dice") for _ in range(5)]
for row in rows:
row["timestamp_decoded"] = "2026-07-11 07:21:22"
annotated = annotate_groups(rows)
self.assertEqual([row["timestamp_group_ordinal"] for row in annotated], list(range(10)))
self.assertEqual({row["timestamp_raw_hex"] for row in annotated}, {"aa"})
def test_monopoly_singles_with_same_second_remain_separate(self):
rows = [self._synthetic_row(1, "aa", "dice"), self._synthetic_row(2, "bb", "dice")]
for row in rows:
row["timestamp_decoded"] = "2026-07-11 07:21:22"
annotated = annotate_groups(rows)
self.assertEqual([row["timestamp_group_ordinal"] for row in annotated], [0, 0])
self.assertEqual([row["timestamp_raw_hex"] for row in annotated], ["aa", "bb"])
def test_uid_source_matches_committed_network_fixture(self):
fixture = load_network_fixture()
rows = annotate_groups(fixture_session().build_rows("permanent"))
self.assertEqual(rows[0]["uid"], fixture["expected"]["permanent_first_uid"])
def test_uid_uses_pool_timestamp_and_ordinal_only(self):
row = {
"pool_group_id": "Lottery_LimitedCharacter",
"timestamp_raw_hex": "40e93247c3097b23",
"dice": 5,
"reward_key_hex": "10a58d957dd1a58dad95d17dc1c800",
"quantity": 50,
}
changed_content = {
**row,
"dice": 1,
"reward_key_hex": "98bdc9ad7dd9a5b99501",
"quantity": 1,
}
changed_pool = {**row, "pool_group_id": "Lottery_Permanent"}
self.assertEqual(make_uid(row, 0), "74a9ef4aacde549dfe8e8e7cc6ddd65b")
self.assertEqual(make_uid(changed_content, 0), make_uid(row, 0))
self.assertNotEqual(make_uid(changed_pool, 0), make_uid(row, 0))
self.assertNotEqual(make_uid(row, 1), make_uid(row, 0))
def test_pages_1_to_5_exports_every_row(self):
annotated = annotate_groups(fixture_session().build_rows("permanent"))
exported = [row for row in annotated if row["export_record"] is True]
# Every decoded row is exported; boundary groups are never dropped.
self.assertEqual(len(annotated), 25)
self.assertEqual(len(exported), 25)
@staticmethod
def _synthetic_row(page, timestamp_hex, result_type):
return {
"page": page,
"timestamp_raw_hex": timestamp_hex,
"timestamp_decoded": f"ts-{timestamp_hex}",
"result_type": result_type,
"dice": 4 if result_type == "dice" else 0,
"reward_key_hex": "10a58d9539bdc9b585b101",
"quantity": 1,
}
def test_oldest_group_with_partial_dice_count_exports_without_warning(self):
rows = [self._synthetic_row(1, "aa", "dice") for _ in range(5)]
rows += [self._synthetic_row(2, "bb", "dice") for _ in range(5)]
rows += [self._synthetic_row(3, "bb", "dice") for _ in range(4)]
rows += [self._synthetic_row(3, "bb", "points_gift")]
annotated = annotate_groups(rows)
exported = [row for row in annotated if row["export_record"] is True]
# Oldest group is a partially captured 10-pull on a full final page. Its
# captured prefix is ordinal-stable, so it is exported with stable UIDs.
self.assertEqual(len(exported), 15)
oldest = [row for row in annotated if row["timestamp_raw_hex"] == "bb"]
self.assertTrue(all(row["uid"] for row in oldest))
self.assertTrue(all(row["uid_status"] == "stable" for row in oldest))
self.assertEqual([row["timestamp_group_ordinal"] for row in oldest], list(range(10)))
def test_incomplete_oldest_prefix_keeps_stable_uids(self):
full = [self._synthetic_row(1, "aa", "dice") for _ in range(5)]
full += [self._synthetic_row(2, "bb", "dice") for _ in range(3)]
full += [self._synthetic_row(3, "bb", "dice") for _ in range(2)]
truncated = [r for r in full if r["page"] in (1, 2)]
full_rows = annotate_groups([dict(r) for r in full])
trunc_rows = annotate_groups([dict(r) for r in truncated])
full_uids = [r["uid"] for r in full_rows if r["timestamp_raw_hex"] == "bb"][:3]
trunc_uids = [r["uid"] for r in trunc_rows if r["timestamp_raw_hex"] == "bb"]
# Capturing only the first 3 of a 5-record oldest group yields the same
# UIDs those rows have in the full capture.
self.assertEqual(len(trunc_uids), 3)
self.assertEqual(trunc_uids, full_uids)
def test_oldest_group_with_ten_dice_exports_on_full_final_page(self):
rows = [self._synthetic_row(1, "aa", "dice") for _ in range(5)]
rows += [self._synthetic_row(2, "bb", "dice") for _ in range(5)]
rows += [self._synthetic_row(3, "bb", "dice") for _ in range(5)]
annotated = annotate_groups(rows)
exported = [row for row in annotated if row["export_record"] is True]
self.assertEqual(len(exported), 15)
def test_run_selection_anchors_to_page_1_and_keeps_newest(self):
# Page 2's response was lost: captured pages 1, 3, 4, 5.
pairs = [(p, p * 2, 0, 0, 0, 0, b"", "permanent") for p in (1, 3, 4, 5)]
run, warnings = select_continuous_run_from_page_1(pairs)
# The page-1 run (just page 1, the newest history) is kept; later pages are
# ignored with a gap warning, never silently discarding page 1.
self.assertEqual([p[0] for p in run], [1])
self.assertEqual(len(warnings), 1)
self.assertEqual(warnings[0]["code"], "PAGE_GAP_DETECTED")
self.assertEqual(warnings[0]["ignored_pages"], [3, 4, 5])
def test_run_selection_warns_when_page_1_missing(self):
pairs = [(p, p * 2, 0, 0, 0, 0, b"", "permanent") for p in (3, 4, 5)]
run, warnings = select_continuous_run_from_page_1(pairs)
self.assertEqual([p[0] for p in run], [3, 4, 5])
self.assertEqual(warnings[0]["code"], "DID_NOT_START_AT_PAGE_1")
def test_committed_network_scan_exports_all_rows(self):
fixture = load_network_fixture()
annotated = annotate_groups(fixture_session().build_rows("permanent"))
exported = [row for row in annotated if row["export_record"] is True]
self.assertEqual(len(annotated), fixture["expected"]["permanent_records"])
self.assertEqual(len(exported), fixture["expected"]["permanent_records"])
def test_monopoly_uid_compatibility_vectors_are_frozen(self):
vectors = [
("Lottery_Permanent", "0000000000000000", 0, "5da57468adb23fcfde530fd849a25767"),
("Lottery_Permanent", "00a243eb689b7a23", 0, "8f9fb9bc92a867f6f41e5839e94c7991"),
("Lottery_Permanent", "00a243eb689b7a23", 1, "6a0bea71d091b9b7063780fcc86238d5"),
("Lottery_LimitedCharacter", "00a243eb689b7a23", 0, "b8f5d3c27315c70ceafbe8eb2bab0fc2"),
("Lottery_LimitedCharacter", "ffffffffffffffff", 9, "66916e1f077fdf7664f0f0edc4c64eb8"),
]
for pool_group_id, timestamp_raw_hex, ordinal, expected_uid in vectors:
with self.subTest(
pool_group_id=pool_group_id,
timestamp=timestamp_raw_hex,
ordinal=ordinal,
):
row = {
"pool_group_id": pool_group_id,
"timestamp_raw_hex": timestamp_raw_hex,
}
self.assertEqual(make_uid(row, ordinal), expected_uid)
def test_arc_uid_compatibility_vectors_are_frozen(self):
vectors = [
("0000000000000000", 0, "faa0b07cefd71758d4a8d64bc2a2f78e"),
("00a243eb689b7a23", 0, "7a7ef75281fcced043f691d269477cfa"),
("00a243eb689b7a23", 1, "f798c076ea1d137a08fe62df859e8a7f"),
("ffffffffffffffff", 9, "d114cc6c313a15f55ca12910801d78ea"),
]
for timestamp_raw_hex, ordinal, expected_uid in vectors:
with self.subTest(timestamp=timestamp_raw_hex, ordinal=ordinal):
self.assertEqual(make_arc_uid(timestamp_raw_hex, ordinal), expected_uid)