Skip to content

Commit f561054

Browse files
committed
Add ThreadRepo
1 parent 61e2dd5 commit f561054

3 files changed

Lines changed: 316 additions & 3 deletions

File tree

codegen-gradle/build.gradle.kts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ plugins {
1818
group = "com.embabel.guide"
1919
version = "0.1.0-SNAPSHOT"
2020

21+
val drivineVersion = "0.0.18"
22+
2123
repositories {
2224
mavenCentral()
2325
mavenLocal()
@@ -31,10 +33,10 @@ repositories {
3133

3234
dependencies {
3335
// Drivine core library
34-
implementation("org.drivine:drivine4j:0.0.15")
36+
implementation("org.drivine:drivine4j:$drivineVersion")
3537

3638
// KSP processor for code generation
37-
ksp("org.drivine:drivine4j-codegen:0.0.15")
39+
ksp("org.drivine:drivine4j-codegen:$drivineVersion")
3840

3941
// Dependencies needed for domain classes to compile
4042
implementation("com.embabel.agent:embabel-agent-api:0.3.2-SNAPSHOT")

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<dependency>
4747
<groupId>org.drivine</groupId>
4848
<artifactId>drivine4j-spring-boot-starter</artifactId>
49-
<version>0.0.15</version>
49+
<version>0.0.18</version>
5050
</dependency>
5151

5252

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
/*
2+
* Copyright 2024-2025 Embabel Software, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.embabel.guide.chat.repository
17+
18+
import com.embabel.guide.Neo4jPropertiesInitializer
19+
import com.embabel.guide.chat.service.ThreadService
20+
import com.embabel.guide.domain.GuideUserData
21+
import com.embabel.guide.domain.GuideUserRepository
22+
import com.embabel.guide.domain.WebUserData
23+
import com.embabel.guide.util.UUIDv7
24+
import org.junit.jupiter.api.Assertions.*
25+
import org.junit.jupiter.api.BeforeEach
26+
import org.junit.jupiter.api.Test
27+
import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration
28+
import org.springframework.beans.factory.annotation.Autowired
29+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
30+
import org.springframework.boot.test.context.SpringBootTest
31+
import org.springframework.test.context.ActiveProfiles
32+
import org.springframework.test.context.ContextConfiguration
33+
import org.springframework.transaction.annotation.Transactional
34+
import java.util.*
35+
36+
/**
37+
* Test for ThreadRepositoryImpl using the type-safe DSL.
38+
*/
39+
@SpringBootTest
40+
@ActiveProfiles("test")
41+
@ContextConfiguration(initializers = [Neo4jPropertiesInitializer::class])
42+
@ImportAutoConfiguration(exclude = [McpClientAutoConfiguration::class])
43+
@Transactional
44+
class ThreadRepositoryImplTest {
45+
46+
@Autowired
47+
private lateinit var threadRepository: ThreadRepositoryImpl
48+
49+
@Autowired
50+
private lateinit var guideUserRepository: GuideUserRepository
51+
52+
private lateinit var testUser: com.embabel.guide.domain.GuideUser
53+
54+
@BeforeEach
55+
fun setUp() {
56+
// Create a test user for thread authorship
57+
val guideUserData = GuideUserData(
58+
UUID.randomUUID().toString(),
59+
null,
60+
null
61+
)
62+
val webUserData = WebUserData(
63+
"thread-test-${UUID.randomUUID()}",
64+
"Thread Test User",
65+
"threadtestuser-${UUID.randomUUID()}",
66+
"threadtest@example.com",
67+
"hashedpassword",
68+
null
69+
)
70+
testUser = guideUserRepository.createWithWebUser(guideUserData, webUserData)
71+
}
72+
73+
@Test
74+
fun `test create thread with message`() {
75+
// Given: A thread ID and message
76+
val threadId = UUIDv7.generateString()
77+
val message = "Hello, this is a test message"
78+
79+
// When: We create a thread with the message
80+
val created = threadRepository.createWithMessage(
81+
threadId = threadId,
82+
userId = testUser.core.id,
83+
title = "Test Thread",
84+
message = message,
85+
role = ThreadService.ROLE_USER
86+
)
87+
88+
// Then: The thread is created with the correct data
89+
assertNotNull(created)
90+
assertEquals(threadId, created.thread.threadId)
91+
assertEquals("Test Thread", created.thread.title)
92+
assertNotNull(created.thread.createdAt)
93+
94+
// And: The thread has one turn with the message
95+
assertEquals(1, created.turns.size)
96+
val turn = created.turns.first()
97+
assertEquals(ThreadService.ROLE_USER, turn.turn.role)
98+
assertEquals(message, turn.current.text)
99+
assertEquals(testUser.core.id, turn.authoredBy?.core?.id)
100+
}
101+
102+
@Test
103+
fun `test create thread with assistant message`() {
104+
// Given: A thread ID and assistant message
105+
val threadId = UUIDv7.generateString()
106+
val message = "Welcome! How can I help you?"
107+
108+
// When: We create a thread with an assistant message
109+
val created = threadRepository.createWithMessage(
110+
threadId = threadId,
111+
userId = testUser.core.id,
112+
title = "Welcome",
113+
message = message,
114+
role = ThreadService.ROLE_ASSISTANT
115+
)
116+
117+
// Then: The turn has the assistant role
118+
assertEquals(1, created.turns.size)
119+
assertEquals(ThreadService.ROLE_ASSISTANT, created.turns.first().turn.role)
120+
assertEquals(message, created.turns.first().current.text)
121+
}
122+
123+
@Test
124+
fun `test find thread by ID`() {
125+
// Given: We create a thread
126+
val threadId = UUIDv7.generateString()
127+
threadRepository.createWithMessage(
128+
threadId = threadId,
129+
userId = testUser.core.id,
130+
title = "Findable Thread",
131+
message = "Test message",
132+
role = ThreadService.ROLE_USER
133+
)
134+
135+
// When: We find by thread ID
136+
val found = threadRepository.findByThreadId(threadId)
137+
138+
// Then: The thread is found
139+
assertTrue(found.isPresent)
140+
assertEquals(threadId, found.get().thread.threadId)
141+
assertEquals("Findable Thread", found.get().thread.title)
142+
assertEquals(1, found.get().turns.size)
143+
}
144+
145+
@Test
146+
fun `test findByThreadId returns empty when not found`() {
147+
// When: We search for a non-existent thread
148+
val found = threadRepository.findByThreadId("nonexistent-thread-id")
149+
150+
// Then: An empty Optional is returned
151+
assertFalse(found.isPresent)
152+
}
153+
154+
@Test
155+
fun `test find threads by user ID`() {
156+
// Given: We create multiple threads for the test user
157+
val thread1Id = UUIDv7.generateString()
158+
val thread2Id = UUIDv7.generateString()
159+
160+
threadRepository.createWithMessage(
161+
threadId = thread1Id,
162+
userId = testUser.core.id,
163+
title = "User Thread 1",
164+
message = "First thread message",
165+
role = ThreadService.ROLE_USER
166+
)
167+
168+
threadRepository.createWithMessage(
169+
threadId = thread2Id,
170+
userId = testUser.core.id,
171+
title = "User Thread 2",
172+
message = "Second thread message",
173+
role = ThreadService.ROLE_USER
174+
)
175+
176+
// When: We find threads by user ID
177+
val threads = threadRepository.findByUserId(testUser.core.id)
178+
179+
// Then: Both threads are found
180+
assertTrue(threads.size >= 2)
181+
assertTrue(threads.any { it.thread.threadId == thread1Id })
182+
assertTrue(threads.any { it.thread.threadId == thread2Id })
183+
}
184+
185+
@Test
186+
fun `test findByUserId returns empty list when user has no threads`() {
187+
// Given: A user with no threads
188+
val anotherUser = guideUserRepository.createWithWebUser(
189+
GuideUserData(UUID.randomUUID().toString(), null, null),
190+
WebUserData(
191+
"no-threads-${UUID.randomUUID()}",
192+
"No Threads User",
193+
"nothreadsuser-${UUID.randomUUID()}",
194+
"nothreads@example.com",
195+
"hash",
196+
null
197+
)
198+
)
199+
200+
// When: We find threads for this user
201+
val threads = threadRepository.findByUserId(anotherUser.core.id)
202+
203+
// Then: Empty list is returned
204+
assertTrue(threads.isEmpty())
205+
}
206+
207+
@Test
208+
fun `test thread has correct timestamps`() {
209+
// Given: We create a thread
210+
val threadId = UUIDv7.generateString()
211+
val beforeCreate = java.time.Instant.now()
212+
213+
val created = threadRepository.createWithMessage(
214+
threadId = threadId,
215+
userId = testUser.core.id,
216+
title = null,
217+
message = "Timestamp test",
218+
role = ThreadService.ROLE_USER
219+
)
220+
221+
val afterCreate = java.time.Instant.now()
222+
223+
// Then: All timestamps are within the expected range
224+
assertNotNull(created.thread.createdAt)
225+
assertTrue(created.thread.createdAt!! >= beforeCreate.minusMillis(100))
226+
assertTrue(created.thread.createdAt!! <= afterCreate.plusMillis(100))
227+
228+
val turn = created.turns.first()
229+
assertNotNull(turn.turn.createdAt)
230+
assertNotNull(turn.current.createdAt)
231+
}
232+
233+
@Test
234+
fun `test thread without title`() {
235+
// Given: We create a thread without a title
236+
val threadId = UUIDv7.generateString()
237+
238+
// When: We create the thread with null title
239+
val created = threadRepository.createWithMessage(
240+
threadId = threadId,
241+
userId = testUser.core.id,
242+
title = null,
243+
message = "No title thread",
244+
role = ThreadService.ROLE_USER
245+
)
246+
247+
// Then: The thread is created with null title
248+
assertNull(created.thread.title)
249+
}
250+
251+
@Test
252+
fun `test deleteAll removes all threads`() {
253+
// Given: We create a thread
254+
val threadId = UUIDv7.generateString()
255+
threadRepository.createWithMessage(
256+
threadId = threadId,
257+
userId = testUser.core.id,
258+
title = "Delete Test",
259+
message = "Will be deleted",
260+
role = ThreadService.ROLE_USER
261+
)
262+
263+
// When: We delete all threads
264+
threadRepository.deleteAll()
265+
266+
// Then: The thread is no longer found
267+
val found = threadRepository.findByThreadId(threadId)
268+
assertFalse(found.isPresent)
269+
}
270+
271+
@Test
272+
fun `test turn version has correct editor role`() {
273+
// Given: We create threads with different roles
274+
val userThreadId = UUIDv7.generateString()
275+
val assistantThreadId = UUIDv7.generateString()
276+
277+
val userThread = threadRepository.createWithMessage(
278+
threadId = userThreadId,
279+
userId = testUser.core.id,
280+
title = null,
281+
message = "User message",
282+
role = ThreadService.ROLE_USER
283+
)
284+
285+
val assistantThread = threadRepository.createWithMessage(
286+
threadId = assistantThreadId,
287+
userId = testUser.core.id,
288+
title = null,
289+
message = "Assistant message",
290+
role = ThreadService.ROLE_ASSISTANT
291+
)
292+
293+
// Then: The editor role matches the turn role
294+
assertEquals(ThreadService.ROLE_USER, userThread.turns.first().current.editorRole)
295+
assertEquals(ThreadService.ROLE_ASSISTANT, assistantThread.turns.first().current.editorRole)
296+
}
297+
298+
@Test
299+
fun `test createWithMessage throws when user not found`() {
300+
// When/Then: Creating a thread with non-existent user throws
301+
assertThrows(IllegalArgumentException::class.java) {
302+
threadRepository.createWithMessage(
303+
threadId = UUIDv7.generateString(),
304+
userId = "nonexistent-user-id",
305+
title = null,
306+
message = "Should fail",
307+
role = ThreadService.ROLE_USER
308+
)
309+
}
310+
}
311+
}

0 commit comments

Comments
 (0)