Weekly Events-calendar curation routine and deterministic deduper
This commit is contained in:
196
tests/test_reconcile.py
Normal file
196
tests/test_reconcile.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""Spec for reconcile.py — written before the implementation.
|
||||
|
||||
Fixtures are the REAL events pulled from the live "Events" calendar on
|
||||
2026-06-06, trimmed to the fields reconcile uses. Crucially, none of them carry
|
||||
an `autoKey` (they were hand-created via the chat workflow), so these fixtures
|
||||
exercise exactly the case the fallback (title, date) matching must handle.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import reconcile as R # noqa: E402
|
||||
|
||||
TIGNOR = {
|
||||
"summary": "Christopher Tignor + Julia Kent — Public Records (tickets req'd)",
|
||||
"start": {"dateTime": "2026-06-12T19:00:00-04:00", "timeZone": "America/New_York"},
|
||||
}
|
||||
BRANCA = {
|
||||
"summary": 'Glenn Branca: Symphony No. 13 "Hallucination City" for 100 Guitars '
|
||||
"— Lincoln Center (tickets rec'd)",
|
||||
"start": {"dateTime": "2026-06-12T19:30:00-04:00", "timeZone": "America/New_York"},
|
||||
}
|
||||
SMORG = { # recurring instance (RRULE master expanded)
|
||||
"summary": "Smorgasburg @ The Oculus (day-of option)",
|
||||
"start": {"dateTime": "2026-06-19T11:00:00-04:00", "timeZone": "America/New_York"},
|
||||
"recurringEventId": "dcu0np1bp18mknfpdjdpbidamg",
|
||||
}
|
||||
GOVISLAND = { # recurring all-day instance
|
||||
"summary": "Governors Island + Six Coasts (day-of option)",
|
||||
"start": {"date": "2026-06-19"},
|
||||
"recurringEventId": "ul9ncfjd8po4augmn04k11g5es",
|
||||
}
|
||||
EXISTING = [TIGNOR, BRANCA, SMORG, GOVISLAND]
|
||||
|
||||
|
||||
def cand(title, start, **kw):
|
||||
return {"title": title, "start": start, **kw}
|
||||
|
||||
|
||||
class TestNormalize(unittest.TestCase):
|
||||
def test_strips_trailing_tag(self):
|
||||
self.assertEqual(R.normalize_title("Foo Bar (tickets req'd)"), "foo bar")
|
||||
|
||||
def test_strips_trailing_dayof_tag(self):
|
||||
self.assertEqual(R.normalize_title("Smorgasburg @ The Oculus (day-of option)"),
|
||||
"smorgasburg @ the oculus")
|
||||
|
||||
def test_strip_venue_em_dash(self):
|
||||
self.assertEqual(
|
||||
R.strip_venue("christopher tignor + julia kent — public records"),
|
||||
"christopher tignor + julia kent")
|
||||
|
||||
def test_strip_venue_noop_without_separator(self):
|
||||
self.assertEqual(R.strip_venue("smorgasburg @ the oculus"),
|
||||
"smorgasburg @ the oculus")
|
||||
|
||||
|
||||
class TestStartDate(unittest.TestCase):
|
||||
def test_google_datetime(self):
|
||||
self.assertEqual(R.start_date(TIGNOR), "2026-06-12")
|
||||
|
||||
def test_google_all_day(self):
|
||||
self.assertEqual(R.start_date(GOVISLAND), "2026-06-19")
|
||||
|
||||
def test_candidate_iso_string(self):
|
||||
self.assertEqual(R.start_date(cand("x", "2026-07-10T20:00:00")), "2026-07-10")
|
||||
|
||||
def test_candidate_all_day_date(self):
|
||||
self.assertEqual(R.start_date(cand("x", "2026-07-10", all_day=True)), "2026-07-10")
|
||||
|
||||
|
||||
class TestDedup(unittest.TestCase):
|
||||
def test_exact_re_report_is_duplicate(self):
|
||||
c = cand("Christopher Tignor + Julia Kent — Public Records (tickets req'd)",
|
||||
"2026-06-12T19:00:00")
|
||||
self.assertTrue(R.is_duplicate(c, TIGNOR))
|
||||
|
||||
def test_same_event_without_venue_or_tag(self):
|
||||
c = cand("Christopher Tignor + Julia Kent", "2026-06-12T19:00:00")
|
||||
self.assertTrue(R.is_duplicate(c, TIGNOR))
|
||||
|
||||
def test_branca_variant_tag_and_time_same_date(self):
|
||||
# model re-reports with the other tag (req'd vs rec'd), no venue, 7:00 vs 7:30
|
||||
c = cand('Glenn Branca: Symphony No. 13 "Hallucination City" for 100 Guitars',
|
||||
"2026-06-12T19:00:00")
|
||||
self.assertTrue(R.is_duplicate(c, BRANCA))
|
||||
|
||||
def test_recurring_candidate_matches_series_on_other_date(self):
|
||||
c = cand("Smorgasburg @ The Oculus", "2026-07-03T11:00:00",
|
||||
recurrence="RRULE:FREQ=WEEKLY;BYDAY=FR")
|
||||
self.assertTrue(R.is_duplicate(c, SMORG))
|
||||
|
||||
def test_unrelated_event_same_date_not_duplicate(self):
|
||||
c = cand("Ryoji Ikeda — The Shed", "2026-06-12T20:00:00") # same date as Tignor/Branca
|
||||
self.assertFalse(any(R.is_duplicate(c, e) for e in EXISTING))
|
||||
|
||||
def test_same_title_different_date_not_duplicate(self):
|
||||
# a genuinely new one-off Tignor show on another date should NOT be suppressed
|
||||
c = cand("Christopher Tignor + Julia Kent — Public Records", "2026-09-04T19:00:00")
|
||||
self.assertFalse(any(R.is_duplicate(c, e) for e in EXISTING))
|
||||
|
||||
|
||||
class TestReconcile(unittest.TestCase):
|
||||
def test_filters_and_stamps_autokey(self):
|
||||
cands = [
|
||||
cand("Christopher Tignor + Julia Kent", "2026-06-12T19:00:00"), # dup
|
||||
cand("Ryoji Ikeda — The Shed", "2026-07-10T20:00:00"), # new
|
||||
]
|
||||
out = R.reconcile(cands, EXISTING)
|
||||
self.assertEqual([i["title"] for i in out["insert"]], ["Ryoji Ikeda — The Shed"])
|
||||
self.assertEqual(len(out["skip"]), 1)
|
||||
self.assertEqual(out["insert"][0]["autoKey"],
|
||||
R.auto_key("Ryoji Ikeda — The Shed", "2026-07-10"))
|
||||
|
||||
def test_autokey_exact_match_short_circuits_title(self):
|
||||
# if an existing event carries OUR autoKey, match even when the display
|
||||
# name is unrecognizable (e.g. the user renamed it on the calendar)
|
||||
key = R.auto_key("Some Talk", "2026-08-01")
|
||||
ev = {"summary": "Totally Different Display Name",
|
||||
"start": {"date": "2026-08-01"},
|
||||
"extendedProperties": {"private": {"autoKey": key}}}
|
||||
self.assertTrue(R.is_duplicate(cand("Some Talk", "2026-08-01"), ev))
|
||||
|
||||
def test_idempotent_second_run(self):
|
||||
# feeding the previous run's inserts back in (now present on the calendar)
|
||||
# produces zero new inserts
|
||||
first = R.reconcile([cand("Ryoji Ikeda — The Shed", "2026-07-10T20:00:00")], EXISTING)
|
||||
as_calendar_event = {
|
||||
"summary": "Ryoji Ikeda — The Shed",
|
||||
"start": {"dateTime": "2026-07-10T20:00:00-04:00"},
|
||||
"extendedProperties": {"private": {"autoKey": first["insert"][0]["autoKey"]}},
|
||||
}
|
||||
second = R.reconcile([cand("Ryoji Ikeda — The Shed", "2026-07-10T20:00:00")],
|
||||
EXISTING + [as_calendar_event])
|
||||
self.assertEqual(second["insert"], [])
|
||||
|
||||
|
||||
class TestLoader(unittest.TestCase):
|
||||
def test_bare_list(self):
|
||||
self.assertEqual(R.as_event_list([TIGNOR]), [TIGNOR])
|
||||
|
||||
def test_events_wrapper(self): # shape returned by the list-events tool
|
||||
self.assertEqual(R.as_event_list({"events": [TIGNOR], "summary": "Events"}), [TIGNOR])
|
||||
|
||||
def test_items_wrapper(self): # raw Google API shape
|
||||
self.assertEqual(R.as_event_list({"items": [TIGNOR]}), [TIGNOR])
|
||||
|
||||
def test_unrecognized_returns_empty(self):
|
||||
self.assertEqual(R.as_event_list({"nope": 1}), [])
|
||||
|
||||
|
||||
class TestHardening(unittest.TestCase):
|
||||
"""Regression tests for defects the adversarial review reproduced live."""
|
||||
|
||||
def test_intra_run_collapses_same_date_variants(self):
|
||||
# two titles for the same show on the same date, against an EMPTY calendar
|
||||
cands = [cand("Tim Hecker — Pioneer Works", "2026-08-07T20:00:00"),
|
||||
cand("Tim Hecker (tickets req'd)", "2026-08-07T20:00:00")]
|
||||
out = R.reconcile(cands, [])
|
||||
self.assertEqual(len(out["insert"]), 1)
|
||||
self.assertEqual(len(out["skip"]), 1)
|
||||
|
||||
def test_recurring_candidate_matches_nonrecurring_same_title(self):
|
||||
# connector may return an expanded instance with no recurringEventId
|
||||
existing = {"summary": "Smorgasburg @ The Oculus",
|
||||
"start": {"dateTime": "2026-06-19T11:00:00-04:00"}}
|
||||
c = cand("Smorgasburg @ The Oculus", "2026-09-04T11:00:00",
|
||||
recurrence="RRULE:FREQ=WEEKLY;BYDAY=FR")
|
||||
self.assertTrue(R.is_duplicate(c, existing))
|
||||
|
||||
def test_null_extended_properties_does_not_crash(self):
|
||||
ev1 = {"summary": "X", "start": {"date": "2026-08-01"}, "extendedProperties": None}
|
||||
ev2 = {"summary": "Y", "start": {"date": "2026-08-01"},
|
||||
"extendedProperties": {"private": None}}
|
||||
self.assertFalse(R.is_duplicate(cand("Totally Other", "2026-08-01"), ev1))
|
||||
self.assertFalse(R.is_duplicate(cand("Totally Other", "2026-08-01"), ev2))
|
||||
|
||||
def test_today_drops_past_dated(self):
|
||||
cands = [cand("Old Show", "2020-01-01T20:00:00"),
|
||||
cand("Future Show", "2026-12-01T20:00:00")]
|
||||
out = R.reconcile(cands, [], today="2026-06-08")
|
||||
self.assertEqual([R.title_of(i) for i in out["insert"]], ["Future Show"])
|
||||
self.assertEqual(len(out["dropped_past"]), 1)
|
||||
self.assertEqual(R.title_of(out["dropped_past"][0]["candidate"]), "Old Show")
|
||||
|
||||
def test_max_caps_inserts_and_overflows_in_order(self):
|
||||
cands = [cand(f"Show {i}", f"2026-07-0{i}T20:00:00") for i in range(1, 6)]
|
||||
out = R.reconcile(cands, [], max_new=3)
|
||||
self.assertEqual([R.title_of(i) for i in out["insert"]],
|
||||
["Show 1", "Show 2", "Show 3"])
|
||||
self.assertEqual(len(out["dropped_overflow"]), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user