Skip to content

Commit 7e837ec

Browse files
committed
tests: Refactor A2A test, bump Testcontainers, improve awaitility tests
1. Replace Testcontainers JUnit dependency and refactor Docker test setup - Removed `testcontainers-junit` dependency and replaced usage with direct `testcontainers` library. - Don't build container in Gradle - Upgraded `testcontainers` to the latest version. - Refactored Python A2A server Docker handling into `TestA2APythonServer` object for cleaner test configuration. 2. Enhance Awaitility extensions to support return values - Added support for returning values from `untilAsserted` extensions. - Updated test cases to verify return value functionality. 3. Replace direct assertions with Awaitility in A2A tests - Refactored tests to use Awaitility's coroutine extensions for asserting eventually consistent conditions. - Simplified `sendMessageStreaming`, `resubscribeTask`, and notification configuration logic.
1 parent 9780b59 commit 7e837ec

File tree

8 files changed

+140
-112
lines changed

8 files changed

+140
-112
lines changed

.github/pull_request_template.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Also, please tick the appropriate points in the checklist below.
2020
- [ ] Documentation update
2121
- [ ] Tests improvement
2222
- [ ] Refactoring
23+
- [ ] CI/CD changes
24+
- [ ] Dependencies update
2325

2426
#### Checklist
2527
- [ ] The pull request has a description of the proposed change

a2a/a2a-client/build.gradle.kts

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import ai.koog.gradle.publish.maven.Publishing.publishToMaven
2-
import org.gradle.internal.os.OperatingSystem
3-
import org.gradle.kotlin.dsl.support.serviceOf
4-
import java.io.ByteArrayOutputStream
52

63
group = rootProject.group
74
version = rootProject.version
@@ -41,7 +38,7 @@ kotlin {
4138

4239
implementation(libs.ktor.client.cio)
4340
implementation(libs.ktor.client.logging)
44-
implementation(libs.testcontainers.junit)
41+
implementation(libs.testcontainers)
4542
runtimeOnly(libs.logback.classic)
4643
}
4744
}
@@ -57,36 +54,3 @@ kotlin {
5754
}
5855

5956
publishToMaven()
60-
61-
tasks.register<Exec>("dockerBuildTestPythonA2AServer") {
62-
group = "docker"
63-
description = "Build Python A2A test server image"
64-
workingDir = file("../test-python-a2a-server")
65-
commandLine = listOf("docker", "build", "-t", "test-python-a2a-server", ".")
66-
67-
onlyIf {
68-
// do not attempt to check for docker on windows
69-
if (OperatingSystem.current().isWindows) {
70-
return@onlyIf false
71-
}
72-
73-
try {
74-
val buffer = ByteArrayOutputStream()
75-
76-
serviceOf<ExecOperations>().exec {
77-
commandLine = listOf("docker", "--version")
78-
standardOutput = buffer
79-
errorOutput = buffer
80-
}
81-
82-
true
83-
} catch (_: Exception) {
84-
logger.warn("Docker not available. Skipping task 'dockerBuildTestPythonA2AServer'")
85-
86-
false
87-
}
88-
}
89-
}
90-
tasks.named("jvmTest") {
91-
dependsOn("dockerBuildTestPythonA2AServer")
92-
}

a2a/a2a-client/src/jvmTest/kotlin/ai/koog/a2a/client/A2AClientJsonRpcIntegrationTest.kt

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,15 @@ package ai.koog.a2a.client
22

33
import ai.koog.a2a.test.BaseA2AProtocolTest
44
import ai.koog.a2a.transport.client.jsonrpc.http.HttpJSONRPCClientTransport
5-
import ai.koog.test.utils.DockerAvailableCondition
65
import io.ktor.client.HttpClient
76
import io.ktor.client.plugins.logging.LogLevel
87
import io.ktor.client.plugins.logging.Logging
9-
import kotlinx.coroutines.test.runTest
8+
import kotlinx.coroutines.runBlocking
109
import org.junit.jupiter.api.AfterAll
1110
import org.junit.jupiter.api.BeforeAll
1211
import org.junit.jupiter.api.TestInstance
13-
import org.junit.jupiter.api.extension.ExtendWith
1412
import org.junit.jupiter.api.parallel.Execution
1513
import org.junit.jupiter.api.parallel.ExecutionMode
16-
import org.testcontainers.containers.GenericContainer
17-
import org.testcontainers.containers.wait.strategy.Wait
18-
import org.testcontainers.junit.jupiter.Container
19-
import org.testcontainers.junit.jupiter.Testcontainers
2014
import kotlin.test.Test
2115
import kotlin.time.Duration.Companion.seconds
2216

@@ -26,16 +20,10 @@ import kotlin.time.Duration.Companion.seconds
2620
* using the JSON-RPC standard.
2721
*/
2822
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
29-
@Testcontainers
30-
@ExtendWith(DockerAvailableCondition::class)
3123
@Execution(ExecutionMode.SAME_THREAD, reason = "Working with the same instance of test server.")
3224
class A2AClientJsonRpcIntegrationTest : BaseA2AProtocolTest() {
3325
companion object {
34-
@Container
35-
val testA2AServer: GenericContainer<*> =
36-
GenericContainer("test-python-a2a-server")
37-
.withExposedPorts(9999)
38-
.waitingFor(Wait.forListeningPort())
26+
val testA2AServer = TestA2APythonServer
3927
}
4028

4129
override val testTimeout = 10.seconds
@@ -47,14 +35,14 @@ class A2AClientJsonRpcIntegrationTest : BaseA2AProtocolTest() {
4735
}
4836

4937
@Suppress("HttpUrlsUsage")
50-
private val agentUrl by lazy { "http://${testA2AServer.host}:${testA2AServer.getMappedPort(9999)}" }
38+
private val agentUrl by lazy { "http://${testA2AServer.host}:${testA2AServer.port}" }
5139

5240
private lateinit var transport: HttpJSONRPCClientTransport
5341

5442
override lateinit var client: A2AClient
5543

5644
@BeforeAll
57-
fun setUp() = runTest {
45+
fun setUp() = runBlocking {
5846
transport = HttpJSONRPCClientTransport(
5947
url = agentUrl,
6048
baseHttpClient = httpClient
@@ -72,8 +60,9 @@ class A2AClientJsonRpcIntegrationTest : BaseA2AProtocolTest() {
7260
}
7361

7462
@AfterAll
75-
fun tearDown() = runTest {
63+
fun tearDown() {
7664
transport.close()
65+
testA2AServer.shutdown()
7766
}
7867

7968
@Test
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package ai.koog.a2a.client
2+
3+
import org.slf4j.LoggerFactory
4+
import org.testcontainers.containers.GenericContainer
5+
import org.testcontainers.containers.output.Slf4jLogConsumer
6+
import org.testcontainers.containers.wait.strategy.Wait
7+
import org.testcontainers.images.builder.ImageFromDockerfile
8+
import kotlin.io.path.Path
9+
import kotlin.time.Duration.Companion.seconds
10+
import kotlin.time.toJavaDuration
11+
12+
object TestA2APythonServer {
13+
14+
private const val PORT = 9999
15+
private val STARTUP_TIMEOUT = 20.seconds.toJavaDuration()
16+
17+
private val image =
18+
ImageFromDockerfile("test-python-a2a-server:latest", false) // "false" prevents deleting intermediate images
19+
.withFileFromPath(".", Path("../test-python-a2a-server")) // Specify Dockerfile context path
20+
private val container: GenericContainer<*> =
21+
GenericContainer(image)
22+
.withTmpFs(mapOf("/tmp" to "rw,noexec,size=16m"))
23+
.withExposedPorts(PORT)
24+
.withReuse(true)
25+
.waitingFor(Wait.forListeningPort())
26+
.withLogConsumer(Slf4jLogConsumer(LoggerFactory.getLogger(TestA2APythonServer::class.java)))
27+
.withStartupTimeout(STARTUP_TIMEOUT)
28+
29+
init {
30+
container.start()
31+
}
32+
33+
fun shutdown() = runCatching { container.stop() }
34+
35+
val host: String by lazy {
36+
container.host
37+
}
38+
39+
val port: Int by lazy {
40+
container.getMappedPort(PORT)
41+
}
42+
}

a2a/a2a-test/src/jvmMain/kotlin/ai/koog/a2a/test/BaseA2AProtocolTest.kt

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ai.koog.a2a.exceptions.A2AInternalErrorException
55
import ai.koog.a2a.model.AgentCapabilities
66
import ai.koog.a2a.model.AgentCard
77
import ai.koog.a2a.model.AgentSkill
8+
import ai.koog.a2a.model.Event
89
import ai.koog.a2a.model.Message
910
import ai.koog.a2a.model.MessageSendConfiguration
1011
import ai.koog.a2a.model.MessageSendParams
@@ -101,7 +102,7 @@ abstract class BaseA2AProtocolTest {
101102
}
102103

103104
open fun `test get authenticated extended agent card`() = runTest(timeout = testTimeout) {
104-
val request = Request<Nothing?>(data = null)
105+
val request = Request(data = null)
105106

106107
val response = client.getAuthenticatedExtendedAgentCard(request)
107108

@@ -172,10 +173,10 @@ abstract class BaseA2AProtocolTest {
172173

173174
val response = client.sendMessage(request)
174175

175-
response should {
176-
it.id shouldBe request.id
176+
response should { response ->
177+
response.id shouldBe request.id
177178

178-
it.data.shouldBeInstanceOf<Message> {
179+
response.data.shouldBeInstanceOf<Message> {
179180
it.role shouldBe Role.Agent
180181
it.parts shouldBe listOf(TextPart("Hello World"))
181182
it.contextId shouldBe "test-context"
@@ -197,19 +198,23 @@ abstract class BaseA2AProtocolTest {
197198
),
198199
)
199200

200-
val events = client
201-
.sendMessageStreaming(createTaskRequest)
202-
.toList()
203-
.map { it.data }
201+
val events: List<Event> = await.untilAsserted(this) {
202+
val list = client
203+
.sendMessageStreaming(createTaskRequest)
204+
.toList()
205+
.map { it.data }
204206

205-
events shouldHaveSize 3
206-
events[0].shouldBeInstanceOf<Task> {
207-
it.contextId shouldBe "test-context"
208-
it.status should {
207+
list shouldHaveSize 3
208+
return@untilAsserted list
209+
}!!
210+
211+
events[0].shouldBeInstanceOf<Task> { task ->
212+
task.contextId shouldBe "test-context"
213+
task.status should {
209214
it.state shouldBe TaskState.Submitted
210215
}
211216

212-
it.history shouldNotBeNull {
217+
task.history shouldNotBeNull {
213218
this shouldHaveSize 1
214219

215220
this[0] should {
@@ -344,12 +349,15 @@ abstract class BaseA2AProtocolTest {
344349
)
345350
)
346351

347-
val events = client
348-
.resubscribeTask(resubscribeTaskRequest)
349-
.toList()
350-
.map { it.data }
351-
352-
events.shouldNotBeEmpty()
352+
val events =
353+
await.ignoreExceptions().untilAsserted(this) {
354+
val list = client
355+
.resubscribeTask(resubscribeTaskRequest)
356+
.toList()
357+
.map { it.data }
358+
list.shouldNotBeEmpty()
359+
return@untilAsserted list
360+
}!!
353361

354362
events.shouldForAll {
355363
it.shouldBeInstanceOf<TaskStatusUpdateEvent> {
@@ -415,17 +423,21 @@ abstract class BaseA2AProtocolTest {
415423
)
416424
)
417425

418-
val getPushConfigResponse = client.getTaskPushNotificationConfig(getPushConfigRequest)
419-
getPushConfigResponse.data shouldBe pushConfig
426+
await.untilAsserted(this) {
427+
val response = client.getTaskPushNotificationConfig(getPushConfigRequest)
428+
response.data shouldBe pushConfig
429+
}
420430

421431
val listPushConfigRequest = Request(
422432
data = TaskIdParams(
423433
id = taskId,
424434
)
425435
)
426436

427-
val listPushConfigResponse = client.listTaskPushNotificationConfig(listPushConfigRequest)
428-
listPushConfigResponse.data shouldBe listOf(pushConfig)
437+
await.untilAsserted(this) {
438+
val listPushConfigResponse = client.listTaskPushNotificationConfig(listPushConfigRequest)
439+
listPushConfigResponse.data shouldBe listOf(pushConfig)
440+
}
429441

430442
val deletePushConfigRequest = Request(
431443
data = TaskPushNotificationConfigParams(
@@ -436,8 +448,10 @@ abstract class BaseA2AProtocolTest {
436448

437449
client.deleteTaskPushNotificationConfig(deletePushConfigRequest)
438450

439-
shouldThrowExactly<A2AInternalErrorException> {
440-
client.getTaskPushNotificationConfig(getPushConfigRequest)
451+
await.untilAsserted(this) {
452+
shouldThrowExactly<A2AInternalErrorException> {
453+
client.getTaskPushNotificationConfig(getPushConfigRequest)
454+
}
441455
}
442456
}
443457
}

gradle/libs.versions.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ mockito = "5.21.0"
2929
mockk = "1.14.7"
3030
mokksy = "0.5.1"
3131
mysql = "8.0.33"
32-
netty = "4.2.6.Final"
32+
netty = "4.2.9.Final"
3333
okhttp = "5.3.2"
3434
opentelemetry = "1.51.0"
3535
oshai-logging = "7.0.7"
@@ -38,7 +38,7 @@ slf4j = "2.0.17"
3838
spring-boot = "3.5.9"
3939
spring-management = "1.1.7"
4040
sqlite = "3.51.1.0"
41-
testcontainers = "1.19.7"
41+
testcontainers = "1.21.4"
4242

4343
[libraries]
4444
jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
@@ -90,7 +90,6 @@ mcp-server = { module = "io.modelcontextprotocol:kotlin-sdk-server", version.ref
9090
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
9191
jetsign-gradle-plugin = { module = "com.jetbrains:jet-sign", version.ref = "jetsign" }
9292
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
93-
testcontainers-junit = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" }
9493
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }
9594
testcontainers-mysql = { module = "org.testcontainers:mysql", version.ref = "testcontainers" }
9695
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }

0 commit comments

Comments
 (0)