From 4535ba223ba781bb0a6680bfbf0fa36b080c1398 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 12:46:05 -0500 Subject: [PATCH] test: verify Kiosk consumes real Portal-generated schedule payloads Adds two fixtures captured from a running Portal (daily and weekly) and tests that parse them with the Kiosk API client and evaluate them with the schedule engine, including adjacency and Sunday->Monday spillover. Proves the Kiosk consumes actual Portal output with no hand-editing. --- docs/fixtures/schedule-portal-daily.json | 1 + docs/fixtures/schedule-portal-weekly.json | 1 + tests/test_schedule_engine.cpp | 50 +++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 docs/fixtures/schedule-portal-daily.json create mode 100644 docs/fixtures/schedule-portal-weekly.json diff --git a/docs/fixtures/schedule-portal-daily.json b/docs/fixtures/schedule-portal-daily.json new file mode 100644 index 0000000..01b85f5 --- /dev/null +++ b/docs/fixtures/schedule-portal-daily.json @@ -0,0 +1 @@ +{"version":"7f53efde17d291a1ecf604e266d3803d1495b986","type":"daily","daily":[{"id":"6a9be0a4-4a1c-4a2c-929d-7b6c6fe56d91","startTime":"06:00","startMinutes":360,"endTime":"07:00","endMinutes":420,"overnight":false,"durationMinutes":60,"dynamicAudio":true,"proclamationIds":["310138f3-adfc-45ce-ae52-07279cd25b48"]},{"id":"65fecafc-abc8-4796-bb12-ed8e9d99e0fe","startTime":"18:00","startMinutes":1080,"endTime":"20:30","endMinutes":1230,"overnight":false,"durationMinutes":150,"dynamicAudio":false,"proclamationIds":["3805242e-4930-4dd2-afb1-130593b1dbab"]}],"weekly":{},"blocks":[{"id":"6a9be0a4-4a1c-4a2c-929d-7b6c6fe56d91","startTime":"06:00","startMinutes":360,"endTime":"07:00","endMinutes":420,"overnight":false,"durationMinutes":60,"dynamicAudio":true,"proclamationIds":["310138f3-adfc-45ce-ae52-07279cd25b48"]},{"id":"65fecafc-abc8-4796-bb12-ed8e9d99e0fe","startTime":"18:00","startMinutes":1080,"endTime":"20:30","endMinutes":1230,"overnight":false,"durationMinutes":150,"dynamicAudio":false,"proclamationIds":["3805242e-4930-4dd2-afb1-130593b1dbab"]}]} \ No newline at end of file diff --git a/docs/fixtures/schedule-portal-weekly.json b/docs/fixtures/schedule-portal-weekly.json new file mode 100644 index 0000000..983bb1d --- /dev/null +++ b/docs/fixtures/schedule-portal-weekly.json @@ -0,0 +1 @@ +{"version":"6de852ba72ce1ab3052150640730ee87a7d460b5","type":"weekly","daily":[],"weekly":{"mon":[{"id":"a1054b78-480a-4e87-8465-aac8d677c666","startTime":"06:00","startMinutes":360,"endTime":"07:00","endMinutes":420,"overnight":false,"durationMinutes":60,"dynamicAudio":true,"proclamationIds":["310138f3-adfc-45ce-ae52-07279cd25b48"]},{"id":"25979ec9-f7da-4aa4-864f-f74b1a2da78b","startTime":"07:00","startMinutes":420,"endTime":"08:30","endMinutes":510,"overnight":false,"durationMinutes":90,"dynamicAudio":true,"proclamationIds":["3805242e-4930-4dd2-afb1-130593b1dbab"]}],"tue":[],"wed":[],"thu":[],"fri":[],"sat":[],"sun":[{"id":"e2237d98-834c-433b-8979-b56ddad2ef7a","startTime":"23:00","startMinutes":1380,"endTime":"01:00","endMinutes":60,"overnight":true,"durationMinutes":120,"dynamicAudio":true,"proclamationIds":["fcd5a291-3d6e-4678-9d85-9b550e80f1c7"]}]},"blocks":[]} \ No newline at end of file diff --git a/tests/test_schedule_engine.cpp b/tests/test_schedule_engine.cpp index 571222b..a7c11d7 100644 --- a/tests/test_schedule_engine.cpp +++ b/tests/test_schedule_engine.cpp @@ -296,3 +296,53 @@ TEST(ScheduleManagerTest, OfflineFallsBackToCache) { EXPECT_EQ(etag, "\"cached\""); radiantwave::sync::schedule_cache::clear(); } + +// ─── Phase 5: consume real Portal-generated payloads ─────────────────── +// +// These fixtures were captured from a running Portal (GET +// /api/v2/licenses/{key}/schedule). They prove the Kiosk parses and evaluates +// exactly what the Portal emits, with no hand-editing in between. + +TEST(SchedulePortalParityTest, ParsesPortalDailyPayload) { + const auto p = loadPayload("schedule-portal-daily.json"); + EXPECT_EQ(p.type, "daily"); + ASSERT_TRUE(p.hasSchedule()); + ASSERT_EQ(p.daily.size(), 2u); + // 06:00-07:00 (dynamicAudio true) and 18:00-20:30 (false). + EXPECT_EQ(p.daily[0].start_minutes, 360); + EXPECT_EQ(p.daily[0].end_minutes, 420); + EXPECT_TRUE(p.daily[0].dynamic_audio); + EXPECT_FALSE(p.daily[1].dynamic_audio); + EXPECT_FALSE(p.daily[0].proclamation_ids.empty()); + EXPECT_EQ(p.legacy_blocks.size(), 2u); + + // Engine decisions: inside / gap / inside. + EXPECT_TRUE(evaluate(p, 6 * 60 + 30, Weekday::Tue).hasBlock); + EXPECT_FALSE(evaluate(p, 12 * 60, Weekday::Tue).hasBlock); + EXPECT_TRUE(evaluate(p, 19 * 60, Weekday::Tue).hasBlock); +} + +TEST(SchedulePortalParityTest, ParsesPortalWeeklyPayloadAndSpillover) { + const auto p = loadPayload("schedule-portal-weekly.json"); + EXPECT_EQ(p.type, "weekly"); + ASSERT_TRUE(p.hasSchedule()); + // Monday has two adjacent blocks; Sunday has one crossing block. + int mon = 0; + int sun = 0; + for (const auto& b : p.weekly) { + if (b.day == "mon") ++mon; + if (b.day == "sun") ++sun; + } + EXPECT_EQ(mon, 2); + EXPECT_EQ(sun, 1); + + // Monday 00:30 belongs to Sunday's crossing block. + const auto spill = evaluate(p, 30, Weekday::Mon); + ASSERT_TRUE(spill.hasBlock); + EXPECT_EQ(spill.block.day, "sun"); + + // Monday 06:30 is a Monday block. + const auto monBlock = evaluate(p, 6 * 60 + 30, Weekday::Mon); + ASSERT_TRUE(monBlock.hasBlock); + EXPECT_EQ(monBlock.block.day, "mon"); +}