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 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 13:52:33 +02:00
co-authored by Claude Opus 5
parent d2ab99867e
commit 19b0719066
@@ -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<File> =
mainSourceRoot.walkTopDown().filter { it.isFile && it.extension == "kt" }.toList()
private val outsideDataLayer: List<File> =
kotlinSources.filterNot { it.invariantSeparatorsPath.contains("de/jeanlucmakiola/clockula/data/") }
private fun List<File>.containing(vararg needles: String): List<String> =
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()
}
}