app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 7a6cd5b7a5fb1d7c5075a9ff2d129d2332fe0812
parent 63449f5d59ddb49f84498915b3ce8d41ce36f15b
Author: triesap <tyson@radroots.org>
Date:   Mon, 10 Aug 2026 22:11:18 +0000

test: qualify the HarvestCircle build logic

- exercise every convention plugin in isolated fixtures without buildSrc coupling
- prove strict configuration-cache storage and reuse in fixtures and the application
- cover missing authority, stale generation, unsupported hosts, and empty test inventory
- reject incomplete package metadata, unsafe native payloads, and unknown provenance

Diffstat:
Mbuild-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mbuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasks.kt | 62++++++++++++++++++++++++++++++++++++++++----------------------
Abuild-logic/plugins/src/test/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasksTest.kt | 28++++++++++++++++++++++++++++
3 files changed, 127 insertions(+), 26 deletions(-)

diff --git a/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt b/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt @@ -4,6 +4,7 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.UnexpectedBuildFailure import kotlin.io.path.createTempDirectory import kotlin.io.path.createDirectories +import kotlin.io.path.exists import kotlin.io.path.writeText import kotlin.test.Test import kotlin.test.assertTrue @@ -71,16 +72,30 @@ class ConventionPluginSmokeTest { val result = runner.build() assertTrue(result.output.contains("BUILD SUCCESSFUL"), pluginId) + assertTrue(!fixture.resolve("buildSrc").exists(), "$pluginId fixture must not provide buildSrc classes") if (pluginId == "org.harvestcircle.build.root") { assertTrue(result.output.contains("verifyProductCoordinates"), result.output) - assertTrue(runner.build().output.contains("Reusing configuration cache")) - } - if (pluginId == "org.harvestcircle.build.rust-ffi") { - assertTrue(runner.build().output.contains("Reusing configuration cache")) } + assertTrue(runner.build().output.contains("Reusing configuration cache"), pluginId) } } + @Test + fun rootPluginFailsClosedWhenTheProductManifestIsMissing() { + val fixture = createTempDirectory("harvestcircle-root-missing-manifest-") + fixture.resolve("settings.gradle.kts").writeText("rootProject.name = \"fixture\"\n") + fixture.resolve("build.gradle.kts").writeText("plugins { id(\"org.harvestcircle.build.root\") }\n") + + val result = + GradleRunner.create() + .withProjectDir(fixture.toFile()) + .withPluginClasspath() + .withArguments("verifyProductCoordinates", "--stacktrace") + .buildAndFail() + + assertTrue(result.output.contains("harvestcircle-v1.properties"), result.output) + } + private fun prepareDesktopFixture( fixture: java.nio.file.Path, withUnitTest: Boolean = true, @@ -330,6 +345,46 @@ class ConventionPluginSmokeTest { } } + @Test + fun packagingPluginAcceptsGovernedProvenanceAndRejectsUnknownInputs() { + val governed = createTempDirectory("harvestcircle-package-governed-provenance-") + preparePackagingBuild(governed, "exit 0") + val governedEnvironment = + System.getenv() + + mapOf( + "HARVESTCIRCLE_BUILD_SOURCE_COMMIT" to "a".repeat(40), + "HARVESTCIRCLE_BUILD_SOURCE_DIRTY" to "false", + "HARVESTCIRCLE_BUILD_RADROOTS_REVISION" to "b".repeat(40), + "SOURCE_DATE_EPOCH" to "1770000000", + ) + val success = + GradleRunner.create() + .withProjectDir(governed.toFile()) + .withPluginClasspath() + .withEnvironment(governedEnvironment) + .withArguments(":app:desktop:verifyReleaseBuildProvenance", "--stacktrace") + .build() + assertTrue(success.output.contains("BUILD SUCCESSFUL"), success.output) + + val standalone = createTempDirectory("harvestcircle-package-unknown-provenance-") + preparePackagingBuild(standalone, "exit 0") + val failure = + GradleRunner.create() + .withProjectDir(standalone.toFile()) + .withPluginClasspath() + .withEnvironment( + System.getenv() - + setOf( + "HARVESTCIRCLE_BUILD_SOURCE_COMMIT", + "HARVESTCIRCLE_BUILD_SOURCE_DIRTY", + "HARVESTCIRCLE_BUILD_RADROOTS_REVISION", + "SOURCE_DATE_EPOCH", + ), + ).withArguments(":app:desktop:verifyReleaseBuildProvenance", "--stacktrace") + .buildAndFail() + assertTrue(failure.output.contains("Release source commit provenance is unknown or malformed"), failure.output) + } + private fun prepareDesktopBuild( fixture: java.nio.file.Path, withUnitTest: Boolean, diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasks.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasks.kt @@ -32,11 +32,7 @@ public abstract class VerifyDesktopBuildMetadataArtifact : DefaultTask() { jar.getJarEntry("org/harvestcircle/application/generated/DesktopBuildMetadata.class") ?: throw GradleException("Desktop build metadata is missing from the application artifact") val metadata = jar.getInputStream(entry).use { it.readBytes() }.toString(Charsets.ISO_8859_1) - expectedBuildEvidence.get().forEach { evidence -> - require(metadata.contains(evidence)) { - "Desktop application artifact is missing generated build evidence" - } - } + requireBuildMetadataEvidence(metadata, expectedBuildEvidence.get()) } } } @@ -315,26 +311,48 @@ public abstract class VerifyReleaseBuildProvenance : DefaultTask() { private fun packagedNativeLibraries( root: File, expectedEntry: String, -): List<ByteArray> = - root.walkTopDown() - .filter { it.isFile && it.extension == "jar" } - .flatMap { jarFile -> - JarFile(jarFile).use { jar -> - val nativeEntries = +): List<ByteArray> { + val entries = + root.walkTopDown() + .filter { it.isFile && it.extension == "jar" } + .flatMap { jarFile -> + JarFile(jarFile).use { jar -> jar.entries().asSequence().filter { entry -> !entry.isDirectory && entry.name.substringAfterLast('.').lowercase() in setOf("dylib", "so", "dll") - }.toList() - val productEntries = - nativeEntries.filter { entry -> - entry.name == expectedEntry || entry.name.lowercase().contains("harvestcircle") - } - require(productEntries.none { it.name != expectedEntry }) { - "Package contains an unexpected or test native payload" - } - productEntries.map { entry -> jar.getInputStream(entry).use { it.readBytes() } } - }.asSequence() - }.toList() + }.map { entry -> entry.name to jar.getInputStream(entry).use { it.readBytes() } }.toList() + }.asSequence() + }.toList() + requireSingleCanonicalProductNativeEntry(entries.map { it.first }, expectedEntry) + return entries.filter { it.first == expectedEntry }.map { it.second } +} + +internal fun requireBuildMetadataEvidence( + metadata: String, + expectedBuildEvidence: List<String>, +) { + expectedBuildEvidence.forEach { evidence -> + require(metadata.contains(evidence)) { + "Desktop application artifact is missing generated build evidence" + } + } +} + +internal fun requireSingleCanonicalProductNativeEntry( + nativeEntries: List<String>, + expectedEntry: String, +) { + val productEntries = + nativeEntries.filter { entry -> + entry == expectedEntry || entry.lowercase().contains("harvestcircle") + } + require(productEntries.none { it != expectedEntry }) { + "Package contains an unexpected or test native payload" + } + require(productEntries.size == 1) { + "Package must contain exactly one production native library" + } +} private fun commandOutput(vararg command: String): String { val process = ProcessBuilder(*command).redirectErrorStream(true).start() diff --git a/build-logic/plugins/src/test/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasksTest.kt b/build-logic/plugins/src/test/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasksTest.kt @@ -0,0 +1,28 @@ +package org.harvestcircle.buildlogic.plugins.tasks + +import kotlin.test.Test +import kotlin.test.assertFailsWith + +class PackagingTasksTest { + @Test + fun metadataEvidenceMustBeComplete() { + requireBuildMetadataEvidence("version=1.0 toolchain=21", listOf("1.0", "21")) + + assertFailsWith<IllegalArgumentException> { + requireBuildMetadataEvidence("version=1.0", listOf("1.0", "missing-toolchain")) + } + } + + @Test + fun nativeInventoryRequiresOneCanonicalProductionPayload() { + val expected = "darwin-aarch64/libharvestcircle_ffi.dylib" + requireSingleCanonicalProductNativeEntry(listOf("com/sun/jna/darwin-aarch64/libjnidispatch.jnilib", expected), expected) + + assertFailsWith<IllegalArgumentException> { + requireSingleCanonicalProductNativeEntry(listOf(expected, expected), expected) + } + assertFailsWith<IllegalArgumentException> { + requireSingleCanonicalProductNativeEntry(listOf("darwin-aarch64/libharvestcircle_test_ffi.dylib"), expected) + } + } +}