test(arch): the platform's intents stay at the door, and the show intent stays pointed

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.
This commit is contained in:
2026-09-23 11:02:20 +02:00
parent 46a049f988
commit 46f625b6b2
@@ -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()
}
}