From 46f625b6b2200c2c047937793e16f7a4c8afc282 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 23 Sep 2026 11:02:20 +0200 Subject: [PATCH] test(arch): the platform's intents stay at the door, and the show intent stays pointed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new rules. Nothing under `domain/` may name `android.content.Intent`, so the validator stays testable without a device. The `AlarmClock` action strings may be named only under `domain/interop/` and `interop/`. And `AndroidAlarmScheduler` may not name `AlarmRingActivity` — the defect this milestone fixed would otherwise come back the next time someone reaches for a "show the alarm" intent. --- .../clockula/ArchitectureRulesTest.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt index d9d8ffc..d22312d 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt @@ -135,6 +135,9 @@ class ArchitectureRulesTest { val path = file.invariantSeparatorsPath path.contains("de/jeanlucmakiola/clockula/domain/alarm/") || path.endsWith("de/jeanlucmakiola/clockula/alarm/AlarmEngine.kt") || + // M9 D28: the AlarmClock orchestrator is JVM-testable by + // construction, exactly as the three engines are. + path.endsWith("de/jeanlucmakiola/clockula/interop/AlarmClockHandler.kt") || path.endsWith("de/jeanlucmakiola/clockula/timer/TimerEngine.kt") || path.endsWith("de/jeanlucmakiola/clockula/stopwatch/StopwatchEngine.kt") || path.endsWith("de/jeanlucmakiola/clockula/alarm/AlarmResolver.kt") || @@ -308,4 +311,46 @@ class ArchitectureRulesTest { assertThat(offenders).isEmpty() } + + /** + * M9 D28: one vocabulary for the platform's `AlarmClock` strings, so a + * second file cannot hardcode `"android.intent.action.SET_ALARM"` and drift + * away from the door everyone else goes through. + */ + @Test + fun `the platform alarm-clock literals live in one file`() { + val offenders = kotlinSources + .containing("android.intent.action.SET_ALARM", "android.intent.extra.alarm.") + .filterNot { it.endsWith("de/jeanlucmakiola/clockula/domain/interop/AlarmClockContract.kt") } + + assertThat(offenders).isEmpty() + } + + /** + * M9 D21: the next-alarm show intent is what the system opens when the user + * taps the status-bar alarm icon. Pointing it at the ring screen opens a + * ring screen for an alarm that is not ringing, which closes itself — so + * the fix is pinned rather than left to be re-broken. + */ + @Test + fun `the next-alarm show intent never names the ring screen`() { + val scheduler = kotlinSources + .filter { it.invariantSeparatorsPath.endsWith("alarm/android/AndroidAlarmScheduler.kt") } + + assertThat(scheduler.usingInCode("AlarmRingActivity")).isEmpty() + } + + /** + * M9 D28: the screen does not parse intents — the mirror of "the screen + * schedules no timer and decides no ring". + */ + @Test + fun `the screen parses no intent from another app`() { + val ui = kotlinSources + .filter { it.invariantSeparatorsPath.contains("de/jeanlucmakiola/clockula/ui/") } + + val offenders = ui.usingInCode("AlarmClockHandler", "AlarmClockRequests", "IntentExtras") + + assertThat(offenders).isEmpty() + } }