From 19b07190667d7e12fa13c9aad6e2b2ab90edee10 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 11 Sep 2026 13:52:33 +0200 Subject: [PATCH] test(arch): keep Room and query strings inside the data layer The seam is only worth having if something checks it. This fails the build if androidx.room, an entity or a query string appears outside data/, rather than leaving it to review to notice. Co-Authored-By: Claude Opus 5 --- .../clockula/ArchitectureRulesTest.kt | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt new file mode 100644 index 0000000..5cb6e2f --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/clockula/ArchitectureRulesTest.kt @@ -0,0 +1,69 @@ +package de.jeanlucmakiola.clockula + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test +import java.io.File + +/** + * docs/PLAN.md ยง0's boundary, made mechanical: the app talks to repository + * interfaces and plain-Kotlin models, and only `data/` knows Room exists. + * A grep is a weaker tool than a compiler, but it is the one that fails the + * build when someone reaches through the seam. + */ +class ArchitectureRulesTest { + + private val mainSourceRoot: File = listOf(File("src/main/java"), File("app/src/main/java")) + .firstOrNull { it.isDirectory } + ?: error("Could not resolve the main source root from ${File("").absolutePath}") + + private val kotlinSources: List = + mainSourceRoot.walkTopDown().filter { it.isFile && it.extension == "kt" }.toList() + + private val outsideDataLayer: List = + kotlinSources.filterNot { it.invariantSeparatorsPath.contains("de/jeanlucmakiola/clockula/data/") } + + private fun List.containing(vararg needles: String): List = + filter { file -> needles.any { it in file.readText() } }.map { it.invariantSeparatorsPath } + + @Test + fun `Room does not leak above the data layer`() { + val offenders = outsideDataLayer.containing("androidx.room") + + assertThat(offenders).isEmpty() + } + + @Test + fun `no Dao or Query annotation appears outside the data layer`() { + val offenders = outsideDataLayer.containing("@Dao", "@Query") + + assertThat(offenders).isEmpty() + } + + @Test + fun `no entity or dao type is named outside the data layer`() { + val offenders = outsideDataLayer.containing( + "AlarmEntity", + "TimerEntity", + "WorldClockEntity", + "LapEntity", + "AlarmDao", + "TimerDao", + "WorldClockDao", + "LapDao", + "ClockulaDatabase", + ) + + assertThat(offenders).isEmpty() + } + + @Test + fun `the domain is free of Android types`() { + val domain = kotlinSources + .filter { it.invariantSeparatorsPath.contains("de/jeanlucmakiola/clockula/domain/") } + val offenders = domain.filter { file -> + file.readLines().any { it.startsWith("import android.") || it.startsWith("import androidx.") } + }.map { it.invariantSeparatorsPath } + + assertThat(offenders).isEmpty() + } +}