From 0b31f7c9c4106ca3faf78bd29542d32967897c9f Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 12 Sep 2026 13:07:53 +0200 Subject: [PATCH] test(arch): keep the shell's decisions out of the composables The rules that let M4 ship without Robolectric are only true while they stay true, so they are asserted rather than trusted. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Wmy1BpCKi8KeSjaWhYuCPV --- .../clockula/ArchitectureRulesTest.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt index 69309d3..ae62d7b 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt @@ -70,6 +70,53 @@ class ArchitectureRulesTest { assertThat(offenders).isEmpty() } + /** + * Compose may live in `ui/` and in the one activity that hosts it, and + * nowhere else — the shell's decision surface is pure Kotlin by + * construction, which is what earns the no-Robolectric posture (D17). + */ + private val composeAllowlist: List = kotlinSources + .filter { file -> file.readLines().any { it.startsWith("import androidx.compose.") } } + .map { it.invariantSeparatorsPath } + + @Test + fun `Compose stays inside the ui package and its one host activity`() { + val offenders = composeAllowlist.filterNot { + it.contains("de/jeanlucmakiola/clockula/ui/") || + it.endsWith("de/jeanlucmakiola/clockula/MainActivity.kt") + } + + assertThat(offenders).isEmpty() + } + + @Test + fun `the ui layer names no literal colour`() { + val literals = listOf( + "Color(0x", + "Color.Red", "Color.Green", "Color.Blue", "Color.Yellow", "Color.Cyan", + "Color.Magenta", "Color.White", "Color.Black", "Color.Gray", + "Color.LightGray", "Color.DarkGray", + ) + val ui = kotlinSources.filter { file -> + val path = file.invariantSeparatorsPath + path.contains("de/jeanlucmakiola/clockula/ui/") && + !path.contains("de/jeanlucmakiola/clockula/ui/theme/") + } + + val offenders = ui.containing(*literals.toTypedArray()) + + assertThat(offenders).isEmpty() + } + + @Test + fun `exactly one file outside the ui package may import Compose`() { + val outsideUi = composeAllowlist.filterNot { + it.contains("de/jeanlucmakiola/clockula/ui/") + } + + assertThat(outsideUi).hasSize(1) + } + @Test fun `the alarm engine and its pure neighbours stay free of Android types`() { val jvmTestable = kotlinSources.filter { file ->