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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wmy1BpCKi8KeSjaWhYuCPV
This commit is contained in:
2026-09-12 13:07:53 +02:00
parent 959eb599ff
commit 0b31f7c9c4
@@ -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<String> = 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 ->