197 lines
8.8 KiB
Python
197 lines
8.8 KiB
Python
"""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()
|