Repository: takahirom/roborazzi
Pull request #544 (2024-11-06 02:41:42)

Make RoborazziContextImpl.options ExperimentalRoborazziApi


Patch:
diff --git a/.github/workflows/dependency-diff.yaml b/.github/workflows/dependency-diff.yaml
index c5ff1f3a..547ae440 100644
--- a/.github/workflows/dependency-diff.yaml
+++ b/.github/workflows/dependency-diff.yaml
@@ -20,8 +20,6 @@ jobs:
           modules: |
             roborazzi-compose-ios|commonMainImplementationDependenciesMetadata
             roborazzi-compose-desktop|commonMainImplementationDependenciesMetadata
-            roborazzi-ai-gemini|commonMainImplementationDependenciesMetadata
-            roborazzi-ai-openai|commonMainImplementationDependenciesMetadata
             roborazzi
             roborazzi-compose
             roborazzi-compose-preview-scanner-support
diff --git a/README.md b/README.md
index 24cb19c5..f487d3f7 100644
--- a/README.md
+++ b/README.md
@@ -1011,115 +1011,6 @@ Currently, we don't support all the annotation options provided by the Compose P
 You can check the supported annotations in the [source code](https://github.com/takahirom/roborazzi/blob/main/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt).
 We are looking forward to your contributions to support more annotation options.
 
-</div>
-<div name="topic_ai_powered_image_assertion">
-
-<!-- Generated by docs/topics/ai_powered_image_assertion.md. Do not edit this file. -->
-# Experimental AI-Powered Image Assertion
-
-Roborazzi supports AI-powered image assertion.
-AI-powered image assertion is an experimental feature. Screenshot tests are a great way to verify your app's UI, but verifying the content of the images can be a tedious and time-consuming task. This manual effort reduces scalability. Roborazzi can help automate this process through AI-powered image assertion, making it more efficient and scalable.
-
-There are two new library modules: `io.github.takahirom.roborazzi:roborazzi-ai-gemini` and `io.github.takahirom.roborazzi:roborazzi-ai-openai` for AI-powered image assertion.
-
-`roborazzi-ai-gemini` leverages [Gemini](https://gemini.google.com/) and [generative-ai-kmp](https://github.com/PatilShreyas/generative-ai-kmp), while `roborazzi-ai-openai` utilizes the [OpenAI API](https://platform.openai.com/) through raw HTTP API calls implemented with Ktor and KotlinX Serialization
-
-```kotlin
-...
-@get:Rule
-val composeTestRule = createAndroidComposeRule<MainActivity>()
-
-@get:Rule
-val roborazziRule = RoborazziRule(
-  options = RoborazziRule.Options(
-    roborazziOptions = RoborazziOptions(
-      compareOptions = RoborazziOptions.CompareOptions(
-        aiAssertionOptions = AiAssertionOptions(
-          aiAssertionModel = GeminiAiAssertionModel(
-            // DO NOT HARDCODE your API key in your code.
-            // This is an example passing API Key through unitTests.all{ environment(key, value) }
-            apiKey = System.getenv("gemini_api_key") ?: ""
-          ),
-        )
-      )
-    )
-  )
-)
-
-@Test
-fun captureWithAi() {
-  ROBORAZZI_DEBUG = true
-  onView(ViewMatchers.isRoot())
-    .captureRoboImage(
-      roborazziOptions = provideRoborazziContext().options.addedAiAssertions(
-        AiAssertionOptions.AiAssertion(
-          assertionPrompt = "it should have PREVIOUS button",
-          requiredFulfillmentPercent = 90,
-        ),
-        AiAssertionOptions.AiAssertion(
-          assertionPrompt = "it should show First Fragment",
-          requiredFulfillmentPercent = 90,
-        )
-      )
-    )
-}
-```
-
-## Behavior of AI-Powered Image Assertion
-
-AI-Powered Image Assertion runs only when the images are different. If the images are the same, AI-Powered Image Assertion is skipped.  
-This is because AI-Powered Image Assertion can be slow and expensive.
-
-## Manual Image Assertion
-
-You can use manual image assertion with Roborazzi. This allows you to utilize local LLMs or other LLMs. Manual Image Assertion doesn't require adding any dependencies other than Roborazzi itself.
-
-You must provide the `AiAssertionModel` to `RoborazziOptions` to use manual image assertion.
-
-```kotlin
-interface AiAssertionModel {
-  fun assert(
-    referenceImageFilePath: String,
-    comparisonImageFilePath: String,
-    actualImageFilePath: String,
-    aiAssertionOptions: AiAssertionOptions
-  ): AiAssertionResults
-}
-```
-
-```kotlin
-compareOptions = RoborazziOptions.CompareOptions(
-  aiAssertionOptions = AiAssertionOptions(
-    aiAssertionModel = object : AiAssertionOptions.AiAssertionModel {
-      override fun assert(
-        comparisonImageFilePath: String,
-        aiAssertionOptions: AiAssertionOptions
-      ): AiAssertionResults {
-        // You can use any LLMs here to create AiAssertionResults
-        return AiAssertionResults(
-          aiAssertionResults = aiAssertionOptions.aiAssertions.map { assertion ->
-            AiAssertionResult(
-              assertionPrompt = assertion.assertionPrompt,
-              fulfillmentPercent = fulfillmentPercent,
-              requiredFulfillmentPercent = assertion.requiredFulfillmentPercent,
-              failIfNotFulfilled = assertion.failIfNotFulfilled,
-              explanation = "This is a manual test.",
-            )
-          }
-        )
-      }
-    },
-    aiAssertions = listOf(
-      AiAssertionOptions.AiAssertion(
-        assertionPrompt = "it should have PREVIOUS button",
-        requiredFulfillmentPercent = 90,
-      ),
-    ),
-  )
-)
-          ...
-```
-
 </div>
 <div name="topic_idea_plugin">
 
@@ -1261,6 +1152,13 @@ kotlin {
       }
     }
     ...
+
+// Roborazzi Desktop support uses Context Receivers
+    tasks.withType<KotlinCompile>().configureEach {
+      kotlinOptions {
+        freeCompilerArgs += "-Xcontext-receivers"
+      }
+    }
 ```
 
 Test target Composable function
@@ -1339,8 +1237,7 @@ The sample image
 <!-- Generated by docs/topics/gradle_properties_options.md. Do not edit this file. -->
 # Roborazzi gradle.properties Options and Recommendations
 
-You can configure the following options in your `gradle.properties` file.
-You can also use `-P` to set the options in the command line. For example, `./gradlew test -Proborazzi.test.record=true`.
+You can configure the following options in your `gradle.properties` file:
 
 ## roborazzi.test
 
@@ -1383,15 +1280,6 @@ This option enables you to define the naming strategy for the recorded image. Th
 roborazzi.record.namingStrategy=testClassAndMethod
 ```
 
-## roborazzi.cleanupOldScreenshots
-
-This option allows you to clean up old screenshots. By default, this option is set to false.
-The reason why Roborazzi does not delete old screenshots by default is that Roborazzi doesn't know if you are running filtered tests or not. If you are running filtered tests, Roborazzi will delete the screenshots that are not related to the current test run.
-
-```
-roborazzi.cleanupOldScreenshots=true
-```
-
 ## Robolectric Options
 
 ### robolectric.pixelCopyRenderMode
@@ -1542,11 +1430,6 @@ android {
 It is discussed in [this issue](https://github.com/takahirom/roborazzi/issues/272).
 Additionally, it might be worth trying to run your tests with VisualVM to monitor memory usage and identify potential leaks.
 
-### Q: [IDEA Plugin] Roborazzi Gradle task is not displayed in Tool Window.
-
-**A:** It is discussed in [this issue](https://github.com/takahirom/roborazzi/issues/493).  
-To enable the display of Roborazzi tasks, please enable ***Configure all Gradle tasks during Gradle Sync (this can make Gradle Sync slower)*** in the Settings | Experimental | Gradle.  
-<img src="https://github.com/user-attachments/assets/67fbb2a8-6b2a-458c-bb31-99025f1c1cab" width="800" />
 </div>
 
 ### LICENSE
diff --git a/README.template.md b/README.template.md
index a80ea111..84bbd4d9 100644
--- a/README.template.md
+++ b/README.template.md
@@ -9,8 +9,6 @@
 </div>
 <div name="topic_preview_support">
 </div>
-<div name="topic_ai_powered_image_assertion">
-</div>
 <div name="topic_idea_plugin">
 </div>
 <div name="topic_compose_multiplatform">
diff --git a/docs/roborazzi-docs.tree b/docs/roborazzi-docs.tree
index 5c426f3c..30e3e59b 100644
--- a/docs/roborazzi-docs.tree
+++ b/docs/roborazzi-docs.tree
@@ -11,7 +11,6 @@
     <toc-element topic="build_setup.md"/>
     <toc-element topic="how_to_use.md"/>
     <toc-element topic="preview_support.md"/>
-    <toc-element topic="ai_powered_image_assertion.md"/>
     <toc-element topic="idea_plugin.md"/>
     <toc-element topic="compose_multiplatform.md"/>
     <toc-element topic="gradle_properties_options.md"/>
diff --git a/docs/topics/ai_powered_image_assertion.md b/docs/topics/ai_powered_image_assertion.md
deleted file mode 100644
index 87c62b6b..00000000
--- a/docs/topics/ai_powered_image_assertion.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# Experimental AI-Powered Image Assertion
-
-Roborazzi supports AI-powered image assertion.
-AI-powered image assertion is an experimental feature. Screenshot tests are a great way to verify your app's UI, but verifying the content of the images can be a tedious and time-consuming task. This manual effort reduces scalability. Roborazzi can help automate this process through AI-powered image assertion, making it more efficient and scalable.
-
-There are two new library modules: `io.github.takahirom.roborazzi:roborazzi-ai-gemini` and `io.github.takahirom.roborazzi:roborazzi-ai-openai` for AI-powered image assertion.
-
-`roborazzi-ai-gemini` leverages [Gemini](https://gemini.google.com/) and [generative-ai-kmp](https://github.com/PatilShreyas/generative-ai-kmp), while `roborazzi-ai-openai` utilizes the [OpenAI API](https://platform.openai.com/) through raw HTTP API calls implemented with Ktor and KotlinX Serialization
-
-```kotlin
-...
-@get:Rule
-val composeTestRule = createAndroidComposeRule<MainActivity>()
-
-@get:Rule
-val roborazziRule = RoborazziRule(
-  options = RoborazziRule.Options(
-    roborazziOptions = RoborazziOptions(
-      compareOptions = RoborazziOptions.CompareOptions(
-        aiAssertionOptions = AiAssertionOptions(
-          aiAssertionModel = GeminiAiAssertionModel(
-            // DO NOT HARDCODE your API key in your code.
-            // This is an example passing API Key through unitTests.all{ environment(key, value) }
-            apiKey = System.getenv("gemini_api_key") ?: ""
-          ),
-        )
-      )
-    )
-  )
-)
-
-@Test
-fun captureWithAi() {
-  ROBORAZZI_DEBUG = true
-  onView(ViewMatchers.isRoot())
-    .captureRoboImage(
-      roborazziOptions = provideRoborazziContext().options.addedAiAssertions(
-        AiAssertionOptions.AiAssertion(
-          assertionPrompt = "it should have PREVIOUS button",
-          requiredFulfillmentPercent = 90,
-        ),
-        AiAssertionOptions.AiAssertion(
-          assertionPrompt = "it should show First Fragment",
-          requiredFulfillmentPercent = 90,
-        )
-      )
-    )
-}
-```
-
-## Behavior of AI-Powered Image Assertion
-
-AI-Powered Image Assertion runs only when the images are different. If the images are the same, AI-Powered Image Assertion is skipped.  
-This is because AI-Powered Image Assertion can be slow and expensive.
-
-## Manual Image Assertion
-
-You can use manual image assertion with Roborazzi. This allows you to utilize local LLMs or other LLMs. Manual Image Assertion doesn't require adding any dependencies other than Roborazzi itself.
-
-You must provide the `AiAssertionModel` to `RoborazziOptions` to use manual image assertion.
-
-```kotlin
-interface AiAssertionModel {
-  fun assert(
-    referenceImageFilePath: String,
-    comparisonImageFilePath: String,
-    actualImageFilePath: String,
-    aiAssertionOptions: AiAssertionOptions
-  ): AiAssertionResults
-}
-```
-
-```kotlin
-compareOptions = RoborazziOptions.CompareOptions(
-  aiAssertionOptions = AiAssertionOptions(
-    aiAssertionModel = object : AiAssertionOptions.AiAssertionModel {
-      override fun assert(
-        comparisonImageFilePath: String,
-        aiAssertionOptions: AiAssertionOptions
-      ): AiAssertionResults {
-        // You can use any LLMs here to create AiAssertionResults
-        return AiAssertionResults(
-          aiAssertionResults = aiAssertionOptions.aiAssertions.map { assertion ->
-            AiAssertionResult(
-              assertionPrompt = assertion.assertionPrompt,
-              fulfillmentPercent = fulfillmentPercent,
-              requiredFulfillmentPercent = assertion.requiredFulfillmentPercent,
-              failIfNotFulfilled = assertion.failIfNotFulfilled,
-              explanation = "This is a manual test.",
-            )
-          }
-        )
-      }
-    },
-    aiAssertions = listOf(
-      AiAssertionOptions.AiAssertion(
-        assertionPrompt = "it should have PREVIOUS button",
-        requiredFulfillmentPercent = 90,
-      ),
-    ),
-  )
-)
-          ...
-```
diff --git a/docs/topics/compose_multiplatform.md b/docs/topics/compose_multiplatform.md
index 45e250af..4f0dbfb8 100644
--- a/docs/topics/compose_multiplatform.md
+++ b/docs/topics/compose_multiplatform.md
@@ -122,6 +122,13 @@ kotlin {
       }
     }
     ...
+
+// Roborazzi Desktop support uses Context Receivers
+    tasks.withType<KotlinCompile>().configureEach {
+      kotlinOptions {
+        freeCompilerArgs += "-Xcontext-receivers"
+      }
+    }
 ```
 
 Test target Composable function
diff --git a/docs/topics/faq.md b/docs/topics/faq.md
index 414ec06f..f9434c08 100644
--- a/docs/topics/faq.md
+++ b/docs/topics/faq.md
@@ -126,9 +126,3 @@ android {
 ```
 It is discussed in [this issue](https://github.com/takahirom/roborazzi/issues/272).
 Additionally, it might be worth trying to run your tests with VisualVM to monitor memory usage and identify potential leaks.
-
-### Q: [IDEA Plugin] Roborazzi Gradle task is not displayed in Tool Window.
-
-**A:** It is discussed in [this issue](https://github.com/takahirom/roborazzi/issues/493).  
-To enable the display of Roborazzi tasks, please enable ***Configure all Gradle tasks during Gradle Sync (this can make Gradle Sync slower)*** in the Settings | Experimental | Gradle.  
-<img src="https://github.com/user-attachments/assets/67fbb2a8-6b2a-458c-bb31-99025f1c1cab" width="800" />
\ No newline at end of file
diff --git a/docs/topics/gradle_properties_options.md b/docs/topics/gradle_properties_options.md
index 2146e2b7..a209c964 100644
--- a/docs/topics/gradle_properties_options.md
+++ b/docs/topics/gradle_properties_options.md
@@ -1,7 +1,6 @@
 # Roborazzi gradle.properties Options and Recommendations
 
-You can configure the following options in your `gradle.properties` file.
-You can also use `-P` to set the options in the command line. For example, `./gradlew test -Proborazzi.test.record=true`.
+You can configure the following options in your `gradle.properties` file:
 
 ## roborazzi.test
 
@@ -44,15 +43,6 @@ This option enables you to define the naming strategy for the recorded image. Th
 roborazzi.record.namingStrategy=testClassAndMethod
 ```
 
-## roborazzi.cleanupOldScreenshots
-
-This option allows you to clean up old screenshots. By default, this option is set to false.
-The reason why Roborazzi does not delete old screenshots by default is that Roborazzi doesn't know if you are running filtered tests or not. If you are running filtered tests, Roborazzi will delete the screenshots that are not related to the current test run.
-
-```
-roborazzi.cleanupOldScreenshots=true
-```
-
 ## Robolectric Options
 
 ### robolectric.pixelCopyRenderMode
diff --git a/gradle.properties b/gradle.properties
index 8b345100..fc414c1c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-VERSION_NAME=1.32.0
+VERSION_NAME=1.29.0
 GROUP=io.github.takahirom.roborazzi
 # Project-wide Gradle settings.
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1d030df6..24bd9b06 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,7 +8,6 @@ mavenPublish = "0.25.3"
 composeCompiler = "1.5.10"
 composeMultiplatform = "1.6.2"
 robolectric = "4.12.2"
-generativeaiGoogle = "0.9.0-1.0.1"
 robolectric-android-all = "Q-robolectric-5415296"
 
 roborazzi-for-replacing-by-include-build = "1.0.0"
@@ -21,7 +20,7 @@ androidx-compose-foundation = "1.6.8"
 androidx-compose-runtime = "1.6.8"
 androidx-compose-ui = "1.4.0"
 androidx-compose-ui-test = "1.6.8"
-androidx-compose-ui-test-junit4 = "1.7.5"
+androidx-compose-ui-test-junit4 = "1.7.4"
 androidx-compose-ui-test-manifest = "1.4.0"
 androidx-compose-ui-tooling = "1.4.0"
 androidx-compose-ui-tooling-preview = "1.4.0"
@@ -36,9 +35,8 @@ kim = "0.17.7"
 dropbox-differ = "0.3.0"
 google-android-material = "1.5.0"
 junit = "4.13.2"
-ktor = "2.3.11"
+ktor-serialization-kotlinx-xml = "2.3.11"
 kotlinx-serialization = "1.6.3"
-kotlinx-coroutines = "1.6.0"
 squareup-okhttp = "5.0.0-alpha.11"
 kotlinx-io = "0.3.3"
 webjar-material-design-icons = "4.0.0"
@@ -46,7 +44,7 @@ webjar-materialize = "1.0.0"
 webjars-locator-lite = "0.0.6"
 webpImageio = "0.3.3"
 
-composable-preview-scanner = "0.4.0"
+composable-preview-scanner = "0.3.2"
 
 [libraries]
 roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi-for-replacing-by-include-build" }
@@ -58,8 +56,6 @@ kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" }
 kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" }
 kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit" }
 
-generativeai-google = { module = "dev.shreyaspatil.generativeai:generativeai-google", version.ref = "generativeaiGoogle" }
-
 # for sample
 composable-preview-scanner = { module = "io.github.sergio-sastre.ComposablePreviewScanner:android", version.ref = "composable-preview-scanner" }
 
@@ -95,14 +91,7 @@ dropbox-differ = { module = "com.dropbox.differ:differ", version.ref = "dropbox-
 google-android-material = { module = "com.google.android.material:material", version.ref = "google-android-material" }
 junit = { module = "junit:junit", version.ref = "junit" }
 kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
-ktor-serialization-kotlinx-xml = { module = "io.ktor:ktor-serialization-kotlinx-xml", version.ref = "ktor" }
-ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
-ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
-ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
-ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
-ktor-serialization-gson = { module = "io.ktor:ktor-serialization-gson", version.ref = "ktor" }
-ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
+ktor-serialization-kotlinx-xml = { module = "io.ktor:ktor-serialization-kotlinx-xml", version.ref = "ktor-serialization-kotlinx-xml" }
 squareup-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "squareup-okhttp" }
 squareup-okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines", version.ref = "squareup-okhttp" }
 robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt
index e0fa87c3..15ce447e 100644
--- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt
+++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt
@@ -71,7 +71,7 @@ class RoborazziContextImpl {
   val outputDirectory: String
     get() = ruleOverrideOutputDirectory ?: roborazziSystemPropertyOutputDirectory()
 
-  @InternalRoborazziApi
+  @ExperimentalRoborazziApi
   val options: RoborazziOptions
     get() = ruleOverrideRoborazziOptions ?: RoborazziOptions()
 
diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt
index 8c24bae7..e03c340c 100644
--- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt
+++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt
@@ -100,10 +100,8 @@ data class RoborazziOptions(
     val outputDirectoryPath: String = roborazziSystemPropertyOutputDirectory(),
     val imageComparator: ImageComparator = DefaultImageComparator,
     val comparisonStyle: ComparisonStyle = ComparisonStyle.Grid(),
-    val aiAssertionOptions: AiAssertionOptions? = null,
     val resultValidator: (result: ImageComparator.ComparisonResult) -> Boolean = DefaultResultValidator,
   ) {
-
     @ExperimentalRoborazziApi
     sealed interface ComparisonStyle {
       @ExperimentalRoborazziApi
@@ -153,44 +151,6 @@ data class RoborazziOptions(
         } else {
           JsonOutputCaptureResultReporter().report(captureResult, roborazziTaskType)
         }
-        AiCaptureResultReporter().report(captureResult, roborazziTaskType)
-      }
-    }
-
-    class AiCaptureResultReporter : CaptureResultReporter {
-      override fun report(captureResult: CaptureResult, roborazziTaskType: RoborazziTaskType) {
-        val aiResult = when (captureResult) {
-          is CaptureResult.Changed -> {
-            captureResult.aiAssertionResults
-          }
-
-          is CaptureResult.Added -> {
-            captureResult.aiAssertionResults
-          }
-
-          else -> {
-            null
-          }
-        }
-        aiResult?.aiAssertionResults
-          ?.filter { conditionResult -> conditionResult.requiredFulfillmentPercent != null && conditionResult.failIfNotFulfilled }
-          ?.forEach { conditionResult ->
-            if (conditionResult.fulfillmentPercent < conditionResult.requiredFulfillmentPercent!!) {
-              throw AssertionError(
-                "The generated image did not meet the required prompt fulfillment percentage.\n" +
-                  "* Condition:\n" +
-                  "  - assertionPrompt: ${conditionResult.assertionPrompt}\n" +
-                  "  - failIfNotFulfilled: ${conditionResult.failIfNotFulfilled}\n" +
-                  "  - requiredFulfillmentPercent: ${conditionResult.requiredFulfillmentPercent}\n" +
-                  "* Result:\n" +
-                  "  - fulfillmentPercent: ${conditionResult.fulfillmentPercent}\n" +
-                  "  - explanation: ${conditionResult.explanation}\n" +
-                  "  - referenceFile: ${captureResult.goldenFile}\n" +
-                  "  - compareFile: ${captureResult.compareFile}\n" +
-                  "  - actualFile: ${captureResult.actualFile}\n"
-              )
-            }
-          }
       }
     }
 
@@ -251,30 +211,6 @@ data class RoborazziOptions(
   }
 
   internal val shouldTakeBitmap: Boolean = captureType.shouldTakeScreenshot()
-
-  @ExperimentalRoborazziApi
-  fun addedAiAssertion(
-    assertionPrompt: String,
-    requiredFulfillmentPercent: Int
-  ): RoborazziOptions {
-    return addedAiAssertions(
-      AiAssertionOptions.AiAssertion(
-        assertionPrompt = assertionPrompt,
-        requiredFulfillmentPercent = requiredFulfillmentPercent
-      )
-    )
-  }
-
-  @ExperimentalRoborazziApi
-  fun addedAiAssertions(vararg assertions: AiAssertionOptions.AiAssertion): RoborazziOptions {
-    return copy(
-      compareOptions = compareOptions.copy(
-        aiAssertionOptions = compareOptions.aiAssertionOptions!!.copy(
-          aiAssertions = compareOptions.aiAssertionOptions.aiAssertions + assertions
-        )
-      )
-    )
-  }
 }
 
 expect fun canScreenshot(): Boolean
diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt
index d0b3cd61..748ebee2 100644
--- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt
+++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt
@@ -88,16 +88,15 @@ fun processOutputImageAndReport(
     // Only used by CaptureResult.Changed
     var diffPercentage: Float? = null
 
-    val compareOptions = roborazziOptions.compareOptions
     val changed = if (height == goldenRoboCanvas.height && width == goldenRoboCanvas.width) {
       val comparisonResult: ImageComparator.ComparisonResult =
         newRoboCanvas.differ(
           other = goldenRoboCanvas,
           resizeScale = resizeScale,
-          imageComparator = compareOptions.imageComparator
+          imageComparator = roborazziOptions.compareOptions.imageComparator
         )
       diffPercentage = comparisonResult.pixelDifferences.toFloat() / comparisonResult.pixelCount
-      val changed = !compareOptions.resultValidator(comparisonResult)
+      val changed = !roborazziOptions.compareOptions.resultValidator(comparisonResult)
       reportLog("${goldenFile.name} The differ result :$comparisonResult changed:$changed")
       changed
     } else {
@@ -107,7 +106,7 @@ fun processOutputImageAndReport(
 
     val result: CaptureResult = if (changed) {
       val comparisonFile = File(
-        compareOptions.outputDirectoryPath,
+        roborazziOptions.compareOptions.outputDirectoryPath,
         goldenFile.nameWithoutExtension + "_compare." + goldenFile.extension
       )
       val comparisonCanvas = comparisonCanvasFactory(
@@ -134,7 +133,7 @@ fun processOutputImageAndReport(
         goldenFile
       } else {
         File(
-          compareOptions.outputDirectoryPath,
+          roborazziOptions.compareOptions.outputDirectoryPath,
           goldenFile.nameWithoutExtension + "_actual." + goldenFile.extension
         )
       }
@@ -145,17 +144,6 @@ fun processOutputImageAndReport(
           contextData = contextData,
           imageIoFormat = recordOptions.imageIoFormat,
         )
-      val aiOptions = compareOptions.aiAssertionOptions
-      val aiResult = if (aiOptions != null && aiOptions.aiAssertions.isNotEmpty()) {
-        aiOptions.aiAssertionModel.assert(
-          referenceImageFilePath = goldenFile.absolutePath,
-          comparisonImageFilePath = comparisonFile.absolutePath,
-          actualImageFilePath = actualFile.absolutePath,
-          aiAssertionOptions = aiOptions
-        )
-      } else {
-        null
-      }
       debugLog {
         "processOutputImageAndReport(): actualCanvas is saved " +
           "actualFile:${actualFile.absolutePath}"
@@ -167,7 +155,6 @@ fun processOutputImageAndReport(
           goldenFile = goldenFile.absolutePath,
           timestampNs = System.nanoTime(),
           diffPercentage = diffPercentage,
-          aiAssertionResults = aiResult,
           contextData = contextData,
         )
       } else {
@@ -176,7 +163,6 @@ fun processOutputImageAndReport(
           actualFile = actualFile.absolutePath,
           goldenFile = goldenFile.absolutePath,
           timestampNs = System.nanoTime(),
-          aiAssertionResults = aiResult,
           contextData = contextData,
         )
       }
diff --git a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiAssertionOptions.kt b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiAssertionOptions.kt
deleted file mode 100644
index d3f0766c..00000000
--- a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiAssertionOptions.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.github.takahirom.roborazzi
-
-/**
- * If you want to use AI to compare images, you can specify the model and prompt.
- */
-@ExperimentalRoborazziApi
-data class AiAssertionOptions(
-  val aiAssertionModel: AiAssertionModel,
-  val aiAssertions: List<AiAssertion> = emptyList(),
-  val systemPrompt: String = """Evaluate the following assertion for fulfillment in the new image.
-The evaluation should be based on the comparison between the original image on the left and the new image on the right, with differences highlighted in red in the center. Focus on whether the new image fulfills the requirement specified in the user input.
-
-Output:
-For each assertion:
-A fulfillment percentage from 0 to 100.
-A brief explanation of how this percentage was determined.""",
-  val promptTemplate: String = """Assertions:
-INPUT_PROMPT
-""",
-  val inputPrompt: (AiAssertionOptions) -> String = { aiOptions ->
-    buildString {
-      aiOptions.aiAssertions.forEachIndexed { index, aiAssertion ->
-        appendLine("Assertion ${index + 1}: ${aiAssertion.assertionPrompt}\n")
-      }
-    }
-  },
-) {
-  @ExperimentalRoborazziApi
-  interface AiAssertionModel {
-    fun assert(
-      referenceImageFilePath: String,
-      comparisonImageFilePath: String,
-      actualImageFilePath: String,
-      aiAssertionOptions: AiAssertionOptions
-    ): AiAssertionResults
-    companion object {
-      const val DefaultMaxOutputTokens = 300
-      const val DefaultTemperature = 0.4F
-    }
-  }
-
-  data class AiAssertion(
-    val assertionPrompt: String,
-    val failIfNotFulfilled: Boolean = true,
-    /**
-     * If null, the AI result is not validated. But the fulfillment_percent are still included in the report.
-     */
-    val requiredFulfillmentPercent: Int? = 80
-  )
-}
\ No newline at end of file
diff --git a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResult.kt b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResult.kt
index 2aee2175..b3459a44 100644
--- a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResult.kt
+++ b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResult.kt
@@ -22,7 +22,7 @@ sealed interface CaptureResult {
   val compareFile: String?
   val actualFile: String?
   val goldenFile: String?
-  val contextData: Map<String, @Contextual Any>
+  val contextData: Map<String,@Contextual Any>
 
   val reportFile: String
     get() = when (val result = this) {
@@ -35,11 +35,11 @@ sealed interface CaptureResult {
   @Serializable
   data class Recorded(
     @SerialName("golden_file_path")
-    override val goldenFile: @Contextual String,
+    override val goldenFile:@Contextual String,
     @SerialName("timestamp")
     override val timestampNs: Long,
     @SerialName("context_data")
-    override val contextData: Map<String, @Contextual Any>
+    override val contextData: Map<String,@Contextual Any>
   ) : CaptureResult {
     override val type = "recorded"
     override val actualFile: String?
@@ -51,17 +51,15 @@ sealed interface CaptureResult {
   @Serializable
   data class Added(
     @SerialName("compare_file_path")
-    override val compareFile: @Contextual String,
+    override val compareFile:@Contextual String,
     @SerialName("actual_file_path")
-    override val actualFile: @Contextual String,
+    override val actualFile:@Contextual String,
     @SerialName("golden_file_path")
-    override val goldenFile: @Contextual String,
+    override val goldenFile:@Contextual String,
     @SerialName("timestamp")
     override val timestampNs: Long,
-    @SerialName("ai_assertion_results")
-    val aiAssertionResults: AiAssertionResults?,
     @SerialName("context_data")
-    override val contextData: Map<String, @Contextual Any>
+    override val contextData: Map<String,@Contextual Any>
   ) : CaptureResult {
     override val type = "added"
   }
@@ -69,19 +67,17 @@ sealed interface CaptureResult {
   @Serializable
   data class Changed(
     @SerialName("compare_file_path")
-    override val compareFile: @Contextual String,
+    override val compareFile:@Contextual String,
     @SerialName("golden_file_path")
-    override val goldenFile: @Contextual String,
+    override val goldenFile:@Contextual String,
     @SerialName("actual_file_path")
-    override val actualFile: @Contextual String,
+    override val actualFile:@Contextual String,
     @SerialName("timestamp")
     override val timestampNs: Long,
     @SerialName("diff_percentage")
     val diffPercentage: Float?,
-    @SerialName("ai_assertion_results")
-    val aiAssertionResults: AiAssertionResults?,
     @SerialName("context_data")
-    override val contextData: Map<String, @Contextual Any>
+    override val contextData: Map<String,@Contextual Any>
   ) : CaptureResult {
     override val type = "changed"
   }
@@ -89,11 +85,11 @@ sealed interface CaptureResult {
   @Serializable
   data class Unchanged(
     @SerialName("golden_file_path")
-    override val goldenFile: @Contextual String,
+    override val goldenFile:@Contextual String,
     @SerialName("timestamp")
     override val timestampNs: Long,
     @SerialName("context_data")
-    override val contextData: Map<String, @Contextual Any>
+    override val contextData: Map<String,@Contextual Any>
   ) : CaptureResult {
     override val type = "unchanged"
     override val actualFile: String?
@@ -126,32 +122,12 @@ sealed interface CaptureResult {
       require(decoder is JsonDecoder)
       val type = decoder.decodeJsonElement().jsonObject["type"]!!.jsonPrimitive.content
       return when (type) {
-        "recorded" -> decoder.decodeSerializableValue(Recorded.serializer())
-        "changed" -> decoder.decodeSerializableValue(Changed.serializer())
-        "unchanged" -> decoder.decodeSerializableValue(Unchanged.serializer())
-        "added" -> decoder.decodeSerializableValue(Added.serializer())
-        else -> throw IllegalArgumentException("Unknown type $type")
+      "recorded" -> decoder.decodeSerializableValue(Recorded.serializer())
+      "changed" -> decoder.decodeSerializableValue(Changed.serializer())
+      "unchanged" -> decoder.decodeSerializableValue(Unchanged.serializer())
+      "added" -> decoder.decodeSerializableValue(Added.serializer())
+      else -> throw IllegalArgumentException("Unknown type $type")
       }
     }
   }
 }
-
-@Serializable
-data class AiAssertionResults(
-  @SerialName("ai_assertion_results")
-  val aiAssertionResults: List<AiAssertionResult> = emptyList()
-)
-
-@Serializable
-data class AiAssertionResult(
-  @SerialName("assert_prompt")
-  val assertionPrompt: String,
-  @SerialName("required_fulfillment_percent")
-  val requiredFulfillmentPercent: Int?,
-  @SerialName("fail_if_not_fulfilled")
-  val failIfNotFulfilled: Boolean,
-  @SerialName("fulfillment_percent")
-  val fulfillmentPercent: Int,
-  @SerialName("explanation")
-  val explanation: String?,
-)
\ No newline at end of file
diff --git a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResults.kt b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResults.kt
index 4ead5559..ff94ee7a 100644
--- a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResults.kt
+++ b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResults.kt
@@ -158,13 +158,11 @@ data class CaptureResults(
   }
 
   companion object {
-    @OptIn(ExperimentalSerializationApi::class)
     val json = Json {
       isLenient = true
       encodeDefaults = true
       ignoreUnknownKeys = true
       classDiscriminator = "#class"
-      explicitNulls = false
       serializersModule = SerializersModule {
         contextual(Any::class, AnySerializer)
       }
diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/RoborazziPlugin.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/RoborazziPlugin.kt
index b8ece69e..9d9647b6 100644
--- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/RoborazziPlugin.kt
+++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/RoborazziPlugin.kt
@@ -15,10 +15,8 @@ import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.Task
 import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.file.RegularFile
 import org.gradle.api.model.ObjectFactory
 import org.gradle.api.provider.Property
-import org.gradle.api.provider.Provider
 import org.gradle.api.tasks.InputDirectory
 import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.OutputDirectory
@@ -37,7 +35,6 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithTests
 import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
 import org.jetbrains.kotlin.gradle.targets.native.KotlinNativeBinaryTestRun
 import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest
-import java.io.File
 import java.io.FileNotFoundException
 import java.util.Locale
 import javax.inject.Inject
@@ -59,7 +56,6 @@ open class RoborazziExtension @Inject constructor(objects: ObjectFactory) {
   }
 }
 
-val KnownImageFileExtensions: Set<String> = setOf("png", "gif", "jpg", "jpeg", "webp")
 
 @Suppress("unused")
 // From Paparazzi: https://github.com/cashapp/paparazzi/blob/a76702744a7f380480f323ffda124e845f2733aa/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt
@@ -81,7 +77,7 @@ abstract class RoborazziPlugin : Plugin<Project> {
       else -> throw IllegalStateException("Unsupported test task type: $this")
     }
 
-  @OptIn(InternalRoborazziApi::class, ExperimentalRoborazziApi::class)
+  @OptIn(InternalRoborazziApi::class)
   override fun apply(project: Project) {
     val extension = project.extensions.create("roborazzi", RoborazziExtension::class.java)
 
@@ -157,7 +153,7 @@ abstract class RoborazziPlugin : Plugin<Project> {
           val outputDirFile = outputDir.get().asFile
           if (outputDirFile.exists()) {
             outputDirFile.walkTopDown().forEach { file ->
-              if (KnownImageFileExtensions.contains(file.extension)) {
+              if (file.name.endsWith(".png") || file.name.endsWith(".webp") || file.name.endsWith(".gif")) {
                 file.delete()
               }
             }
@@ -166,7 +162,7 @@ abstract class RoborazziPlugin : Plugin<Project> {
           val intermediateDirFile = intermediateDirForEachVariant.get().asFile
           if (intermediateDirFile.exists()) {
             intermediateDirFile.walkTopDown().forEach { file ->
-              if (KnownImageFileExtensions.contains(file.extension)) {
+              if (file.name.endsWith(".png") || file.name.endsWith(".webp")|| file.name.endsWith(".gif")) {
                 file.delete()
               }
             }
@@ -283,6 +279,34 @@ abstract class RoborazziPlugin : Plugin<Project> {
                 overwrite = true
               )
               finalizeTestTask.infoln("Roborazzi: finalizeTestRoborazziTask Copy files from ${intermediateDir.get()} to ${outputDir.get()} end ${System.currentTimeMillis() - startCopy}ms")
+
+              val results: List<CaptureResult> = resultDirFileTree.get().mapNotNull {
+                if (it.name.endsWith(".json")) {
+                  CaptureResult.fromJsonFile(it.path)
+                } else {
+                  null
+                }
+              }
+              val resultsSummaryFile = resultSummaryFileProperty.get().asFile
+
+              val roborazziResults = CaptureResults.from(results)
+              finalizeTestTask.infoln("Roborazzi: Save result to ${resultsSummaryFile.absolutePath} with results:${results.size} summary:${roborazziResults.resultSummary}")
+
+              val jsonResult = roborazziResults.toJson()
+              resultsSummaryFile.parentFile.mkdirs()
+              resultsSummaryFile.writeText(jsonResult)
+              val reportFile = reportFileProperty.get().asFile
+
+              reportFile.parentFile.mkdirs()
+              WebAssets.create().writeToRoborazziReportsDir(reportFile.parentFile)
+              val reportHtml = readIndexHtmlFile()
+                ?: throw FileNotFoundException("index.html not found in resources/META-INF folder")
+              reportFile.writeText(
+                reportHtml.replace(
+                  oldValue = "REPORT_TEMPLATE_BODY",
+                  newValue = roborazziResults.toHtml(reportFile.parentFile.absolutePath)
+                )
+              )
             }
           }
         })
@@ -322,15 +346,15 @@ abstract class RoborazziPlugin : Plugin<Project> {
             test.infoln("Roborazzi: Set output dir $it to test task")
             it
           })
-          test.outputs.dir(resultDirFileProperty.map {
+          test.outputs.dir(resultDirFileProperty.let {
             test.infoln("Roborazzi: Set output dir $it to test task")
             it
           })
-          test.outputs.file(resultSummaryFileProperty.map {
+          test.outputs.file(resultSummaryFileProperty.let {
             test.infoln("Roborazzi: Set output file $it to test task")
             it
           })
-          test.outputs.file(reportFileProperty.map {
+          test.outputs.file(reportFileProperty.let {
             test.infoln("Roborazzi: Set output file $it to test task")
             it
           })
@@ -396,38 +420,6 @@ abstract class RoborazziPlugin : Plugin<Project> {
                 // Run only root suite
                 return
               }
-              if (doesRoborazziRunProvider.get()) {
-                test.infoln("Roborazzi: test.afterSuite ${test.name}")
-              } else {
-                return
-              }
-
-              val results: List<CaptureResult> = resultDirFileTree.get().mapNotNull {
-                if (it.name.endsWith(".json")) {
-                  CaptureResult.fromJsonFile(it.path)
-                } else {
-                  null
-                }
-              }
-              val resultsSummaryFile = resultSummaryFileProperty.get().asFile
-
-              val roborazziResults = CaptureResults.from(results)
-              saveResults(
-                test = test,
-                resultsSummaryFile = resultsSummaryFile,
-                results = results,
-                roborazziResults = roborazziResults,
-                reportFileProperty = reportFileProperty
-              )
-              // The reason why we do this in afterSuite() is that we want to change the tasks' output in the task execution phase.
-              cleanupOldScreenshotsIfNeeded(
-                test = test,
-                roborazziProperties = roborazziProperties,
-                outputDir = outputDir,
-                intermediateDir = intermediateDir,
-                roborazziResults = roborazziResults,
-              )
-
               // Copy all files from outputDir to intermediateDir
               // so that we can use Gradle's output caching
               test.infoln("Roborazzi: test.doLast Copy files from ${outputDir.get()} to ${intermediateDir.get()}")
@@ -534,70 +526,6 @@ abstract class RoborazziPlugin : Plugin<Project> {
     }
   }
 
-  private fun saveResults(
-    test: AbstractTestTask,
-    resultsSummaryFile: File,
-    results: List<CaptureResult>,
-    roborazziResults: CaptureResults,
-    reportFileProperty: Provider<RegularFile>
-  ) {
-    test.infoln("Roborazzi: Save result to ${resultsSummaryFile.absolutePath} with results:${results.size} summary:${roborazziResults.resultSummary}")
-
-    val jsonResult = roborazziResults.toJson()
-    resultsSummaryFile.parentFile.mkdirs()
-    resultsSummaryFile.writeText(jsonResult)
-    val reportFile = reportFileProperty.get().asFile
-
-    reportFile.parentFile.mkdirs()
-    WebAssets.create().writeToRoborazziReportsDir(reportFile.parentFile)
-    val reportHtml = readIndexHtmlFile()
-      ?: throw FileNotFoundException("index.html not found in resources/META-INF folder")
-    reportFile.writeText(
-      reportHtml.replace(
-        oldValue = "REPORT_TEMPLATE_BODY",
-        newValue = roborazziResults.toHtml(reportFile.parentFile.absolutePath)
-      )
-    )
-  }
-
-  private fun cleanupOldScreenshotsIfNeeded(
-    test: AbstractTestTask,
-    roborazziProperties: Map<String, Any?>,
-    outputDir: DirectoryProperty,
-    intermediateDir: DirectoryProperty,
-    roborazziResults: CaptureResults,
-  ) {
-    if (roborazziProperties["roborazzi.cleanupOldScreenshots"] == "true") {
-      // Delete all images from the intermediateDir
-      intermediateDir.get().asFile.walkTopDown().forEach { file ->
-        if (KnownImageFileExtensions.contains(file.extension)) {
-          file.delete()
-        }
-      }
-
-      // Remove all files not in the results from the outputDir
-      val removingFiles: MutableSet<String> = outputDir.get().asFile
-        .listFiles()
-        ?.toList()
-        .orEmpty()
-        .filter { it.isFile && KnownImageFileExtensions.contains(it.extension) }
-        .map { it.absolutePath }
-        .toMutableSet()
-      roborazziResults.captureResults.forEach { result ->
-        val latestFiles = listOfNotNull(
-          result.actualFile,
-          result.compareFile,
-          result.goldenFile
-        ).map { File(it).absolutePath }
-        removingFiles.removeAll(latestFiles)
-      }
-      test.infoln("Roborazzi: Cleanup old files $removingFiles")
-      removingFiles.forEach { file ->
-        File(file).delete()
-      }
-    }
-  }
-
   private fun readIndexHtmlFile(): String? {
     return this::class.java.classLoader.getResource("META-INF/index.html")?.readText()
   }
diff --git a/roborazzi-ai-gemini/.gitignore b/roborazzi-ai-gemini/.gitignore
deleted file mode 100644
index d1638636..00000000
--- a/roborazzi-ai-gemini/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-build/
\ No newline at end of file
diff --git a/roborazzi-ai-gemini/build.gradle b/roborazzi-ai-gemini/build.gradle
deleted file mode 100644
index 7850852e..00000000
--- a/roborazzi-ai-gemini/build.gradle
+++ /dev/null
@@ -1,61 +0,0 @@
-plugins {
-  id "org.jetbrains.kotlin.multiplatform"
-  id "com.android.library"
-  id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin
-}
-if (System.getenv("INTEGRATION_TEST") != "true") {
-  pluginManager.apply("com.vanniktech.maven.publish")
-}
-
-android.compileSdk(34)
-
-kotlin {
-  applyDefaultHierarchyTemplate()
-
-  iosX64()
-  iosArm64()
-  iosSimulatorArm64()
-
-  jvm()
-
-  androidTarget {
-    publishLibraryVariants("release")
-  }
-
-  sourceSets {
-    commonMain {
-      dependencies {
-        implementation "io.github.takahirom.roborazzi:roborazzi-core:$VERSION_NAME"
-        // To expose generationConfigBuilder
-        api libs.generativeai.google
-        implementation(libs.kotlinx.coroutines.core)
-        implementation(libs.kotlinx.serialization.json)
-      }
-    }
-  }
-  sourceSets.all {
-    it.languageSettings {
-      progressiveMode = true
-      optIn("com.github.takahirom.roborazzi.InternalRoborazziApi")
-      optIn("com.github.takahirom.roborazzi.ExperimentalRoborazziApi")
-    }
-  }
-}
-
-android {
-  namespace 'com.github.takahirom.roborazzi.ai.gemini'
-  compileSdk 34
-
-  defaultConfig {
-    minSdk 21
-    targetSdk 32
-
-    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-  }
-  buildFeatures {}
-  testOptions {
-    unitTests {
-      includeAndroidResources = true
-    }
-  }
-}
diff --git a/roborazzi-ai-gemini/src/androidMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.android.kt b/roborazzi-ai-gemini/src/androidMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.android.kt
deleted file mode 100644
index 70cc4373..00000000
--- a/roborazzi-ai-gemini/src/androidMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.android.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.github.takahirom.roborazzi
-
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import dev.shreyaspatil.ai.client.generativeai.type.PlatformImage
-import java.io.ByteArrayOutputStream
-
-actual fun readByteArrayFromFile(filePath: String): PlatformImage {
-  val bitmap = BitmapFactory.decodeFile(filePath)
-  val outputStream = ByteArrayOutputStream()
-  val data = outputStream.use {
-    bitmap.compress(Bitmap.CompressFormat.JPEG, 80, it)
-    outputStream.toByteArray()
-  }
-  return PlatformImage(data = data)
-}
\ No newline at end of file
diff --git a/roborazzi-ai-gemini/src/commonMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.kt b/roborazzi-ai-gemini/src/commonMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.kt
deleted file mode 100644
index 64482c97..00000000
--- a/roborazzi-ai-gemini/src/commonMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.github.takahirom.roborazzi
-
-import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel
-import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel.Companion.DefaultMaxOutputTokens
-import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel.Companion.DefaultTemperature
-import dev.shreyaspatil.ai.client.generativeai.GenerativeModel
-import dev.shreyaspatil.ai.client.generativeai.type.FunctionType
-import dev.shreyaspatil.ai.client.generativeai.type.GenerationConfig
-import dev.shreyaspatil.ai.client.generativeai.type.PlatformImage
-import dev.shreyaspatil.ai.client.generativeai.type.Schema
-import dev.shreyaspatil.ai.client.generativeai.type.content
-import dev.shreyaspatil.ai.client.generativeai.type.generationConfig
-import kotlinx.coroutines.runBlocking
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-@ExperimentalRoborazziApi
-class GeminiAiAssertionModel(
-  private val apiKey: String,
-  private val modelName: String = "gemini-1.5-flash",
-  private val generationConfigBuilder: GenerationConfig.Builder.() -> Unit = {
-    maxOutputTokens = DefaultMaxOutputTokens
-    temperature = DefaultTemperature
-  }
-) : AiAssertionModel {
-  override fun assert(
-    referenceImageFilePath: String,
-    comparisonImageFilePath: String,
-    actualImageFilePath: String,
-    aiAssertionOptions: AiAssertionOptions
-  ): AiAssertionResults {
-    val systemPrompt = aiAssertionOptions.systemPrompt
-    val generativeModel = GenerativeModel(
-      modelName = modelName,
-      apiKey = apiKey,
-      systemInstruction = content {
-        text(systemPrompt)
-      },
-      generationConfig = generationConfig {
-        responseMimeType = "application/json"
-        responseSchema = Schema(
-          name = "content",
-          description = "content",
-          type = FunctionType.ARRAY,
-          items = Schema(
-            name = "assert_results",
-            description = "An array of assertion results",
-            type = FunctionType.OBJECT,
-            properties = mapOf(
-              "fulfillment_percent" to Schema.int(
-                name = "fulfillment_percent",
-                description = "A fulfillment percentage from 0 to 100",
-              ),
-              "explanation" to Schema(
-                name = "explanation",
-                description = "A brief explanation of how this percentage was determined. If fulfillment_percent is 100, this field should be empty.",
-                type = FunctionType.STRING,
-                nullable = true,
-              )
-            ),
-            required = listOf("fulfillment_percent")
-          ),
-        )
-        generationConfigBuilder()
-      },
-    )
-
-    val template = aiAssertionOptions.promptTemplate
-
-    val inputPrompt = aiAssertionOptions.inputPrompt(aiAssertionOptions)
-    val inputContent = content {
-      image(readByteArrayFromFile(comparisonImageFilePath))
-      val prompt = template.replace("INPUT_PROMPT", inputPrompt)
-      text(prompt)
-
-      debugLog {
-        "RoborazziAi: prompt:$prompt"
-      }
-    }
-
-    val response = runBlocking { generativeModel.generateContent(inputContent) }
-    debugLog {
-      "RoborazziAi: response: ${response.text}"
-    }
-    val geminiResult = CaptureResults.json.decodeFromString<Array<GeminiAiConditionResult>>(
-      requireNotNull(
-        response.text
-      )
-    )
-    return AiAssertionResults(
-      aiAssertionResults = aiAssertionOptions.aiAssertions.mapIndexed { index, condition ->
-        val assertResult = geminiResult.getOrNull(index) ?: GeminiAiConditionResult(
-          fulfillmentPercent = 0,
-          explanation = "AI model did not return a result for this assertion"
-        )
-        AiAssertionResult(
-          assertionPrompt = condition.assertionPrompt,
-          requiredFulfillmentPercent = condition.requiredFulfillmentPercent,
-          failIfNotFulfilled = condition.failIfNotFulfilled,
-          fulfillmentPercent = assertResult.fulfillmentPercent,
-          explanation = assertResult.explanation,
-        )
-      }
-    )
-  }
-}
-
-
-@Serializable
-private data class GeminiAiConditionResult(
-  @SerialName("fulfillment_percent")
-  val fulfillmentPercent: Int,
-  val explanation: String?,
-)
-
-internal expect fun readByteArrayFromFile(filePath: String): PlatformImage
diff --git a/roborazzi-ai-gemini/src/jvmMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.jvm.kt b/roborazzi-ai-gemini/src/jvmMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.jvm.kt
deleted file mode 100644
index a961f781..00000000
--- a/roborazzi-ai-gemini/src/jvmMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.jvm.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.github.takahirom.roborazzi
-
-import dev.shreyaspatil.ai.client.generativeai.type.PlatformImage
-import java.io.File
-
-internal actual fun readByteArrayFromFile(filePath: String): PlatformImage {
-  return PlatformImage(File(filePath).readBytes())
-}
\ No newline at end of file
diff --git a/roborazzi-ai-gemini/src/nativeMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.native.kt b/roborazzi-ai-gemini/src/nativeMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.native.kt
deleted file mode 100644
index d5c5d842..00000000
--- a/roborazzi-ai-gemini/src/nativeMain/kotlin/com/github/takahirom/roborazzi/GeminiRoborazziAi.native.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.github.takahirom.roborazzi
-
-import dev.shreyaspatil.ai.client.generativeai.type.PlatformImage
-import dev.shreyaspatil.ai.client.generativeai.type.asPlatformImage
-import platform.UIKit.UIImage
-
-internal actual fun readByteArrayFromFile(filePath: String): PlatformImage {
-  return UIImage.imageWithContentsOfFile(filePath)!!.asPlatformImage()
-}
\ No newline at end of file
diff --git a/roborazzi-ai-openai/.gitignore b/roborazzi-ai-openai/.gitignore
deleted file mode 100644
index d1638636..00000000
--- a/roborazzi-ai-openai/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-build/
\ No newline at end of file
diff --git a/roborazzi-ai-openai/build.gradle b/roborazzi-ai-openai/build.gradle
deleted file mode 100644
index bb8b5b15..00000000
--- a/roborazzi-ai-openai/build.gradle
+++ /dev/null
@@ -1,66 +0,0 @@
-plugins {
-  id "org.jetbrains.kotlin.multiplatform"
-  id "com.android.library"
-  id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin
-}
-if (System.getenv("INTEGRATION_TEST") != "true") {
-  pluginManager.apply("com.vanniktech.maven.publish")
-}
-
-android.compileSdk(34)
-
-kotlin {
-  applyDefaultHierarchyTemplate()
-
-  iosX64()
-  iosArm64()
-  iosSimulatorArm64()
-
-  jvm()
-
-  androidTarget {
-    publishLibraryVariants("release")
-  }
-
-  sourceSets {
-    commonMain {
-      dependencies {
-        implementation "io.github.takahirom.roborazzi:roborazzi-core:$VERSION_NAME"
-        // To expose requestBuilderModifier
-        api(libs.ktor.client.core)
-        implementation(libs.kotlinx.coroutines.core)
-        implementation(libs.ktor.serialization.json)
-        implementation(libs.ktor.client.cio)
-        implementation(libs.ktor.client.logging)
-        implementation(libs.ktor.client.contentnegotiation)
-        implementation(libs.kotlinx.io.core)
-        implementation(libs.kotlinx.serialization.json)
-      }
-    }
-  }
-  sourceSets.all {
-    it.languageSettings {
-      progressiveMode = true
-      optIn("com.github.takahirom.roborazzi.InternalRoborazziApi")
-      optIn("com.github.takahirom.roborazzi.ExperimentalRoborazziApi")
-    }
-  }
-}
-
-android {
-  namespace 'com.github.takahirom.roborazzi.ai.openai'
-  compileSdk 34
-
-  defaultConfig {
-    minSdk 21
-    targetSdk 32
-
-    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-  }
-  buildFeatures {}
-  testOptions {
-    unitTests {
-      includeAndroidResources = true
-    }
-  }
-}
\ No newline at end of file
diff --git a/roborazzi-ai-openai/src/commonMain/kotlin/com/github/takahirom/roborazzi/OpenAiAiAssertionModel.kt b/roborazzi-ai-openai/src/commonMain/kotlin/com/github/takahirom/roborazzi/OpenAiAiAssertionModel.kt
deleted file mode 100644
index 8b22aacd..00000000
--- a/roborazzi-ai-openai/src/commonMain/kotlin/com/github/takahirom/roborazzi/OpenAiAiAssertionModel.kt
+++ /dev/null
@@ -1,300 +0,0 @@
-package com.github.takahirom.roborazzi
-
-import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel.Companion.DefaultMaxOutputTokens
-import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel.Companion.DefaultTemperature
-import com.github.takahirom.roborazzi.CaptureResults.Companion.json
-import io.ktor.client.HttpClient
-import io.ktor.client.plugins.HttpTimeout
-import io.ktor.client.plugins.HttpTimeout.Plugin.INFINITE_TIMEOUT_MS
-import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
-import io.ktor.client.plugins.logging.LogLevel
-import io.ktor.client.plugins.logging.Logger
-import io.ktor.client.plugins.logging.Logging
-import io.ktor.client.plugins.logging.SIMPLE
-import io.ktor.client.request.HttpRequestBuilder
-import io.ktor.client.request.header
-import io.ktor.client.request.post
-import io.ktor.client.request.setBody
-import io.ktor.client.statement.HttpResponse
-import io.ktor.client.statement.bodyAsText
-import io.ktor.http.ContentType
-import io.ktor.http.contentType
-import io.ktor.serialization.kotlinx.json.json
-import kotlinx.coroutines.runBlocking
-import kotlinx.io.buffered
-import kotlinx.io.files.Path
-import kotlinx.io.files.SystemFileSystem
-import kotlinx.io.readByteArray
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.decodeFromJsonElement
-import kotlinx.serialization.json.jsonObject
-import kotlin.io.encoding.Base64
-import kotlin.io.encoding.ExperimentalEncodingApi
-
-@ExperimentalRoborazziApi
-class OpenAiAiAssertionModel(
-  private val apiKey: String,
-  private val modelName: String = "gpt-4o",
-  private val baseUrl: String = "https://api.openai.com/v1/",
-  private val loggingEnabled: Boolean = false,
-  private val temperature: Float = DefaultTemperature,
-  private val maxTokens: Int = DefaultMaxOutputTokens,
-  private val seed: Int = 1566,
-  private val requestBuilderModifier: (HttpRequestBuilder.() -> Unit) = {
-    header("Authorization", "Bearer $apiKey")
-  },
-  private val httpClient: HttpClient = HttpClient {
-    install(ContentNegotiation) {
-      json(
-        json = json
-      )
-    }
-    install(HttpTimeout) {
-      requestTimeoutMillis = INFINITE_TIMEOUT_MS
-      socketTimeoutMillis = 80_000
-    }
-    if (loggingEnabled) {
-      install(Logging) {
-        logger = Logger.SIMPLE
-        level = LogLevel.ALL
-      }
-    }
-  },
-) : AiAssertionOptions.AiAssertionModel {
-
-  override fun assert(
-    referenceImageFilePath: String,
-    comparisonImageFilePath: String,
-    actualImageFilePath: String,
-    aiAssertionOptions: AiAssertionOptions
-  ): AiAssertionResults {
-    val systemPrompt = aiAssertionOptions.systemPrompt
-    val template = aiAssertionOptions.promptTemplate
-    val inputPrompt = aiAssertionOptions.inputPrompt(aiAssertionOptions)
-    val imageBytes = readByteArrayFromFile(comparisonImageFilePath)
-    val imageBase64 = imageBytes.encodeBase64()
-    val messages = listOf(
-      Message(
-        role = "system",
-        content = listOf(
-          Content(
-            type = "text",
-            text = systemPrompt
-          )
-        ),
-      ),
-      Message(
-        role = "user",
-        content = listOf(
-          Content(
-            type = "text",
-            text = template.replace("INPUT_PROMPT", inputPrompt)
-          ),
-          Content(
-            type = "image_url",
-            imageUrl = ImageUrl(
-              url = "data:image/png;base64,$imageBase64"
-            )
-          )
-        )
-      )
-    )
-    val responseText = runBlocking {
-      chatCompletion(
-        messages = messages,
-        model = modelName
-      )
-    }
-    debugLog {
-      "OpenAiAiModel: response: $responseText"
-    }
-    val aiConditionResults = parseOpenAiResponse(responseText, aiAssertionOptions)
-    return AiAssertionResults(
-      aiAssertionResults = aiConditionResults
-    )
-  }
-
-  private suspend fun chatCompletion(
-    messages: List<Message>,
-    model: String
-  ): String {
-    val requestBody = ChatCompletionRequest(
-      model = model,
-      messages = messages,
-      temperature = temperature,
-      maxTokens = maxTokens,
-      responseFormat = ResponseFormat(
-        type = "json_schema",
-        jsonSchema = buildJsonSchema(),
-      ),
-      seed = seed
-    )
-    val response: HttpResponse = httpClient.post(baseUrl + "chat/completions") {
-      requestBuilderModifier()
-      contentType(ContentType.Application.Json)
-      setBody(requestBody)
-    }
-    val bodyText = response.bodyAsText()
-    debugLog { "OpenAiAiModel: response: $bodyText" }
-    val responseBody: ChatCompletionResponse = json.decodeFromString(bodyText)
-    return responseBody.choices.firstOrNull()?.message?.content ?: ""
-  }
-
-  private fun buildJsonSchema(): JsonObject {
-    val schemaJson = """
-    {
-  "name": "OpenAiResponse1",
-  "description": "Verify image",
-  "strict": true,
-  "schema": {
-    "type": "object",
-    "required": ["results"],
-    "additionalProperties": false,
-    "properties": {
-      "results": {
-        "type": "array",
-        "items": {
-          "type": "object",
-          "required": ["fulfillment_percent", "explanation"],
-          "additionalProperties": false,
-          "properties": {
-            "fulfillment_percent": {
-              "type": "integer"
-            },
-            "explanation": {
-              "type": ["string", "null"]
-            }
-          }
-        }
-      }
-    }
-  }
-}
-    """.trimIndent()
-    return json.parseToJsonElement(schemaJson).jsonObject
-  }
-}
-
-
-private fun parseOpenAiResponse(
-  responseText: String,
-  aiAssertionOptions: AiAssertionOptions
-): List<AiAssertionResult> {
-  val openAiResult = try {
-    val element = json.parseToJsonElement(responseText)
-    val resultsElement = element.jsonObject["results"]
-    val results = if (resultsElement != null) {
-      json.decodeFromJsonElement<List<OpenAiConditionResult>>(resultsElement)
-    } else {
-      emptyList()
-    }
-    OpenAiResponse(results = results)
-  } catch (e: Exception) {
-    debugLog { "Failed to parse OpenAI response: ${e.message}" }
-    OpenAiResponse(results = emptyList())
-  }
-  return aiAssertionOptions.aiAssertions.mapIndexed { index, condition ->
-    val result = openAiResult.results.getOrNull(index)
-    val fulfillmentPercent = result?.fulfillmentPercent ?: 0
-    val explanation = result?.explanation ?: "AI model did not return a result for this assertion"
-    AiAssertionResult(
-      assertionPrompt = condition.assertionPrompt,
-      requiredFulfillmentPercent = condition.requiredFulfillmentPercent,
-      failIfNotFulfilled = condition.failIfNotFulfilled,
-      fulfillmentPercent = fulfillmentPercent,
-      explanation = explanation,
-    )
-  }
-}
-
-private fun readByteArrayFromFile(filePath: String): ByteArray {
-  return SystemFileSystem.source(path = Path(filePath)).buffered().readByteArray()
-}
-
-@OptIn(ExperimentalEncodingApi::class)
-private fun ByteArray.encodeBase64(): String {
-  return Base64.encode(this)
-}
-
-// Request
-
-@Serializable
-private data class ChatCompletionRequest(
-  val model: String,
-  val messages: List<Message>,
-  val temperature: Float,
-  @SerialName("max_tokens") val maxTokens: Int,
-  @SerialName("response_format") val responseFormat: ResponseFormat?,
-  val seed: Int,
-)
-
-@Serializable
-private data class ResponseFormat(
-  val type: String,
-  @SerialName("json_schema") val jsonSchema: JsonObject
-)
-
-@Serializable
-private data class Message(
-  val role: String,
-  val content: List<Content>
-)
-
-@Serializable
-private data class Content(
-  val type: String,
-  val text: String? = null,
-  @SerialName("image_url") val imageUrl: ImageUrl? = null
-)
-
-@Serializable
-private data class ImageUrl(
-  val url: String
-)
-
-@Serializable
-private data class ChatCompletionResponse(
-  val id: String,
-  val `object`: String,
-  val created: Long,
-  val model: String,
-  val choices: List<Choice>,
-  val usage: Usage? = null
-)
-
-@Serializable
-private data class Choice(
-  val index: Int,
-  val message: ChoiceMessage,
-  @SerialName("finish_reason") val finishReason: String? = null,
-)
-
-@Serializable
-private data class ChoiceMessage(
-  val role: String,
-  val content: String
-)
-
-@Serializable
-private data class Usage(
-  @SerialName("prompt_tokens") val promptTokens: Int,
-  @SerialName("completion_tokens") val completionTokens: Int? = null,
-  @SerialName("total_tokens") val totalTokens: Int,
-)
-
-
-// Response
-
-@Serializable
-private data class OpenAiResponse(
-  val results: List<OpenAiConditionResult>
-)
-
-@Serializable
-private data class OpenAiConditionResult(
-  @SerialName("fulfillment_percent")
-  val fulfillmentPercent: Int,
-  val explanation: String?,
-)
\ No newline at end of file
diff --git a/roborazzi-compose-desktop/build.gradle b/roborazzi-compose-desktop/build.gradle
index 55839ca2..8f09b073 100644
--- a/roborazzi-compose-desktop/build.gradle
+++ b/roborazzi-compose-desktop/build.gradle
@@ -13,6 +13,7 @@ kotlin {
     compilations
       .named("main")
     .configure { compilation ->
+      compilation.kotlinOptions.freeCompilerArgs  += "-Xcontext-receivers"
     }
   }
   sourceSets {
diff --git a/roborazzi-compose-desktop/src/commonMain/kotlin/io/github/takahirom/roborazzi/RoborazziDesktop.kt b/roborazzi-compose-desktop/src/commonMain/kotlin/io/github/takahirom/roborazzi/RoborazziDesktop.kt
index bcf0ea34..333f498c 100644
--- a/roborazzi-compose-desktop/src/commonMain/kotlin/io/github/takahirom/roborazzi/RoborazziDesktop.kt
+++ b/roborazzi-compose-desktop/src/commonMain/kotlin/io/github/takahirom/roborazzi/RoborazziDesktop.kt
@@ -2,20 +2,18 @@ package io.github.takahirom.roborazzi
 
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.toAwtImage
+import androidx.compose.ui.test.DesktopComposeUiTest
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
-import com.github.takahirom.roborazzi.AwtRoboCanvas
-import com.github.takahirom.roborazzi.DefaultFileNameGenerator
-import com.github.takahirom.roborazzi.RoboCanvas
-import com.github.takahirom.roborazzi.RoborazziOptions
-import com.github.takahirom.roborazzi.fileWithRecordFilePathStrategy
-import com.github.takahirom.roborazzi.processOutputImageAndReport
-import com.github.takahirom.roborazzi.provideRoborazziContext
+import com.github.takahirom.roborazzi.*
 import java.awt.image.BufferedImage
 import java.io.File
 
+context(DesktopComposeUiTest)
+@OptIn(ExperimentalTestApi::class)
 fun SemanticsNodeInteraction.captureRoboImage(
   filePath: String = DefaultFileNameGenerator.generateFilePath(),
   roborazziOptions: RoborazziOptions = provideRoborazziContext().options,
@@ -29,6 +27,8 @@ fun SemanticsNodeInteraction.captureRoboImage(
   )
 }
 
+context(DesktopComposeUiTest)
+@OptIn(ExperimentalTestApi::class)
 fun SemanticsNodeInteraction.captureRoboImage(
   file: File,
   roborazziOptions: RoborazziOptions
diff --git a/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt b/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt
index 54035b6f..f9054981 100644
--- a/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt
+++ b/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt
@@ -523,7 +523,6 @@ fun SemanticsNodeInteraction.captureRoboImage(
           actualFile = actualFilePath,
           goldenFile = goldenFilePath,
           timestampNs = getNanoTime(),
-          aiAssertionResults = null,
           contextData = emptyMap()
         )
         writeJson(result, resultsDir, nameWithoutExtension)
@@ -541,7 +540,6 @@ fun SemanticsNodeInteraction.captureRoboImage(
           goldenFile = goldenFilePath,
           timestampNs = getNanoTime(),
           diffPercentage = Float.NaN,
-          aiAssertionResults = null,
           contextData = emptyMap()
         )
         writeJson(result, resultsDir, nameWithoutExtension)
@@ -568,7 +566,6 @@ fun SemanticsNodeInteraction.captureRoboImage(
           actualFile = actualFilePath,
           goldenFile = goldenFilePath,
           timestampNs = getNanoTime(),
-          aiAssertionResults = null,
           contextData = emptyMap()
         )
         writeJson(result, resultsDir, nameWithoutExtension)
@@ -586,7 +583,6 @@ fun SemanticsNodeInteraction.captureRoboImage(
           goldenFile = goldenFilePath,
           timestampNs = getNanoTime(),
           diffPercentage = Float.NaN,
-          aiAssertionResults = null,
           contextData = emptyMap()
         )
         writeJson(result, resultsDir, nameWithoutExtension)
@@ -613,7 +609,6 @@ fun SemanticsNodeInteraction.captureRoboImage(
           actualFile = goldenFilePath,
           goldenFile = goldenFilePath,
           timestampNs = getNanoTime(),
-          aiAssertionResults = null,
           contextData = emptyMap()
         )
         writeJson(result, resultsDir, nameWithoutExtension)
@@ -631,7 +626,6 @@ fun SemanticsNodeInteraction.captureRoboImage(
           goldenFile = goldenFilePath,
           timestampNs = getNanoTime(),
           diffPercentage = Float.NaN,
-          aiAssertionResults = null,
           contextData = emptyMap()
         )
         writeJson(result, resultsDir, nameWithoutExtension)
@@ -658,7 +652,6 @@ fun SemanticsNodeInteraction.captureRoboImage(
           actualFile = goldenFilePath,
           goldenFile = goldenFilePath,
           timestampNs = getNanoTime(),
-          aiAssertionResults = null,
           contextData = emptyMap()
         )
         writeJson(result, resultsDir, nameWithoutExtension)
@@ -676,7 +669,6 @@ fun SemanticsNodeInteraction.captureRoboImage(
           goldenFile = goldenFilePath,
           timestampNs = getNanoTime(),
           diffPercentage = Float.NaN,
-          aiAssertionResults = null,
           contextData = emptyMap()
         )
         writeJson(result, resultsDir, nameWithoutExtension)
diff --git a/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt b/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt
index e606c0d4..fb829713 100644
--- a/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt
+++ b/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt
@@ -4,23 +4,12 @@ import android.content.res.Configuration
 import org.robolectric.RuntimeEnvironment.setFontScale
 import org.robolectric.RuntimeEnvironment.setQualifiers
 import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo
-import sergio.sastre.composable.preview.scanner.android.device.domain.RobolectricDeviceQualifierBuilder
 import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview
 
 @ExperimentalRoborazziApi
 fun ComposablePreview<AndroidPreviewInfo>.applyToRobolectricConfiguration() {
   val preview = this
 
-  fun setDevice(device: String){
-    if (device.isNotBlank()) {
-      // Requires `io.github.sergio-sastre.ComposablePreviewScanner:android:0.4.0` or later
-      RobolectricDeviceQualifierBuilder.build(device)?.run {
-        setQualifiers(this)
-      }
-    }
-  }
-  setDevice(preview.previewInfo.device)
-
   fun setUiMode(uiMode: Int) {
     val nightMode =
       when (uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
diff --git a/roborazzi-idea-plugin/src/main/kotlin/com/github/takahirom/roborazzi/idea/settings/AppSettingsComponent.kt b/roborazzi-idea-plugin/src/main/kotlin/com/github/takahirom/roborazzi/idea/settings/AppSettingsComponent.kt
index ddc1c1e7..eb7636e0 100644
--- a/roborazzi-idea-plugin/src/main/kotlin/com/github/takahirom/roborazzi/idea/settings/AppSettingsComponent.kt
+++ b/roborazzi-idea-plugin/src/main/kotlin/com/github/takahirom/roborazzi/idea/settings/AppSettingsComponent.kt
@@ -3,46 +3,26 @@ package com.github.takahirom.roborazzi.idea.settings
 import com.intellij.ui.components.JBLabel
 import com.intellij.ui.components.JBTextField
 import com.intellij.util.ui.FormBuilder
-import java.awt.Dimension
-import javax.swing.Box
-import javax.swing.BoxLayout
 import javax.swing.JComponent
-import javax.swing.JLabel
 import javax.swing.JPanel
-import javax.swing.JSeparator
 
 /**
  * Supports creating and managing a [JPanel] for the Settings Dialog.
  */
 class AppSettingsComponent {
-  private val myMainPanel: JPanel
-  private val imagesPathFromModuleText: JBTextField = JBTextField()
-
-  private val descriptionText = """
-    <html>To enable the display of Roborazzi tasks, please enable<br>
-    <b>Configure all Gradle tasks during Gradle Sync (this can make Gradle Sync slower)</b> in the Settings | Experimental | Gradle.</html>
-""".trimIndent()
+    private val myMainPanel: JPanel
+    private val imagesPathFromModuleText: JBTextField = JBTextField()
+
+    init {
+        myMainPanel = FormBuilder.createFormBuilder()
+            .addLabeledComponent(JBLabel("Enter images directory path from module: "), imagesPathFromModuleText, 1, false)
+            .addComponentFillVertically(JPanel(), 0)
+            .getPanel()
+    }
 
-  init {
-    myMainPanel = FormBuilder.createFormBuilder()
-      .addLabeledComponent(
-        JBLabel("Enter images directory path from module: "),
-        imagesPathFromModuleText,
-        1,
-        false
-      )
-      // adjust margin between components
-      .addVerticalGap(8)
-      .addComponent(createNoteSection())
-      .addComponent(JBLabel(descriptionText).apply {
-        verticalAlignment = JBLabel.TOP
-        preferredSize = Dimension(400, 200)
-      })
-      .addComponentFillVertically(JPanel(), 0)
-      .panel
-  }
     val panel: JPanel
         get() = myMainPanel
+
     val preferredFocusedComponent: JComponent
         get() = imagesPathFromModuleText
 
@@ -51,20 +31,4 @@ class AppSettingsComponent {
         set(newText) {
             imagesPathFromModuleText.setText(newText)
         }
-
-  private fun createNoteSection(): JPanel {
-    val panel = JPanel()
-    panel.layout = BoxLayout(panel, BoxLayout.X_AXIS)
-
-    val label = JLabel("Note")
-    val separator = JSeparator().apply {
-      alignmentY = 0f
-    }
-
-    panel.add(label)
-    panel.add(Box.createHorizontalStrut(8))
-    panel.add(separator)
-
-    return panel
-  }
 }
\ No newline at end of file
diff --git a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt
index 52181972..845068d5 100644
--- a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt
+++ b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt
@@ -28,18 +28,6 @@ class RoborazziRule private constructor(
   private val captureRoot: CaptureRoot,
   private val options: Options = Options()
 ) : TestWatcher() {
-  init {
-    try {
-      val clazz = Class.forName("com.github.takahirom.roborazzi.RoborazziAi")
-      // RoborazziAi is available
-      clazz.getDeclaredMethod("loadRoboAi").invoke(null)
-    } catch (ignored: ClassNotFoundException) {
-      // RoborazziAi is not available
-    } catch (ignored: NoSuchMethodException) {
-      // RoborazziAi is not available
-    }
-  }
-
   /**
    * If you add this annotation to the test, the test will be ignored by
    * roborazzi's CaptureType.LastImage, CaptureType.AllImage and CaptureType.Gif.
diff --git a/sample-android/build.gradle b/sample-android/build.gradle
index 6a967196..2bcf43c9 100644
--- a/sample-android/build.gradle
+++ b/sample-android/build.gradle
@@ -4,14 +4,6 @@ plugins {
   id 'io.github.takahirom.roborazzi'
 }
 
-def localProperties = new Properties()
-def localPropertiesFile = rootProject.file('local.properties')
-if (localPropertiesFile.exists()) {
-  localPropertiesFile.withInputStream { instream ->
-    localProperties.load(instream)
-  }
-}
-
 android {
   namespace 'com.github.takahirom.roborazzi.sample'
   compileSdk 34
@@ -43,12 +35,6 @@ android {
       includeAndroidResources = true
       it.all {
         environment "robolectric.logging.enabled", "true"
-        if (localProperties.getProperty("gemini_api_key") != null) {
-          environment "gemini_api_key", localProperties.getProperty("gemini_api_key")
-        }
-        if (localProperties.getProperty("openai_api_key") != null) {
-          environment "openai_api_key", localProperties.getProperty("openai_api_key")
-        }
         // To run coverage report in Android Studio
         jvmArgs '-noverify'
         // Set the max heap size for the tests JVM(s)
@@ -60,8 +46,6 @@ android {
 
 dependencies {
   testImplementation project(":roborazzi")
-  testImplementation project(":roborazzi-ai-gemini")
-  testImplementation(project(":roborazzi-ai-openai"))
   testImplementation project(":roborazzi-compose")
   testImplementation project(":roborazzi-junit-rule")
 
diff --git a/sample-compose-desktop-jvm/build.gradle.kts b/sample-compose-desktop-jvm/build.gradle.kts
index b43a9a25..5203802b 100644
--- a/sample-compose-desktop-jvm/build.gradle.kts
+++ b/sample-compose-desktop-jvm/build.gradle.kts
@@ -39,5 +39,6 @@ compose.desktop {
 tasks.withType<KotlinCompile>().configureEach {
   kotlinOptions {
     incremental = false
+    freeCompilerArgs += "-Xcontext-receivers"
   }
 }
\ No newline at end of file
diff --git a/sample-compose-desktop-multiplatform/build.gradle.kts b/sample-compose-desktop-multiplatform/build.gradle.kts
index afd3d729..8e0c3966 100644
--- a/sample-compose-desktop-multiplatform/build.gradle.kts
+++ b/sample-compose-desktop-multiplatform/build.gradle.kts
@@ -12,6 +12,9 @@ version = "1.0-SNAPSHOT"
 
 kotlin {
   jvm("desktop") {
+    val test by compilations.getting {
+      kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
+    }
   }
   sourceSets {
     val desktopMain by getting {
@@ -46,5 +49,6 @@ compose.desktop {
 
 tasks.withType<KotlinCompile>().configureEach {
   kotlinOptions {
+    freeCompilerArgs += "-Xcontext-receivers"
   }
 }
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 76a15c9b..20e68d9a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -12,14 +12,12 @@ dependencyResolutionManagement {
     mavenCentral()
   }
 }
-rootProject.name = "roborazzi-root"
+rootProject.name = "roborazzi"
 include ':roborazzi'
 include ':roborazzi-junit-rule'
 include ':roborazzi-compose-desktop'
 include ':roborazzi-compose-ios'
 include ':roborazzi-compose'
-include ':roborazzi-ai-gemini'
-include ':roborazzi-ai-openai'
 include ':roborazzi-painter'
 include ':roborazzi-compose-preview-scanner-support'
 


Test patch:
diff --git a/include-build/roborazzi-core/src/jvmTest/kotlin/io/github/takahirom/roborazzi/CaptureResultTest.kt b/include-build/roborazzi-core/src/jvmTest/kotlin/io/github/takahirom/roborazzi/CaptureResultTest.kt
index e16038bd..efcf8741 100644
--- a/include-build/roborazzi-core/src/jvmTest/kotlin/io/github/takahirom/roborazzi/CaptureResultTest.kt
+++ b/include-build/roborazzi-core/src/jvmTest/kotlin/io/github/takahirom/roborazzi/CaptureResultTest.kt
@@ -36,7 +36,6 @@ class CaptureResultTest {
         actualFile = "/actual_file",
         goldenFile = "/golden_file",
         timestampNs = 123456789,
-        aiAssertionResults = null,
         contextData = mapOf(
           "key" to 2,
           "keyDouble" to 2.5,
@@ -48,7 +47,6 @@ class CaptureResultTest {
         actualFile = "/actual_file",
         timestampNs = 123456789,
         diffPercentage = 0.123f,
-        aiAssertionResults = null,
         contextData = mapOf("key" to Long.MAX_VALUE - 100),
       ),
       CaptureResult.Unchanged(
diff --git a/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProject.kt b/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProject.kt
index 7a107a9f..c3667a3b 100644
--- a/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProject.kt
+++ b/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProject.kt
@@ -18,11 +18,7 @@ class RoborazziGradleRootProject(val testProjectDir: TemporaryFolder) {
   val appModule = AppModule(this, testProjectDir)
   val previewModule = PreviewModule(this, testProjectDir)
 
-  fun runTask(
-    task: String,
-    buildType: BuildType,
-    additionalParameters: Array<String>
-  ): BuildResult {
+  fun runTask(task: String, buildType: BuildType, additionalParameters: Array<String>): BuildResult {
     val buildResult = GradleRunner.create()
       .withProjectDir(testProjectDir.root)
       .withArguments(
@@ -64,11 +60,6 @@ class AppModule(val rootProject: RoborazziGradleRootProject, val testProjectDir:
     return runTask(task)
   }
 
-  fun recordWithCleanupOldScreenshots(): BuildResult {
-    val task = "recordRoborazziDebug"
-    return runTask(task, additionalParameters = arrayOf("-Proborazzi.cleanupOldScreenshots=true"))
-  }
-
   fun recordWithFilter1(): BuildResult {
     val task = "recordRoborazziDebug"
     return runTask(
@@ -145,11 +136,6 @@ class AppModule(val rootProject: RoborazziGradleRootProject, val testProjectDir:
     return runTask(task)
   }
 
-  fun compareWithCleanupOldScreenshots(): BuildResult {
-    val task = "compareRoborazziDebug"
-    return runTask(task, additionalParameters = arrayOf("-Proborazzi.cleanupOldScreenshots=true"))
-  }
-
   fun clear(): BuildResult {
     val task = "clearRoborazziDebug"
     return runTask(task)
@@ -195,7 +181,7 @@ class AppModule(val rootProject: RoborazziGradleRootProject, val testProjectDir:
     buildGradle.addIncludeBuild()
 
     val buildResult = rootProject.runTask(
-      "app:" + task,
+      "app:"+task,
       buildType,
       additionalParameters
     )
@@ -206,9 +192,8 @@ class AppModule(val rootProject: RoborazziGradleRootProject, val testProjectDir:
     private val PATH = "app/build.gradle.kts"
     var removeOutputDirBeforeTestTypeTask = false
     var customOutputDirPath: String? = null
-
     init {
-      addIncludeBuild()
+        addIncludeBuild()
     }
 
     fun addIncludeBuild() {
diff --git a/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProjectTest.kt b/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProjectTest.kt
index fa1b27bc..b207e37c 100644
--- a/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProjectTest.kt
+++ b/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProjectTest.kt
@@ -3,25 +3,15 @@ package io.github.takahirom.roborazzi
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TemporaryFolder
-import org.junit.rules.TestWatcher
 
 /**
  * Run this test with `cd include-build` and `./gradlew roborazzi-gradle-plugin:check`
- * You can also run this test with the following command:
- * ./gradlew roborazzi-gradle-plugin:integrationTest --tests "*RoborazziGradleProjectTest.record"
  */
 class RoborazziGradleProjectTest {
 
   @get:Rule
   val testProjectDir = TemporaryFolder()
 
-  @get:Rule
-  val testNameOutputRule = object : TestWatcher() {
-    override fun starting(description: org.junit.runner.Description?) {
-      println("RoborazziGradleProjectTest.${description?.methodName} started")
-    }
-  }
-
   private val className = "com.github.takahirom.integration_test_project.RoborazziTest"
 
   private var defaultBuildDir = "build"
@@ -318,7 +308,8 @@ class RoborazziGradleProjectTest {
       removeTests()
       record()
 
-      checkResultsSummaryFileNotExists()
+      // Summary file will be generated even if no test files
+      checkResultsSummaryFileExists()
       // Test will be skipped when no source so no output
       checkResultFileNotExists(resultFileSuffix)
     }
@@ -401,21 +392,6 @@ class RoborazziGradleProjectTest {
     }
   }
 
-  @Test
-  fun compareWithDeleteOldScreenshot() {
-    RoborazziGradleRootProject(testProjectDir).appModule.apply {
-      recordWithCleanupOldScreenshots()
-      changeScreen()
-      compareWithCleanupOldScreenshots()
-
-      checkResultsSummaryFileExists()
-      checkRecordedFileExists("$screenshotAndName.testCapture.png")
-      checkResultFileExists(resultFileSuffix)
-      checkRecordedFileExists("$screenshotAndName.testCapture_compare.png")
-      checkRecordedFileExists("$screenshotAndName.testCapture_actual.png")
-    }
-  }
-
   @Test
   fun compareWithSystemParameter() {
     println("start compareWithSystemParameter")
@@ -444,21 +420,6 @@ class RoborazziGradleProjectTest {
     }
   }
 
-  @Test
-  fun ifWeUseCleanupOldScreenshotsTheOldScreenshotsShouldNotExit() {
-    RoborazziGradleRootProject(testProjectDir).appModule.apply {
-      recordWithCleanupOldScreenshots()
-      removeTests()
-      addTestCaptureWithCustomPathTest()
-      recordWithCleanupOldScreenshots()
-
-      checkResultsSummaryFileExists()
-      checkRecordedFileNotExists("$screenshotAndName.testCapture.png")
-      checkRecordedFileExists("$customReferenceScreenshotAndName.png")
-      checkRecordedFileExists("app/build/outputs/roborazzi/custom_outputDirectoryPath_from_rule/custom_outputFileProvider-com.github.takahirom.integration_test_project.RoborazziTest.testCaptureWithCustomPath.png")
-    }
-  }
-
   @Test
   fun compareWithCustomPath() {
     RoborazziGradleRootProject(testProjectDir).appModule.apply {
diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/GeminiAiAiTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/GeminiAiAiTest.kt
deleted file mode 100644
index 4e04d022..00000000
--- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/GeminiAiAiTest.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.github.takahirom.roborazzi.sample
-
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.github.takahirom.roborazzi.AiAssertionOptions
-import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH
-import com.github.takahirom.roborazzi.GeminiAiAssertionModel
-import com.github.takahirom.roborazzi.ROBORAZZI_DEBUG
-import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
-import com.github.takahirom.roborazzi.RoborazziOptions
-import com.github.takahirom.roborazzi.RoborazziRule
-import com.github.takahirom.roborazzi.RoborazziTaskType
-import com.github.takahirom.roborazzi.captureRoboImage
-import com.github.takahirom.roborazzi.provideRoborazziContext
-import com.github.takahirom.roborazzi.roboOutputName
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.GraphicsMode
-import java.io.File
-
-@RunWith(AndroidJUnit4::class)
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-@Config(
-  sdk = [30],
-  qualifiers = RobolectricDeviceQualifiers.NexusOne
-)
-class GeminiAiAiTest {
-  @get:Rule
-  val composeTestRule = createAndroidComposeRule<MainActivity>()
-
-  @get:Rule
-  val roborazziRule = RoborazziRule(
-    options = RoborazziRule.Options(
-      roborazziOptions = RoborazziOptions(
-        taskType =  RoborazziTaskType.Compare,
-        compareOptions = RoborazziOptions.CompareOptions(
-          aiAssertionOptions = AiAssertionOptions(
-            aiAssertionModel = GeminiAiAssertionModel(
-              apiKey = System.getenv("gemini_api_key") ?: ""
-            ),
-          )
-        )
-      )
-    )
-  )
-
-  @Test
-  fun captureWithAi() {
-    ROBORAZZI_DEBUG = true
-    if (System.getenv("gemini_api_key") == null) {
-      println("Skip the test because gemini_api_key is not set.")
-      return
-    }
-    File(DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + File.separator + roboOutputName() + ".png").delete()
-    onView(ViewMatchers.isRoot())
-      .captureRoboImage(
-        roborazziOptions = provideRoborazziContext().options.addedAiAssertions(
-          AiAssertionOptions.AiAssertion(
-            assertionPrompt = "it should have PREVIOUS button",
-            requiredFulfillmentPercent = 90,
-          ),
-          AiAssertionOptions.AiAssertion(
-            assertionPrompt = "it should show First Fragment",
-            requiredFulfillmentPercent = 90,
-          )
-        )
-      )
-    File(DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + File.separator + roboOutputName() + "_compare.png").delete()
-  }
-}
\ No newline at end of file
diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt
index 1f7774e7..7bd95b6e 100644
--- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt
+++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt
@@ -164,8 +164,7 @@ class ManualTest {
 
   @Test
   fun captureRoboImageSampleWithQuery() {
-    val filePath =
-      "${roborazziSystemPropertyOutputDirectory()}/manual_view_first_screen_with_query_view.png"
+    val filePath = "${roborazziSystemPropertyOutputDirectory()}/manual_view_first_screen_with_query_view.png"
     onView(ViewMatchers.isRoot())
       .captureRoboImage(
         filePath = filePath,
diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/OpenAiTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/OpenAiTest.kt
deleted file mode 100644
index ae9deb92..00000000
--- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/OpenAiTest.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.github.takahirom.roborazzi.sample
-
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.github.takahirom.roborazzi.AiAssertionOptions
-import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH
-import com.github.takahirom.roborazzi.OpenAiAiAssertionModel
-import com.github.takahirom.roborazzi.ROBORAZZI_DEBUG
-import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
-import com.github.takahirom.roborazzi.RoborazziOptions
-import com.github.takahirom.roborazzi.RoborazziRule
-import com.github.takahirom.roborazzi.RoborazziTaskType
-import com.github.takahirom.roborazzi.captureRoboImage
-import com.github.takahirom.roborazzi.provideRoborazziContext
-import com.github.takahirom.roborazzi.roboOutputName
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.GraphicsMode
-import java.io.File
-
-@RunWith(AndroidJUnit4::class)
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-@Config(
-  sdk = [30],
-  qualifiers = RobolectricDeviceQualifiers.NexusOne
-)
-class OpenAiTest {
-  @get:Rule
-  val composeTestRule = createAndroidComposeRule<MainActivity>()
-
-  @get:Rule
-  val roborazziRule = RoborazziRule(
-    options = RoborazziRule.Options(
-      roborazziOptions = RoborazziOptions(
-        taskType =  RoborazziTaskType.Compare,
-        compareOptions = RoborazziOptions.CompareOptions(
-          aiAssertionOptions = AiAssertionOptions(
-            aiAssertionModel = OpenAiAiAssertionModel(
-              apiKey = System.getenv("openai_api_key").orEmpty(),
-              modelName = "gpt-4o",
-            ),
-          )
-        )
-      )
-    )
-  )
-
-  @Test
-  fun captureWithAi() {
-    ROBORAZZI_DEBUG = true
-    if (System.getenv("openai_api_key") == null) {
-      println("Skip the test because openai_api_key is not set.")
-      return
-    }
-    File(DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + File.separator + roboOutputName() + ".png").delete()
-    onView(ViewMatchers.isRoot())
-      .captureRoboImage(
-        roborazziOptions = provideRoborazziContext().options.addedAiAssertions(
-          AiAssertionOptions.AiAssertion(
-            assertionPrompt = "it should have PREVIOUS button",
-            requiredFulfillmentPercent = 90,
-          ),
-          AiAssertionOptions.AiAssertion(
-            assertionPrompt = "it should show First Fragment",
-            requiredFulfillmentPercent = 90,
-          )
-        )
-      )
-    File(DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + File.separator + roboOutputName() + "_compare.png").delete()
-  }
-}
\ No newline at end of file
diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/AiManualTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/AiManualTest.kt
deleted file mode 100644
index dedfb948..00000000
--- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/AiManualTest.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-package com.github.takahirom.roborazzi.sample.boxed
-
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onRoot
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.github.takahirom.roborazzi.AiAssertionOptions
-import com.github.takahirom.roborazzi.AiAssertionResult
-import com.github.takahirom.roborazzi.AiAssertionResults
-import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH
-import com.github.takahirom.roborazzi.ROBORAZZI_DEBUG
-import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
-import com.github.takahirom.roborazzi.RoborazziOptions
-import com.github.takahirom.roborazzi.RoborazziTaskType
-import com.github.takahirom.roborazzi.captureRoboImage
-import com.github.takahirom.roborazzi.roboOutputName
-import com.github.takahirom.roborazzi.sample.MainActivity
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.GraphicsMode
-import java.io.File
-
-@RunWith(AndroidJUnit4::class)
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-@Config(
-  sdk = [30],
-  qualifiers = RobolectricDeviceQualifiers.NexusOne
-)
-class AiManualTest {
-  @get:Rule
-  val composeTestRule = createAndroidComposeRule<MainActivity>()
-
-  @Test(
-    expected = AssertionError::class
-  )
-  fun whenAiReturnError() {
-    boxedEnvironment {
-      try {
-        ROBORAZZI_DEBUG = true
-        composeTestRule.onRoot()
-          .captureRoboImage(
-            roborazziOptions = createOptionsFulfillmentPercent(0)
-          )
-      }finally {
-        File(DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH +File.separator+ roboOutputName() + "_compare.png").delete()
-      }
-    }
-  }
-
-  @Test
-  fun whenAiReturnSuccess() {
-    boxedEnvironment {
-      ROBORAZZI_DEBUG = true
-      composeTestRule.onRoot()
-        .captureRoboImage(
-          roborazziOptions = createOptionsFulfillmentPercent(100)
-        )
-      File(DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + File.separator + roboOutputName() + "_compare.png").delete()
-    }
-  }
-
-  private fun createOptionsFulfillmentPercent(fulfillmentPercent: Int) = RoborazziOptions(
-    // Even compare task, it will be failed because of the manual AI model.
-    taskType = RoborazziTaskType.Compare,
-    compareOptions = RoborazziOptions.CompareOptions(
-      aiAssertionOptions = AiAssertionOptions(
-        aiAssertionModel = object : AiAssertionOptions.AiAssertionModel {
-          override fun assert(
-            referenceImageFilePath: String,
-            comparisonImageFilePath: String,
-            actualImageFilePath: String,
-            aiAssertionOptions: AiAssertionOptions
-          ): AiAssertionResults {
-            return AiAssertionResults(
-              aiAssertionResults = aiAssertionOptions.aiAssertions.map { assertion ->
-                AiAssertionResult(
-                  assertionPrompt = assertion.assertionPrompt,
-                  fulfillmentPercent = fulfillmentPercent,
-                  requiredFulfillmentPercent = assertion.requiredFulfillmentPercent,
-                  failIfNotFulfilled = assertion.failIfNotFulfilled,
-                  explanation = "This is a manual test.",
-                )
-              }
-            )
-          }
-        },
-        aiAssertions = listOf(
-          AiAssertionOptions.AiAssertion(
-            assertionPrompt = "it should have PREVIOUS button",
-            requiredFulfillmentPercent = 90,
-          ),
-        ),
-      )
-    )
-  )
-}
\ No newline at end of file