@@ -28,7 +28,12 @@ import com.google.adk.kt.types.Part
2828import com.google.common.truth.Truth.assertThat
2929import java.io.IOException
3030import kotlin.test.assertFailsWith
31+ import kotlin.time.Duration
32+ import kotlin.time.Duration.Companion.hours
33+ import kotlin.time.Duration.Companion.milliseconds
34+ import kotlin.time.Duration.Companion.seconds
3135import kotlin.time.Instant
36+ import kotlinx.coroutines.runBlocking
3237import kotlinx.coroutines.test.runTest
3338import kotlinx.serialization.json.JsonObject
3439import kotlinx.serialization.json.JsonPrimitive
@@ -54,27 +59,33 @@ import org.mockito.kotlin.verifyBlocking
5459@RunWith(JUnit4 ::class )
5560class VertexAiSessionServiceTest {
5661
57- private fun service (client : VertexAiSessionsClient ) =
62+ private fun service (client : VertexAiSessionsClient , sessionTtl : Duration ? = null ) =
5863 VertexAiSessionService (
5964 client,
6065 project = PROJECT ,
6166 location = LOCATION ,
6267 reasoningEngineId = ENGINE_ID ,
68+ sessionTtl = sessionTtl,
6369 )
6470
71+ /* * A client that accepts any create call, for asserting which expiration was forwarded. */
72+ private fun expiringSessionClient () =
73+ mock<VertexAiSessionsClient > {
74+ onBlocking { createSession(any(), any(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn
75+ Result .success(SessionDto (name = " reasoningEngines/123/sessions/s" ))
76+ }
77+
6578 @Test
6679 fun addressesConfiguredEngineRegardlessOfAppName () = runTest {
67- val client =
68- mock<VertexAiSessionsClient > {
69- onBlocking { createSession(any(), any(), anyOrNull()) } doReturn
70- Result .success(SessionDto (name = " reasoningEngines/123/sessions/s" ))
71- }
80+ val client = expiringSessionClient()
7281
7382 // The app name is only a label; the service always addresses the engine set at construction.
7483 val unused =
7584 service(client).createSession(SessionKey (" any-label" , " user" , id = null ), state = null )
7685
77- verifyBlocking(client) { createSession(eq(ENGINE ), eq(" user" ), anyOrNull()) }
86+ verifyBlocking(client) {
87+ createSession(eq(ENGINE ), eq(" user" ), anyOrNull(), anyOrNull(), anyOrNull())
88+ }
7889 }
7990
8091 @Test
@@ -137,7 +148,9 @@ class VertexAiSessionServiceTest {
137148 fun createSession_mapsClientResponse () = runTest {
138149 val client =
139150 mock<VertexAiSessionsClient > {
140- onBlocking { createSession(eq(ENGINE ), eq(" user" ), anyOrNull()) } doReturn
151+ onBlocking {
152+ createSession(eq(ENGINE ), eq(" user" ), anyOrNull(), anyOrNull(), anyOrNull())
153+ } doReturn
141154 Result .success(
142155 SessionDto (
143156 name = " reasoningEngines/123/sessions/session-1" ,
@@ -160,7 +173,7 @@ class VertexAiSessionServiceTest {
160173 fun createSession_clientFails_propagates () = runTest {
161174 val client =
162175 mock<VertexAiSessionsClient > {
163- onBlocking { createSession(any(), any(), anyOrNull()) } doReturn
176+ onBlocking { createSession(any(), any(), anyOrNull(), anyOrNull(), anyOrNull() ) } doReturn
164177 Result .failure(IOException (" boom" ))
165178 }
166179
@@ -169,6 +182,142 @@ class VertexAiSessionServiceTest {
169182 }
170183 }
171184
185+ @Test
186+ fun createSession_ttl_forwardsTtlOnly () {
187+ val client = expiringSessionClient()
188+
189+ runBlocking {
190+ val unused =
191+ service(client).createSession(SessionKey (" 123" , " user" , id = null ), ttl = 24 .hours)
192+ }
193+
194+ verifyBlocking(client) {
195+ createSession(eq(ENGINE ), eq(" user" ), anyOrNull(), eq(24 .hours), eq(null ))
196+ }
197+ }
198+
199+ @Test
200+ fun createSession_expireTime_forwardsExpireTimeOnly () {
201+ val client = expiringSessionClient()
202+ val expiry = Instant .parse(" 2026-10-01T00:00:00Z" )
203+
204+ runBlocking {
205+ val unused =
206+ service(client).createSession(SessionKey (" 123" , " user" , id = null ), expireTime = expiry)
207+ }
208+
209+ verifyBlocking(client) {
210+ createSession(eq(ENGINE ), eq(" user" ), anyOrNull(), eq(null ), eq(expiry))
211+ }
212+ }
213+
214+ @Test
215+ fun createSession_noExpiration_forwardsNeither () {
216+ val client = expiringSessionClient()
217+
218+ // The SessionService overload must not invent an expiration of its own.
219+ runBlocking {
220+ val unused = service(client).createSession(SessionKey (" 123" , " user" , id = null ))
221+ }
222+
223+ verifyBlocking(client) {
224+ createSession(eq(ENGINE ), eq(" user" ), anyOrNull(), eq(null ), eq(null ))
225+ }
226+ }
227+
228+ @Test
229+ fun createSession_ttlAndExpireTime_throwsWithoutCallingBackend () {
230+ val client = expiringSessionClient()
231+
232+ assertFailsWith<IllegalArgumentException > {
233+ runBlocking {
234+ service(client)
235+ .createSession(
236+ SessionKey (" 123" , " user" , id = null ),
237+ ttl = 24 .hours,
238+ expireTime = Instant .parse(" 2026-10-01T00:00:00Z" ),
239+ )
240+ }
241+ }
242+ verifyBlocking(client, never()) {
243+ createSession(any(), any(), anyOrNull(), anyOrNull(), anyOrNull())
244+ }
245+ }
246+
247+ @Test
248+ fun createSession_serviceTtl_appliedThroughSessionServiceInterface () {
249+ val client = expiringSessionClient()
250+ // The runner and the web server create sessions through the interface, where the per-call
251+ // overload is unreachable, so the configured default has to reach them.
252+ val sessionService: SessionService = service(client, sessionTtl = 24 .hours)
253+
254+ runBlocking {
255+ val unused = sessionService.createSession(SessionKey (" 123" , " user" , id = null ))
256+ }
257+
258+ verifyBlocking(client) {
259+ createSession(eq(ENGINE ), eq(" user" ), anyOrNull(), eq(24 .hours), eq(null ))
260+ }
261+ }
262+
263+ @Test
264+ fun createSession_perCallTtl_overridesServiceTtl () {
265+ val client = expiringSessionClient()
266+
267+ runBlocking {
268+ val unused =
269+ service(client, sessionTtl = 24 .hours)
270+ .createSession(SessionKey (" 123" , " user" , id = null ), ttl = 48 .hours)
271+ }
272+
273+ verifyBlocking(client) {
274+ createSession(eq(ENGINE ), eq(" user" ), anyOrNull(), eq(48 .hours), eq(null ))
275+ }
276+ }
277+
278+ @Test
279+ fun createSession_perCallExpireTime_suppressesServiceTtl () {
280+ val client = expiringSessionClient()
281+ val expiry = Instant .parse(" 2026-10-01T00:00:00Z" )
282+
283+ // Both arms are a single wire choice, so the default must not ride along with expireTime.
284+ runBlocking {
285+ val unused =
286+ service(client, sessionTtl = 24 .hours)
287+ .createSession(SessionKey (" 123" , " user" , id = null ), expireTime = expiry)
288+ }
289+
290+ verifyBlocking(client) {
291+ createSession(eq(ENGINE ), eq(" user" ), anyOrNull(), eq(null ), eq(expiry))
292+ }
293+ }
294+
295+ @Test
296+ fun constructor_sessionTtlBelowOneSecond_throws () {
297+ for (bad in listOf (Duration .ZERO , (- 1 ).seconds, 500 .milliseconds)) {
298+ assertFailsWith<IllegalArgumentException > {
299+ service(mock<VertexAiSessionsClient >(), sessionTtl = bad)
300+ }
301+ }
302+ }
303+
304+ @Test
305+ fun createSession_ttlBelowOneSecond_throwsWithoutCallingBackend () {
306+ val client = expiringSessionClient()
307+
308+ // 500ms is positive but truncates to "0s" on the wire, so it must be rejected too.
309+ for (bad in listOf (Duration .ZERO , (- 1 ).seconds, 500 .milliseconds)) {
310+ assertFailsWith<IllegalArgumentException > {
311+ runBlocking {
312+ service(client).createSession(SessionKey (" 123" , " user" , id = null ), ttl = bad)
313+ }
314+ }
315+ }
316+ verifyBlocking(client, never()) {
317+ createSession(any(), any(), anyOrNull(), anyOrNull(), anyOrNull())
318+ }
319+ }
320+
172321 @Test
173322 fun getSession_notFound_returnsNull () = runTest {
174323 val client =
0 commit comments