diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ed668f579e5ba0c6963d6a42b41995bfda385153..e2ffce6de3bf17c34bf3e1f83585d47d4272fc65 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,5 @@
 image:
-  name: wnwalker/android-gradle-build-image:34.0.0-jdk17-gradle8.7
+  name: wnwalker/temerity-build-image@sha256:b1ea20cd261395671a541996f7e0cc2437ecf95a813b6fcc7581b41cde167fbf
 
 variables:
   ORG_GRADLE_PROJECT_signingKey: $SIGNING_KEY
diff --git a/.sdkmanrc b/.sdkmanrc
index 955c2580880005a57da000c2c867ea6a706864bd..dc041b2d9f45321aaea04f45e17d92088480ebb6 100644
--- a/.sdkmanrc
+++ b/.sdkmanrc
@@ -1,3 +1,3 @@
 # Enable auto-env through the sdkman_auto_env config
 # Add key=value pairs of SDKs to use below
-java=17.0.12-tem
\ No newline at end of file
+java=17.0.14-tem
\ No newline at end of file
diff --git a/build-image/temerity-build-image.dockerfile b/build-image/temerity-build-image.dockerfile
index 4363c9d3691899d12946807ecec2f9c50045766a..37e7f9d3d47a5989eddd471b2fdbff00c2d03cd9 100644
--- a/build-image/temerity-build-image.dockerfile
+++ b/build-image/temerity-build-image.dockerfile
@@ -1,10 +1,13 @@
 # Based on code available at https://github.com/MobileDevOps/android-sdk-image/blob/9cab35c5d0433ff08cc34f14b82f0f417464bc8e/Dockerfile
+# To build and publish using experimental Docker buildx: 
+# docker buildx build --platform linux/amd64 --push -t <docker_hub_username>/<image_name>:<tag> . --file temerity-build-image.dockerfile
+
 # To build:
 # docker build -t <docker_hub_username>/<image_name>:<tag> . --file temerity-build-image.dockerfile
 # To publish:
 # docker push <docker_hub_username>/<image_name>:<tag>
 
-FROM --platform=linux/amd64 ubuntu:24.10
+FROM bitnami/minideb:bookworm AS base
 
 LABEL maintainer="wnwalker"
 
@@ -12,8 +15,8 @@ LABEL maintainer="wnwalker"
 # https://developer.android.com/studio/index.html
 ENV ANDROID_SDK_TOOLS_VERSION=11076708
 ENV ANDROID_SDK_TOOLS_CHECKSUM=2d2d50857e4eb553af5a6dc3ad507a17adf43d115264b1afc116f95c92e5e258
-ENV GRADLE_VERSION=8.7
-ENV JAVA_VERSION=17.0.12-tem
+ENV GRADLE_VERSION=8.11
+ENV JAVA_VERSION=17.0.14-tem
 ENV ANDROID_HOME=/opt/android-sdk-linux
 ENV ANDROID_SDK_ROOT=$ANDROID_HOME
 ENV PATH=$PATH:$ANDROID_HOME/cmdline-tools:$ANDROID_HOME/cmdline-tools/bin:$ANDROID_HOME/platform-tools
@@ -42,7 +45,7 @@ RUN apt-get -qq update \
     git > /dev/null \
     && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
 
-ENV HOME=/home/agbi
+ENV HOME=/home/tbi
 WORKDIR $HOME/app
 
 # Install SDKMAN
@@ -50,8 +53,10 @@ RUN curl -s "https://get.sdkman.io" | bash
 SHELL ["/bin/bash", "-c"]
 
 # Set the environment variable for sdkman to auto-answer during installation
-RUN echo "sdkman_auto_answer=true" > "$HOME/.sdkman/etc/config"
-RUN echo "sdkman_auto_env=true" > "$HOME/.sdkman/etc/config"
+RUN mkdir -p "$HOME/.sdkman/etc/"
+RUN touch "$HOME/.sdkman/etc/config"
+RUN echo "sdkman_auto_answer=true" >> "$HOME/.sdkman/etc/config"
+RUN echo "sdkman_auto_env=true" >> "$HOME/.sdkman/etc/config"
 
 # Install Gradle, Java
 RUN source "${HOME}/.sdkman/bin/sdkman-init.sh" \
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index b7a239d6292ca1e41d8cb48a041e90f5403dacb1..97212ad8a534c567ec9dae79bd96a7eeffd55960 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -28,12 +28,8 @@ dependencies {
 }
 
 kotlin {
-    target {
-        compilations.configureEach {
-            kotlinOptions {
-                jvmToolchain(libs.versions.java.get().toInt())
-            }
-        }
+    compilerOptions {
+        jvmToolchain(libs.versions.jvm.target.get().toInt())
     }
 }
 
diff --git a/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/FormattingConventionPlugin.kt b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/FormattingConventionPlugin.kt
index 4cb05356feddd63d524191e16d0538277869476a..418c6591deacedbd9aaa0a5b6107b72964de1c6a 100644
--- a/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/FormattingConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/FormattingConventionPlugin.kt
@@ -11,7 +11,7 @@ class FormattingConventionPlugin : Plugin<Project> {
       kotlin {
         target("**/*.kt")
         targetExclude("${layout.buildDirectory}/**/*.kt")
-        ktlint().editorConfigOverride(
+        ktlint("1.5.0").editorConfigOverride(
           mapOf(
             "indent_size" to "2",
             "continuation_indent_size" to "2",
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0070fa1bb5ece7cfb86312628307ca39e1160568..778814576534e00e449e40e3be69ba7685e4044e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,47 +1,48 @@
 [versions]
-agp = "8.2.2"
-java = "17"
-kotlin = "2.1.0"
-ksp = "2.1.0-1.0.29"
-
-kotlinx-serialization = "1.8.0-RC"
-ktor = "3.0.3"
-cache4k = "0.13.0"
+agp = "8.7.3"
+jvm-target = "17"
+jvm-version = "17.0.14"
+kotlin = "2.1.10"
+ksp = "2.1.10-1.0.31"
+
+kotlinx-serialization = "1.8.0"
+ktor = "3.1.1"
+cache4k = "0.14.0"
 statelyConcurrentCollections = "2.1.0"
-slf4j = "2.0.16"
-kotlinx-io = "0.6.0"
+slf4j = "2.0.17"
+kotlinx-io = "0.7.0"
 
-ktorfit = "2.2.0"
+ktorfit = "2.4.0"
 coroutines = "1.10.1"
-datetime = "0.6.1"
-sandwich = "2.0.10"
+datetime = "0.6.2"
+sandwich = "2.1.0"
 arrow = "2.0.0"
 # Main Koin version - for core, android deps
-koin = "4.0.1"
-koinTest = "4.0.1"
-gradleBuildConfigPlugin = "5.5.1"
+koin = "4.0.2"
+koinTest = "4.0.2"
+gradleBuildConfigPlugin = "5.5.4"
 akkurate = "0.11.0"
 exposed = "0.56.0"
 
 dotenv-vault = "0.0.3"
-kermit = "2.0.4"
+kermit = "2.0.5"
 appdirs = "1.2.0"
 kstore = "0.9.1"
-kmpIo = "0.1.5"
+kmpIo = "0.1.6"
 kotlinSemver = "2.0.0"
 jansi = "2.4.1"
 temerity = "[0.1.0-dev0z+41180a5,0.1.0]"
 
-kotlinx-dataframe = "0.14.2"
+kotlinx-dataframe = "0.15.0"
 klaxon = "5.6"
-kotest = "6.0.0.M1"
+kotest = "6.0.0.M2"
 kotest-datatest = "5.9.1"
 
 dokka = "2.0.0"
-spotless = "7.0.0.BETA4"
-gitSemVer = "3.1.7"
-conventionalCommits = "1.0.12"
-konsist = "0.16.1"
+spotless = "7.0.0"
+gitSemVer = "4.0.2"
+conventionalCommits = "1.0.15"
+konsist = "0.17.3"
 
 minSdk = "24"
 targetSdk = "34"
@@ -66,6 +67,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
 kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
 kotlinx-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "coroutines" }
 kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines" }
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
 kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
 kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
 
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b82aa23a4f05d39d81870f8355ca43324f027298..94113f200e61179d552438ad36de4a645738eac2 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 51505d6d476a09b0dd62e478eee32bb068ec655b..241007b54e8ef1c5f9622944596e67f9f34d3ad3 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -23,6 +23,7 @@ dependencyResolutionManagement {
         maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
         maven("https://plugins.gradle.org/m2")
         maven("https://git.ucsc.edu/api/v4/projects/12162/packages/maven")
+        maven("https://oss.sonatype.org/content/repositories/snapshots")
     }
 }
 
diff --git a/temerity/build.gradle.kts b/temerity/build.gradle.kts
index 10e38070333cc5d56439413bf07b39930599db52..0a4a0f62af75b1a686de1f479579e2cb8e7e0fe2 100644
--- a/temerity/build.gradle.kts
+++ b/temerity/build.gradle.kts
@@ -15,7 +15,6 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
 import io.github.andreabrighi.gradle.gitsemver.conventionalcommit.ConventionalCommit
 import io.github.z4kn4fein.semver.Version
 import kotlinx.datetime.Clock
@@ -68,6 +67,7 @@ buildConfig {
             }
         }
     })
+    buildConfigField("JVM_VERSION", provider { libs.versions.jvm.version.get() })
 }
 
 gitSemVer {
@@ -79,11 +79,10 @@ dependencies {
 }
 
 kotlin {
-    jvmToolchain(libs.versions.java.get().toInt())
+    jvmToolchain(libs.versions.jvm.target.get().toInt())
     applyDefaultHierarchyTemplate()
     explicitApi()
 
-    @OptIn(ExperimentalKotlinGradlePluginApi::class)
     compilerOptions {
         freeCompilerArgs.add("-Xexpect-actual-classes")
     }
@@ -111,6 +110,7 @@ kotlin {
                 implementation(libs.statelyConcurrentCollections)
                 implementation(libs.kmpIo)
                 implementation(libs.kotlinx.io)
+                implementation(libs.kotlinSemver)
             }
         }
         val commonTest by getting {
@@ -142,6 +142,7 @@ kotlin {
                 implementation(libs.kotest.runner.junit5)
                 implementation(libs.kotlinx.coroutines.slf4j)
                 implementation(libs.kotlinx.coroutines.debug)
+                implementation(libs.kotlinx.coroutines.test)
                 implementation(libs.kotlinx.dataframe)
                 // Required for report exports
                 // Used to convert dataframe rows to serializable objects
@@ -149,8 +150,6 @@ kotlin {
 
                 implementation(libs.koin.test)
                 implementation(libs.koin.test.junit5)
-
-                implementation(libs.kotlinSemver)
             }
         }
     }
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
index 973acb0b0731e4ef8cd6439f356158e6584a24c2..97261a23863e7f1c0bd6739d798f72434e1062c8 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
@@ -111,6 +111,7 @@ public class Temerity internal constructor(
    * Constructor for Temerity
    * @param temApiToken The [String] API token to use for the Temerity client
    */
+  @Suppress("Unused")
   internal constructor(temApiToken: String) : this(TemClientConfig.withToken(temApiToken))
 
   /**
@@ -299,80 +300,72 @@ public class Temerity internal constructor(
     }
   }
 
-  public override suspend fun getUsers(): List<User> =
-    withContext(libraryCoroutineDispatcher) {
-      val userRequest = platformApi.getUsers()
-      val response = userRequest.executeApiResponse<String>()
-      decodeResponseCatching(response)
-    }
-
-  public override suspend fun getUser(userId: Long): User =
-    withContext(libraryCoroutineDispatcher) {
-      val returnedUsers = getUsers()
-      returnedUsers.first { it.userId == userId }
-    }
+  public override suspend fun getUsers(): List<User> = withContext(libraryCoroutineDispatcher) {
+    val userRequest = platformApi.getUsers()
+    val response = userRequest.executeApiResponse<String>()
+    decodeResponseCatching(response)
+  }
 
-  public override suspend fun createUser(primaryIdentifier: String, newUser: NewUser): HttpResponse =
-    withContext(libraryCoroutineDispatcher) {
-      val validationMessage = newUser.validate()
-      if (validationMessage != "Valid") {
-        error(validationMessage)
-      } else {
-        val serializedNewUser = createJobScope(jsonProcessingDispatcher).run {
-          json.encodeToString(newUser)
-        }
-        val request = platformApi.createUser(primaryIdentifier, serializedNewUser)
-        request.getOrThrow()
-      }
-    }
+  public override suspend fun getUser(userId: Long): User = withContext(libraryCoroutineDispatcher) {
+    val returnedUsers = getUsers()
+    returnedUsers.first { it.userId == userId }
+  }
 
-  public override suspend fun updateUser(userId: Long, userUpdate: UserUpdate): HttpResponse =
-    withContext(libraryCoroutineDispatcher) {
-      val serializedUpdatedUser = createJobScope(jsonProcessingDispatcher).run {
-        json.encodeToString(userUpdate)
+  public override suspend fun createUser(primaryIdentifier: String, newUser: NewUser): HttpResponse = withContext(libraryCoroutineDispatcher) {
+    val validationMessage = newUser.validate()
+    if (validationMessage != "Valid") {
+      error(validationMessage)
+    } else {
+      val serializedNewUser = createJobScope(jsonProcessingDispatcher).run {
+        json.encodeToString(newUser)
       }
-      val request = platformApi.setUser(userId, serializedUpdatedUser)
+      val request = platformApi.createUser(primaryIdentifier, serializedNewUser)
       request.getOrThrow()
     }
+  }
 
-  public override suspend fun deleteUser(userId: Long): HttpResponse =
-    withContext(libraryCoroutineDispatcher) {
-      val request = platformApi.deleteUser(userId)
-      request.getOrThrow()
+  public override suspend fun updateUser(userId: Long, userUpdate: UserUpdate): HttpResponse = withContext(libraryCoroutineDispatcher) {
+    val serializedUpdatedUser = createJobScope(jsonProcessingDispatcher).run {
+      json.encodeToString(userUpdate)
     }
+    val request = platformApi.setUser(userId, serializedUpdatedUser)
+    request.getOrThrow()
+  }
 
-  public override suspend fun refreshCachedUserRoles(): List<String> =
-    withContext(libraryCoroutineDispatcher) {
-      val usersRequest = platformApi.getUsers()
-      val returnedUsersResponse = usersRequest.executeApiResponse<String>().getOrThrow()
-      val returnedUserList = createJobScope(jsonProcessingDispatcher).run {
-        json.decodeFromString<List<User>>(returnedUsersResponse)
-      }
-      val roleTypes = returnedUserList.map { it.userType }.distinct()
-      cachedUserRoleList.clear()
-      roleTypes.forEach {
-        cachedUserRoleList[it.hashCode()] = it
-      }
-      roleTypes
-    }
+  public override suspend fun deleteUser(userId: Long): HttpResponse = withContext(libraryCoroutineDispatcher) {
+    val request = platformApi.deleteUser(userId)
+    request.getOrThrow()
+  }
 
-  public override suspend fun getCachedUserRoles(refresh: Boolean): List<String> =
-    withContext(libraryCoroutineDispatcher) {
-      if (refresh) {
-        refreshCachedUserRoles()
-      }
-      val returnedUserRoles = cachedUserRoleList.values.toList()
-      returnedUserRoles.ifEmpty {
-        refreshCachedUserRoles()
-      }
+  public override suspend fun refreshCachedUserRoles(): List<String> = withContext(libraryCoroutineDispatcher) {
+    val usersRequest = platformApi.getUsers()
+    val returnedUsersResponse = usersRequest.executeApiResponse<String>().getOrThrow()
+    val returnedUserList = createJobScope(jsonProcessingDispatcher).run {
+      json.decodeFromString<List<User>>(returnedUsersResponse)
+    }
+    val roleTypes = returnedUserList.map { it.userType }.distinct()
+    cachedUserRoleList.clear()
+    roleTypes.forEach {
+      cachedUserRoleList.put(it.hashCode(), it)
     }
+    roleTypes
+  }
 
-  public override suspend fun getUserGroups(userId: Long): List<UserGroup> =
-    withContext(libraryCoroutineDispatcher) {
-      val request = platformApi.getUserGroups(userId)
-      val response = request.executeApiResponse<String>()
-      decodeResponseCatching(response)
+  public override suspend fun getCachedUserRoles(refresh: Boolean): List<String> = withContext(libraryCoroutineDispatcher) {
+    if (refresh) {
+      refreshCachedUserRoles()
     }
+    val returnedUserRoles = cachedUserRoleList.values.toList()
+    returnedUserRoles.ifEmpty {
+      refreshCachedUserRoles()
+    }
+  }
+
+  public override suspend fun getUserGroups(userId: Long): List<UserGroup> = withContext(libraryCoroutineDispatcher) {
+    val request = platformApi.getUserGroups(userId)
+    val response = request.executeApiResponse<String>()
+    decodeResponseCatching(response)
+  }
 
   public override suspend fun getUserGroupsOwned(userId: Long): List<UserGroup> = withContext(libraryCoroutineDispatcher) {
     val request = platformApi.getUserGroupsOwned(userId)
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
index f10a839c34c9e7dffb0ccc0f5ba63bc6c1ff3ef4..6251f96c2fcf2df7952b86e1aac37be3fd86dcaf 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
@@ -15,6 +15,8 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
+@file:Suppress("MemberVisibilityCanBePrivate")
+
 package edu.ucsc.its.temerity.extensions.datetime
 
 import kotlinx.datetime.Clock
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt
index baf4c7d887f311954ef2b7cd7fe29e6472e57d47..6389ef182bcf2867d8aebb4521ff7cfca8ed1b5e 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt
@@ -56,11 +56,10 @@ internal class FilesystemLogWriter internal constructor(
   }
 
   companion object {
-    operator fun invoke(block: Builder.() -> Unit) =
-      with(FilesystemLogWriterBuilder()) {
-        block(this)
-        build()
-      }
+    operator fun invoke(block: Builder.() -> Unit) = with(FilesystemLogWriterBuilder()) {
+      block(this)
+      build()
+    }
   }
 
   interface Builder {
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt
index 937184a81effcd30fa56e2e7c6035ca4ef089653..c007a54eb15a376260a7090304095527df34c294 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt
@@ -26,12 +26,10 @@ import kotlinx.datetime.format
  * Formats a [LocalDate] object as a string in the format used by the YuJa API for audit log requests.
  * @return A string representation of the date in the format "dd/MM/yyyy".
  */
-internal fun LocalDate.applyAuditLogFormat(): String =
-  format(AUDIT_LOG_REQUEST_DATE_FORMAT)
+internal fun LocalDate.applyAuditLogFormat(): String = format(AUDIT_LOG_REQUEST_DATE_FORMAT)
 
 /**
  * Formats a [LocalDate] object as a string in the format used by the YuJa API for sessions.
  * @return A string representation of the date in the format "dd-MM-yyyy".
  */
-internal fun LocalDate.applyScheduledSessionDateFormat(): String =
-  format(SCHEDULED_SESSION_DATE_FORMAT)
+internal fun LocalDate.applyScheduledSessionDateFormat(): String = format(SCHEDULED_SESSION_DATE_FORMAT)
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt
index d11bb8f44f4a9c7949808ddded9343f09f56e290..1b15cc11b595bf9a03b2f503136c03539f55395e 100644
--- a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt
@@ -33,18 +33,15 @@ private fun withBrightColor(messageStringFormatter: MessageStringFormatter = Def
 internal class ColorFormatter(
   messageStringFormatter: MessageStringFormatter = DefaultFormatter,
 ) : WrappingFormatter(messageStringFormatter) {
-  override fun prefix(severity: Severity?, tag: Tag?, message: Message) =
-    severity?.toAnsiColor() ?: ""
+  override fun prefix(severity: Severity?, tag: Tag?, message: Message) = severity?.toAnsiColor() ?: ""
 
-  override fun suffix(severity: Severity?, tag: Tag?, message: Message) =
-    resetColor()
+  override fun suffix(severity: Severity?, tag: Tag?, message: Message) = resetColor()
 }
 
 internal class BrightColorFormatter(
   messageStringFormatter: MessageStringFormatter = DefaultFormatter,
 ) : WrappingFormatter(messageStringFormatter) {
-  override fun prefix(severity: Severity?, tag: Tag?, message: Message) =
-    severity?.toBrightAnsiColor() ?: ""
+  override fun prefix(severity: Severity?, tag: Tag?, message: Message) = severity?.toBrightAnsiColor() ?: ""
 
   override fun suffix(severity: Severity?, tag: Tag?, message: Message) = resetColor()
 }
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt
index efec7524c820719f11fafb4a3f51598cb40545d1..4c6b0b83626df5ed163ccccab912786e1a3196ba 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt
@@ -17,17 +17,10 @@
  */
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.core.Temerity
 import kotlinx.coroutines.runBlocking
 
-private class DevDeviceApiTests : TemerityFunSpec() {
+private class DevDeviceApiTests : TemDevFunSpec() {
   init {
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
-
     test("4.2.1 - PlatformClient can fetch a list of devices") {
       runBlocking {
         val returnedDevices = testTemerity.getDevices()
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt
index 521acc2a60bb99349dfffc7102a1e740e8bb94f8..41be5fea20aa996d8fef57a874c3e3b210090276 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt
@@ -17,17 +17,10 @@
  */
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.core.Temerity
 import kotlinx.coroutines.runBlocking
 
-private class DevGroupApiTests : TemerityFunSpec() {
+private class DevGroupApiTests : TemDevFunSpec() {
   init {
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
-
     test("2.2.1 - PlatformClient can fetch a list of groups") {
       runBlocking {
         val returnedGroups = testTemerity.getGroups()
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt
index 43885a0c174e9dabb24a2fffc3bb777ff28d2e0e..07c2e7e8027e19d3444265dc92a94a14b61bc3fc 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt
@@ -20,7 +20,6 @@ package edu.ucsc.its.temerity.test
 import co.touchlab.kermit.Logger
 import com.skydoves.sandwich.StatusCode
 import com.skydoves.sandwich.ktor.getStatusCode
-import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.model.NewUser
 import edu.ucsc.its.temerity.model.UserUpdate
 import io.kotest.common.ExperimentalKotest
@@ -28,17 +27,11 @@ import io.kotest.matchers.shouldBe
 import io.ktor.http.HttpStatusCode
 import kotlinx.coroutines.runBlocking
 
-private class DevUserApiTests : TemerityFunSpec() {
+private class DevUserApiTests : TemDevFunSpec() {
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
-
     test("1.2.1 - PlatformClient can fetch a list of users") {
       runBlocking {
         val returnedUsers = testTemerity.getUsers()
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
index aa3e10bdf99fe3a9071122dffa4839d8bd57ed9b..64b5cbc5ff3176921774331c3982695df4e03c86 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
@@ -15,25 +15,21 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
+@file:OptIn(KotestInternal::class)
+
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.common.ExperimentalKotest
+import io.kotest.common.KotestInternal
 import io.kotest.engine.runBlocking
 import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.minutes
 
-private class DevUtilityTests : TemerityFunSpec() {
-
+private class DevUtilityTests : TemDevFunSpec() {
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
     timeout = 5.minutes.inWholeMilliseconds
 
     test("Delete all Canvas Test Student Users").config(blockingTest = true, timeout = 12.hours) {
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ModelTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ModelTests.kt
index cec8e213c84032918802ac2e63629c41aa607c97..7ef1162569b947f7fe90f38ae471a216b2e198bb 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ModelTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ModelTests.kt
@@ -21,7 +21,6 @@ import edu.ucsc.its.temerity.model.EventType
 import io.kotest.common.ExperimentalKotest
 
 private class ModelTests : TemerityFunSpec() {
-
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt
index ec0e28666aa40da5528082abbda2e9fc1d2789a4..87a59821f0318ea4551199a3d0f5a614608a5df0 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt
@@ -17,7 +17,6 @@
  */
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.model.AuditLogSortOrder.NEW_FIRST
 import edu.ucsc.its.temerity.model.EventType.AUTOMATED_SESSION_FAILED_TO_START
 import edu.ucsc.its.temerity.model.EventType.AUTOMATED_SESSION_MONITOR
@@ -47,7 +46,7 @@ import kotlin.uuid.ExperimentalUuidApi
 import kotlin.uuid.Uuid
 
 @OptIn(ExperimentalUuidApi::class)
-private class ProdReportTests : TemerityFunSpec() {
+private class ProdReportTests : TemProdFunSpec() {
 
   init {
     @OptIn(ExperimentalKotest::class)
@@ -55,12 +54,6 @@ private class ProdReportTests : TemerityFunSpec() {
 
     val today = currentDate()
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureProdEnvironment(dotenv)
-      }
-    }
-
     timeout = 5.minutes.inWholeMilliseconds
 
     test("PlatformClient can fetch a list of devices") {
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
index 632ec391b6437708d66aeb8e13061be3e9c6426d..4dfbc5f4ee81950826933234163307c86fa64412 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
@@ -15,26 +15,22 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
+@file:OptIn(KotestInternal::class)
+
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.common.ExperimentalKotest
+import io.kotest.common.KotestInternal
 import io.kotest.engine.runBlocking
 import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.minutes
 
-private class ProdUtilityTests : TemerityFunSpec() {
+private class ProdUtilityTests : TemProdFunSpec() {
 
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureProdEnvironment(dotenv)
-      }
-    }
-
     timeout = 5.minutes.inWholeMilliseconds
 
     test("Delete all Canvas Test Student Users").config(blockingTest = true, timeout = 12.hours) {
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
index 8ae6e130ed5eb1bc61d3312c4c2820c213f8d063..c3c4f8cc5c9e9878129c86fe1ec9c7bd5c771d77 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
@@ -42,17 +42,11 @@ import kotlin.uuid.ExperimentalUuidApi
 import kotlin.uuid.Uuid
 
 @OptIn(ExperimentalUuidApi::class)
-private class TemerityDevTest : TemerityFunSpec() {
+private class TemerityDevTest : TemDevFunSpec() {
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
-
     test("Temerity client returns a correctly-formatted version String") {
       val returnedVersion = testTemerity.version.toVersion()
       kermit.d { "Returned client version: $returnedVersion" }
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt
index d343b206640489762e84e2164050d4ee1e883571..f2c8893b9ef6c7c056651f846f405426c55c93d9 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt
@@ -15,6 +15,8 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
+@file:OptIn(KotestInternal::class)
+
 package edu.ucsc.its.temerity.test
 
 import edu.ucsc.its.temerity.core.TemClientConfig
@@ -23,7 +25,9 @@ import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
 import io.github.cdimascio.dotenv.Configuration
 import io.github.cdimascio.dotenv.Dotenv
 import io.github.cdimascio.dotenv.dotenv
+import io.kotest.common.KotestInternal
 import io.kotest.core.extensions.TestCaseExtension
+import io.kotest.core.spec.Spec
 import io.kotest.core.spec.style.FunSpec
 import io.kotest.core.test.TestCase
 import io.kotest.core.test.TestResult
@@ -68,11 +72,10 @@ private class KoinExtensionImpl(
     execute(testCase)
   }
 
-  private fun TestCase.isApplicable() =
-    mode == KoinLifecycleMode.Root &&
-      isRootTest() ||
-      mode == KoinLifecycleMode.Test &&
-      type == TestType.Test
+  private fun TestCase.isApplicable() = mode == KoinLifecycleMode.Root &&
+    isRootTest() ||
+    mode == KoinLifecycleMode.Test &&
+    type == TestType.Test
 }
 
 enum class KoinLifecycleMode {
@@ -104,3 +107,21 @@ internal abstract class TemerityFunSpec : FunSpec() {
   internal val kermit: KermitLogger = createLogger()
   internal lateinit var testTemerity: Temerity
 }
+
+internal abstract class TemDevFunSpec : TemerityFunSpec() {
+  override suspend fun beforeSpec(spec: Spec) {
+    testTemerity = Temerity {
+      configureDevEnvironment(dotenv)
+    }
+    super.beforeSpec(spec)
+  }
+}
+
+internal abstract class TemProdFunSpec : TemerityFunSpec() {
+  override suspend fun beforeSpec(spec: Spec) {
+    testTemerity = Temerity {
+      configureProdEnvironment(dotenv)
+    }
+    super.beforeSpec(spec)
+  }
+}
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/konsist/TestKonsistTest.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/konsist/TestKonsistTest.kt
index b8920bc3153e0ab7b066652e0464589d0b6060c1..e988c97a74fe019b386df1321ab401a7d2c87427 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/konsist/TestKonsistTest.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/konsist/TestKonsistTest.kt
@@ -27,7 +27,7 @@ class TestKonsistTest : FunSpec() {
 
   init {
 
-    beforeTest {
+    beforeSpec {
       konsist = Konsist.scopeFromProject()
     }