diff --git a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/contract/OpenApiSpecExportTest.kt b/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/contract/OpenApiSpecExportTest.kt index 0f6be68..3d7caba 100644 --- a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/contract/OpenApiSpecExportTest.kt +++ b/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/contract/OpenApiSpecExportTest.kt @@ -7,8 +7,6 @@ import com.jorisjonkers.personalstack.agents.application.RepositoryVerificationS import com.jorisjonkers.personalstack.agents.application.chat.ChatAnswerStreamService import com.jorisjonkers.personalstack.agents.application.maintenance.RunnerMaintenanceService import com.jorisjonkers.personalstack.agents.application.query.ChatSessionQueryService -import com.jorisjonkers.personalstack.agents.application.query.GetConversationQueryService -import com.jorisjonkers.personalstack.agents.application.query.GetMessageQueryService import com.jorisjonkers.personalstack.agents.application.query.GetTurnHistoryQueryService import com.jorisjonkers.personalstack.agents.application.query.GetWorkspaceQueryService import com.jorisjonkers.personalstack.agents.application.query.ListWorkspacesQueryService @@ -36,14 +34,12 @@ import com.jorisjonkers.personalstack.agents.infrastructure.web.AgentSessionCont import com.jorisjonkers.personalstack.agents.infrastructure.web.AgentSetupController import com.jorisjonkers.personalstack.agents.infrastructure.web.AgentSetupExceptionHandler import com.jorisjonkers.personalstack.agents.infrastructure.web.ChatSessionController -import com.jorisjonkers.personalstack.agents.infrastructure.web.ConversationController import com.jorisjonkers.personalstack.agents.infrastructure.web.CredentialController import com.jorisjonkers.personalstack.agents.infrastructure.web.GitController import com.jorisjonkers.personalstack.agents.infrastructure.web.HealthController import com.jorisjonkers.personalstack.agents.infrastructure.web.InternalCredentialController import com.jorisjonkers.personalstack.agents.infrastructure.web.InternalGitHubTokenController import com.jorisjonkers.personalstack.agents.infrastructure.web.KubernetesExceptionHandler -import com.jorisjonkers.personalstack.agents.infrastructure.web.MessageController import com.jorisjonkers.personalstack.agents.infrastructure.web.ProjectController import com.jorisjonkers.personalstack.agents.infrastructure.web.RepositoryAccessDeniedExceptionHandler import com.jorisjonkers.personalstack.agents.infrastructure.web.RepositoryController @@ -81,13 +77,11 @@ import java.nio.file.Paths AgentSetupController::class, AgentSessionController::class, ChatSessionController::class, - ConversationController::class, CredentialController::class, GitController::class, HealthController::class, InternalCredentialController::class, InternalGitHubTokenController::class, - MessageController::class, ProjectController::class, RepositoryController::class, SessionStatusController::class, @@ -118,13 +112,11 @@ import java.nio.file.Paths AgentSetupController::class, AgentSessionController::class, ChatSessionController::class, - ConversationController::class, CredentialController::class, GitController::class, HealthController::class, InternalCredentialController::class, InternalGitHubTokenController::class, - MessageController::class, ProjectController::class, RepositoryController::class, SessionStatusController::class, @@ -256,12 +248,6 @@ class OpenApiSpecExportTest @Bean fun chatSessionQueryService(): ChatSessionQueryService = mockk(relaxed = true) - @Bean - fun getConversationQueryService(): GetConversationQueryService = mockk(relaxed = true) - - @Bean - fun getMessageQueryService(): GetMessageQueryService = mockk(relaxed = true) - @Bean fun getTurnHistoryQueryService(): GetTurnHistoryQueryService = mockk(relaxed = true) diff --git a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/AgentsApiContractIntegrationTest.kt b/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/AgentsApiContractIntegrationTest.kt index ce5251d..27c72f1 100644 --- a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/AgentsApiContractIntegrationTest.kt +++ b/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/AgentsApiContractIntegrationTest.kt @@ -45,57 +45,6 @@ class AgentsApiContractIntegrationTest .andExpect(jsonPath("$.paths").exists()) } - @Test - fun conversationCreationResponseMatchesExpectedSchema() { - val userId = UUID.randomUUID().toString() - - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "Contract Test Chat"))), - ).andExpect(status().isCreated) - .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.id").exists()) - .andExpect(jsonPath("$.userId").exists()) - .andExpect(jsonPath("$.title").value("Contract Test Chat")) - .andExpect(jsonPath("$.status").value("ACTIVE")) - .andExpect(jsonPath("$.createdAt").exists()) - .andExpect(jsonPath("$.updatedAt").exists()) - } - - @Test - fun messageResponseMatchesExpectedSchema() { - val userId = UUID.randomUUID().toString() - - val convResult = - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "Message Schema Test"))), - ).andExpect(status().isCreated) - .andReturn() - - val conversationId = objectMapper.readTree(convResult.response.contentAsString)["id"].asText() - - mockMvc - .perform( - post("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "Schema check"))), - ).andExpect(status().isCreated) - .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.id").exists()) - .andExpect(jsonPath("$.conversationId").exists()) - .andExpect(jsonPath("$.role").value("USER")) - .andExpect(jsonPath("$.content").value("Schema check")) - .andExpect(jsonPath("$.createdAt").exists()) - } - @Test fun healthEndpointResponseMatchesSchema() { mockMvc @@ -329,26 +278,4 @@ class AgentsApiContractIntegrationTest .andExpect(jsonPath("$.session.id").value(sessionId)) .andExpect(jsonPath("$.messages[0].body").value("hello world")) } - - @Test - fun conversationListResponseIsValidJSONArray() { - val userId = UUID.randomUUID().toString() - - // Create a conversation so the list is non-empty - mockMvc.perform( - post("/api/v1/conversations") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "List Test Chat"))), - ) - - mockMvc - .perform(get("/api/v1/conversations").header("X-User-Id", userId)) - .andExpect(status().isOk) - .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$").isArray) - .andExpect(jsonPath("$[0].id").exists()) - .andExpect(jsonPath("$[0].title").exists()) - .andExpect(jsonPath("$[0].status").exists()) - } } diff --git a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/ConversationFlowIntegrationTest.kt b/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/ConversationFlowIntegrationTest.kt deleted file mode 100644 index 4a1f752..0000000 --- a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/ConversationFlowIntegrationTest.kt +++ /dev/null @@ -1,179 +0,0 @@ -package com.jorisjonkers.personalstack.agents.flow - -import com.fasterxml.jackson.databind.ObjectMapper -import com.jorisjonkers.personalstack.agents.IntegrationTestBase -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.MediaType -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import org.springframework.test.web.servlet.setup.MockMvcBuilders -import org.springframework.web.context.WebApplicationContext -import java.util.UUID - -class ConversationFlowIntegrationTest - @Autowired - constructor( - private val webApplicationContext: WebApplicationContext, - ) : IntegrationTestBase { - private lateinit var mockMvc: MockMvc - private val objectMapper = ObjectMapper() - - @BeforeEach - fun setUp() { - mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build() - } - - @Test - fun createAndGetConversation() { - val userId = UUID.randomUUID().toString() - - val result = - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "Integration Chat"))), - ).andExpect(status().isCreated) - .andExpect(jsonPath("$.title").value("Integration Chat")) - .andExpect(jsonPath("$.status").value("ACTIVE")) - .andReturn() - - val id = objectMapper.readTree(result.response.contentAsString)["id"].asText() - - mockMvc - .perform(get("/api/v1/conversations/$id").header("X-User-Id", userId)) - .andExpect(status().isOk) - .andExpect(jsonPath("$.title").value("Integration Chat")) - } - - @Test - fun createConversationWithoutXUserIdReturns400() { - mockMvc - .perform( - post("/api/v1/conversations") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "No Auth"))), - ).andExpect(status().isBadRequest) - } - - @Test - fun listConversationsReturnsOnlyUserSConversations() { - val userId1 = UUID.randomUUID().toString() - val userId2 = UUID.randomUUID().toString() - - mockMvc.perform( - post("/api/v1/conversations") - .header("X-User-Id", userId1) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "User1 Chat"))), - ) - - mockMvc.perform( - post("/api/v1/conversations") - .header("X-User-Id", userId2) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "User2 Chat"))), - ) - - mockMvc - .perform(get("/api/v1/conversations").header("X-User-Id", userId1)) - .andExpect(status().isOk) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].title").value("User1 Chat")) - } - - @Test - fun archiveConversationReturns204() { - val userId = UUID.randomUUID().toString() - - val result = - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "To Archive"))), - ).andReturn() - - val id = objectMapper.readTree(result.response.contentAsString)["id"].asText() - - mockMvc - .perform(delete("/api/v1/conversations/$id").header("X-User-Id", userId)) - .andExpect(status().isNoContent) - } - - @Test - fun archiveNonExistentConversationReturns404() { - val userId = UUID.randomUUID().toString() - val nonExistentId = UUID.randomUUID() - - mockMvc - .perform(delete("/api/v1/conversations/$nonExistentId").header("X-User-Id", userId)) - .andExpect(status().isNotFound) - } - - @Test - fun getNonExistentConversationReturns404() { - val userId = UUID.randomUUID().toString() - val nonExistentId = UUID.randomUUID() - - mockMvc - .perform(get("/api/v1/conversations/$nonExistentId").header("X-User-Id", userId)) - .andExpect(status().isNotFound) - } - - @Test - fun createConversationWithBlankTitleReturns422() { - val userId = UUID.randomUUID().toString() - - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to ""))), - ).andExpect(status().isUnprocessableContent) - } - - @Test - fun sendAndRetrieveMessages() { - val userId = UUID.randomUUID().toString() - - val convResult = - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "Message Test"))), - ).andExpect(status().isCreated) - .andReturn() - - val conversationId = objectMapper.readTree(convResult.response.contentAsString)["id"].asText() - - mockMvc - .perform( - post("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "Hello there"))), - ).andExpect(status().isCreated) - .andExpect(jsonPath("$.content").value("Hello there")) - .andExpect(jsonPath("$.role").value("USER")) - - mockMvc - .perform( - get("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId), - ).andExpect(status().isOk) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].content").value("Hello there")) - } - } diff --git a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/MessageFlowIntegrationTest.kt b/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/MessageFlowIntegrationTest.kt deleted file mode 100644 index a51156d..0000000 --- a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/flow/MessageFlowIntegrationTest.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.jorisjonkers.personalstack.agents.flow - -import com.fasterxml.jackson.databind.ObjectMapper -import com.jorisjonkers.personalstack.agents.IntegrationTestBase -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.MediaType -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import org.springframework.test.web.servlet.setup.MockMvcBuilders -import org.springframework.web.context.WebApplicationContext -import java.util.UUID - -class MessageFlowIntegrationTest - @Autowired - constructor( - private val webApplicationContext: WebApplicationContext, - ) : IntegrationTestBase { - private lateinit var mockMvc: MockMvc - private val objectMapper = ObjectMapper() - - @BeforeEach - fun setUp() { - mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build() - } - - @Test - fun sendMessageAndRetrieveByConversation() { - val userId = UUID.randomUUID().toString() - val conversationId = createConversation(userId, "Msg Flow Test") - - mockMvc - .perform( - post("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "First message"))), - ).andExpect(status().isCreated) - .andExpect(jsonPath("$.content").value("First message")) - - mockMvc - .perform( - get("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId), - ).andExpect(status().isOk) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].content").value("First message")) - } - - @Test - fun sendMessageWithoutXUserIdReturns400() { - val userId = UUID.randomUUID().toString() - val conversationId = createConversation(userId, "No Auth Msg") - - mockMvc - .perform( - post("/api/v1/conversations/$conversationId/messages") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "Should fail"))), - ).andExpect(status().isBadRequest) - } - - @Test - fun sendMessageWithBlankContentReturns422() { - val userId = UUID.randomUUID().toString() - val conversationId = createConversation(userId, "Blank Msg") - - mockMvc - .perform( - post("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to ""))), - ).andExpect(status().isUnprocessableContent) - } - - @Test - fun messagesAreOrderedByCreationTime() { - val userId = UUID.randomUUID().toString() - val conversationId = createConversation(userId, "Ordered Msgs") - - mockMvc.perform( - post("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "First"))), - ) - - mockMvc.perform( - post("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "Second"))), - ) - - mockMvc - .perform( - get("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId), - ).andExpect(status().isOk) - .andExpect(jsonPath("$.length()").value(2)) - .andExpect(jsonPath("$[0].content").value("First")) - .andExpect(jsonPath("$[1].content").value("Second")) - } - - @Test - fun messagesForDifferentConversationsAreIsolated() { - val userId = UUID.randomUUID().toString() - val conv1 = createConversation(userId, "Conv1") - val conv2 = createConversation(userId, "Conv2") - - mockMvc.perform( - post("/api/v1/conversations/$conv1/messages") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "Conv1 message"))), - ) - - mockMvc.perform( - post("/api/v1/conversations/$conv2/messages") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "Conv2 message"))), - ) - - mockMvc - .perform( - get("/api/v1/conversations/$conv1/messages") - .header("X-User-Id", userId), - ).andExpect(status().isOk) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].content").value("Conv1 message")) - - mockMvc - .perform( - get("/api/v1/conversations/$conv2/messages") - .header("X-User-Id", userId), - ).andExpect(status().isOk) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].content").value("Conv2 message")) - } - - private fun createConversation( - userId: String, - title: String, - ): String { - val result = - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to title))), - ).andExpect(status().isCreated) - .andReturn() - - return objectMapper.readTree(result.response.contentAsString)["id"].asText() - } - } diff --git a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/persistence/JooqConversationRepositoryIntegrationTest.kt b/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/persistence/JooqConversationRepositoryIntegrationTest.kt deleted file mode 100644 index fa04b3d..0000000 --- a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/persistence/JooqConversationRepositoryIntegrationTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.jorisjonkers.personalstack.agents.persistence - -import com.jorisjonkers.personalstack.agents.IntegrationTestBase -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import java.time.Instant -import java.util.UUID - -class JooqConversationRepositoryIntegrationTest - @Autowired - constructor( - private val conversationRepository: ConversationRepository, - ) : IntegrationTestBase { - @Test - fun saveAndFindByIdReturnsTheSavedConversation() { - val conversation = buildConversation(title = "Test Conversation") - conversationRepository.save(conversation) - - val found = (conversationRepository.findById(conversation.id)).required() - - assertThat(found.title).isEqualTo("Test Conversation") - assertThat(found.status).isEqualTo(ConversationStatus.ACTIVE) - } - - @Test - fun findbyidReturnsNullWhenConversationDoesNotExist() { - val result = conversationRepository.findById(ConversationId(UUID.randomUUID())) - - assertThat(result).isNull() - } - - @Test - fun findbyuseridReturnsAllConversationsForTheUser() { - val userId = UUID.randomUUID() - val first = buildConversation(userId = userId, title = "First") - val second = buildConversation(userId = userId, title = "Second") - val other = buildConversation(title = "Other User") - conversationRepository.save(first) - conversationRepository.save(second) - conversationRepository.save(other) - - val results = conversationRepository.findByUserId(userId) - - assertThat(results).hasSize(2) - assertThat(results.map { it.title }).containsExactlyInAnyOrder("First", "Second") - } - - @Test - fun saveUpdatesExistingConversationOnConflict() { - val conversation = buildConversation(title = "Original") - conversationRepository.save(conversation) - - val updated = - conversation.copy( - title = "Updated", - status = ConversationStatus.ARCHIVED, - updatedAt = Instant.now(), - ) - conversationRepository.save(updated) - - val found = (conversationRepository.findById(conversation.id)).required() - assertThat(found.title).isEqualTo("Updated") - assertThat(found.status).isEqualTo(ConversationStatus.ARCHIVED) - } - - private fun buildConversation( - userId: UUID = UUID.randomUUID(), - title: String = "Test", - ): Conversation { - val now = Instant.now() - return Conversation( - id = ConversationId(UUID.randomUUID()), - userId = userId, - title = title, - status = ConversationStatus.ACTIVE, - createdAt = now, - updatedAt = now, - ) - } - } diff --git a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/persistence/JooqMessageRepositoryIntegrationTest.kt b/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/persistence/JooqMessageRepositoryIntegrationTest.kt deleted file mode 100644 index 2b0be65..0000000 --- a/api/src/integrationTest/kotlin/com/jorisjonkers/personalstack/agents/persistence/JooqMessageRepositoryIntegrationTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.jorisjonkers.personalstack.agents.persistence - -import com.jorisjonkers.personalstack.agents.IntegrationTestBase -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.agents.domain.model.Message -import com.jorisjonkers.personalstack.agents.domain.model.MessageId -import com.jorisjonkers.personalstack.agents.domain.model.MessageRole -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import com.jorisjonkers.personalstack.agents.domain.port.MessageRepository -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import java.time.Instant -import java.util.UUID - -class JooqMessageRepositoryIntegrationTest - @Autowired - constructor( - private val conversationRepository: ConversationRepository, - private val messageRepository: MessageRepository, - ) : IntegrationTestBase { - @Test - fun saveAndFindByConversationIdReturnsSavedMessages() { - val conversation = buildConversation() - conversationRepository.save(conversation) - - val message = buildMessage(conversationId = conversation.id, content = "Hello", role = MessageRole.USER) - messageRepository.save(message) - - val found = messageRepository.findByConversationId(conversation.id) - - assertThat(found).hasSize(1) - assertThat(found[0].content).isEqualTo("Hello") - assertThat(found[0].role).isEqualTo(MessageRole.USER) - } - - @Test - fun findbyconversationidReturnsEmptyListWhenNoMessagesExist() { - val conversation = buildConversation() - conversationRepository.save(conversation) - - val found = messageRepository.findByConversationId(conversation.id) - - assertThat(found).isEmpty() - } - - @Test - fun findbyconversationidReturnsMessagesInAscendingCreationOrder() { - val conversation = buildConversation() - conversationRepository.save(conversation) - - val first = buildMessage(conversationId = conversation.id, content = "First", role = MessageRole.USER) - val second = - buildMessage( - conversationId = conversation.id, - content = "Second", - role = MessageRole.ASSISTANT, - createdAt = Instant.now().plusSeconds(1), - ) - messageRepository.save(first) - messageRepository.save(second) - - val found = messageRepository.findByConversationId(conversation.id) - - assertThat(found).hasSize(2) - assertThat(found[0].content).isEqualTo("First") - assertThat(found[1].content).isEqualTo("Second") - } - - @Test - fun findbyconversationidDoesNotReturnMessagesForOtherConversations() { - val conversation1 = buildConversation() - val conversation2 = buildConversation() - conversationRepository.save(conversation1) - conversationRepository.save(conversation2) - - messageRepository.save(buildMessage(conversationId = conversation1.id, content = "Conv1 message")) - messageRepository.save(buildMessage(conversationId = conversation2.id, content = "Conv2 message")) - - val found = messageRepository.findByConversationId(conversation1.id) - - assertThat(found).hasSize(1) - assertThat(found[0].content).isEqualTo("Conv1 message") - } - - private fun buildConversation(): Conversation { - val now = Instant.now() - return Conversation( - id = ConversationId(UUID.randomUUID()), - userId = UUID.randomUUID(), - title = "Test Conversation", - status = ConversationStatus.ACTIVE, - createdAt = now, - updatedAt = now, - ) - } - - private fun buildMessage( - conversationId: ConversationId, - content: String = "Test message", - role: MessageRole = MessageRole.USER, - createdAt: Instant = Instant.now(), - ): Message = - Message( - id = MessageId(UUID.randomUUID()), - conversationId = conversationId, - role = role, - content = content, - createdAt = createdAt, - ) - } diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/ArchiveConversationCommand.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/ArchiveConversationCommand.kt deleted file mode 100644 index 3853d19..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/ArchiveConversationCommand.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.command - -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.common.command.Command - -data class ArchiveConversationCommand( - val conversationId: ConversationId, - val userId: String, -) : Command diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/ArchiveConversationCommandHandler.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/ArchiveConversationCommandHandler.kt deleted file mode 100644 index d004a79..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/ArchiveConversationCommandHandler.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.command - -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import com.jorisjonkers.personalstack.common.command.CommandHandler -import com.jorisjonkers.personalstack.common.exception.DomainException -import com.jorisjonkers.personalstack.common.exception.NotFoundException -import org.springframework.stereotype.Service -import java.time.Instant -import java.util.UUID - -@Service -class ArchiveConversationCommandHandler( - private val conversationRepository: ConversationRepository, -) : CommandHandler { - override fun handle(command: ArchiveConversationCommand) { - val conversation = - conversationRepository.findById(command.conversationId) - ?: throw NotFoundException("Conversation", command.conversationId.value.toString()) - - val requestingUserId = parseUserId(command.userId) - requireOwner(command, conversation.userId, requestingUserId) - - val archived = - conversation.copy( - status = ConversationStatus.ARCHIVED, - updatedAt = Instant.now(), - ) - conversationRepository.save(archived) - } - - private fun parseUserId(userId: String): UUID = - runCatching { UUID.fromString(userId) }.getOrNull() - ?: throw DomainException("Invalid userId format: $userId", "INVALID_USER_ID") - - private fun requireOwner( - command: ArchiveConversationCommand, - conversationUserId: UUID, - requestingUserId: UUID, - ) { - if (conversationUserId == requestingUserId) return - throw DomainException( - "User ${command.userId} does not own conversation ${command.conversationId.value}", - "FORBIDDEN", - ) - } -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/SendMessageCommand.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/SendMessageCommand.kt deleted file mode 100644 index b6a961d..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/SendMessageCommand.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.command - -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.MessageId -import com.jorisjonkers.personalstack.agents.domain.model.MessageRole -import com.jorisjonkers.personalstack.common.command.Command - -data class SendMessageCommand( - val messageId: MessageId, - val conversationId: ConversationId, - val userId: String, - val content: String, - val role: MessageRole, -) : Command diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/SendMessageCommandHandler.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/SendMessageCommandHandler.kt deleted file mode 100644 index 142f80d..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/SendMessageCommandHandler.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.command - -import com.jorisjonkers.personalstack.agents.domain.model.Message -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import com.jorisjonkers.personalstack.agents.domain.port.MessageRepository -import com.jorisjonkers.personalstack.common.command.CommandHandler -import com.jorisjonkers.personalstack.common.exception.NotFoundException -import org.springframework.stereotype.Service -import java.time.Instant - -@Service -class SendMessageCommandHandler( - private val conversationRepository: ConversationRepository, - private val messageRepository: MessageRepository, -) : CommandHandler { - override fun handle(command: SendMessageCommand) { - require(command.content.isNotBlank()) { "Message content must not be blank" } - - conversationRepository.findById(command.conversationId) - ?: throw NotFoundException("Conversation", command.conversationId.value.toString()) - - val message = - Message( - id = command.messageId, - conversationId = command.conversationId, - role = command.role, - content = command.content, - createdAt = Instant.now(), - ) - messageRepository.save(message) - } -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/StartConversationCommand.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/StartConversationCommand.kt deleted file mode 100644 index ecf6c71..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/StartConversationCommand.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.command - -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.common.command.Command -import java.util.UUID - -data class StartConversationCommand( - val conversationId: ConversationId, - val userId: UUID, - val title: String, -) : Command diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/StartConversationCommandHandler.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/StartConversationCommandHandler.kt deleted file mode 100644 index b7335e5..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/command/StartConversationCommandHandler.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.command - -import com.jorisjonkers.personalstack.agents.domain.event.ConversationStartedEvent -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import com.jorisjonkers.personalstack.common.command.CommandHandler -import org.springframework.context.ApplicationEventPublisher -import org.springframework.stereotype.Service -import java.time.Instant - -@Service -class StartConversationCommandHandler( - private val conversationRepository: ConversationRepository, - private val eventPublisher: ApplicationEventPublisher, -) : CommandHandler { - override fun handle(command: StartConversationCommand) { - require(command.title.isNotBlank()) { "Title must not be blank" } - - val now = Instant.now() - val conversation = - Conversation( - id = command.conversationId, - userId = command.userId, - title = command.title.trim(), - status = ConversationStatus.ACTIVE, - createdAt = now, - updatedAt = now, - ) - conversationRepository.save(conversation) - - eventPublisher.publishEvent( - ConversationStartedEvent( - conversationId = conversation.id, - userId = conversation.userId, - ), - ) - } -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetConversationQueryService.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetConversationQueryService.kt deleted file mode 100644 index 15cf74e..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetConversationQueryService.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.query - -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import com.jorisjonkers.personalstack.common.exception.NotFoundException -import org.springframework.stereotype.Service -import java.util.UUID - -@Service -class GetConversationQueryService( - private val conversationRepository: ConversationRepository, -) { - fun findById(id: ConversationId): Conversation = - conversationRepository.findById(id) - ?: throw NotFoundException("Conversation", id.value.toString()) - - fun findByUserId(userId: UUID): List = conversationRepository.findByUserId(userId) -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetMessageQueryService.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetMessageQueryService.kt deleted file mode 100644 index a65ca9a..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetMessageQueryService.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.query - -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.Message -import com.jorisjonkers.personalstack.agents.domain.port.MessageRepository -import org.springframework.stereotype.Service - -@Service -class GetMessageQueryService( - private val messageRepository: MessageRepository, -) { - fun findByConversationId(conversationId: ConversationId): List = - messageRepository.findByConversationId(conversationId) -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/event/ConversationStartedEvent.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/event/ConversationStartedEvent.kt deleted file mode 100644 index db3b3f9..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/event/ConversationStartedEvent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.jorisjonkers.personalstack.agents.domain.event - -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.common.event.DomainEvent -import java.time.Instant -import java.util.UUID - -data class ConversationStartedEvent( - val conversationId: ConversationId, - val userId: UUID, - override val occurredAt: Instant = Instant.now(), -) : DomainEvent diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/Conversation.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/Conversation.kt deleted file mode 100644 index 539f1ff..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/Conversation.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.jorisjonkers.personalstack.agents.domain.model - -import java.time.Instant -import java.util.UUID - -data class Conversation( - val id: ConversationId, - val userId: UUID, - val title: String, - val status: ConversationStatus, - val createdAt: Instant, - val updatedAt: Instant, -) diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/ConversationId.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/ConversationId.kt deleted file mode 100644 index 38be19a..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/ConversationId.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.jorisjonkers.personalstack.agents.domain.model - -import java.util.UUID - -@JvmInline -value class ConversationId( - val value: UUID, -) diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/ConversationStatus.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/ConversationStatus.kt deleted file mode 100644 index 9f6b4b4..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/ConversationStatus.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.jorisjonkers.personalstack.agents.domain.model - -enum class ConversationStatus { - ACTIVE, - ARCHIVED, - DELETED, -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/Message.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/Message.kt deleted file mode 100644 index 2de839e..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/Message.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.jorisjonkers.personalstack.agents.domain.model - -import java.time.Instant - -data class Message( - val id: MessageId, - val conversationId: ConversationId, - val role: MessageRole, - val content: String, - val createdAt: Instant, -) diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/MessageId.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/MessageId.kt deleted file mode 100644 index a5edd0e..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/MessageId.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.jorisjonkers.personalstack.agents.domain.model - -import java.util.UUID - -@JvmInline -value class MessageId( - val value: UUID, -) diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/MessageRole.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/MessageRole.kt deleted file mode 100644 index 8e1c989..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/model/MessageRole.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.jorisjonkers.personalstack.agents.domain.model - -enum class MessageRole { - USER, - ASSISTANT, -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/port/ConversationRepository.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/port/ConversationRepository.kt deleted file mode 100644 index ce387af..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/port/ConversationRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.jorisjonkers.personalstack.agents.domain.port - -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import java.util.UUID - -interface ConversationRepository { - fun findById(id: ConversationId): Conversation? - - fun findByUserId(userId: UUID): List - - fun save(conversation: Conversation): Conversation -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/port/MessageRepository.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/port/MessageRepository.kt deleted file mode 100644 index a741d1f..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/domain/port/MessageRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.jorisjonkers.personalstack.agents.domain.port - -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.Message - -interface MessageRepository { - fun save(message: Message): Message - - fun findByConversationId(id: ConversationId): List -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/persistence/JooqConversationRepository.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/persistence/JooqConversationRepository.kt deleted file mode 100644 index 51f8a9a..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/persistence/JooqConversationRepository.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.persistence - -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import com.jorisjonkers.personalstack.agents.jooq.tables.Conversation.CONVERSATION -import org.jooq.DSLContext -import org.jooq.Record -import org.springframework.stereotype.Repository -import java.time.ZoneOffset -import java.util.UUID - -@Repository -class JooqConversationRepository( - private val dsl: DSLContext, -) : ConversationRepository { - override fun findById(id: ConversationId): Conversation? = - dsl - .selectFrom(CONVERSATION) - .where(CONVERSATION.ID.eq(id.value)) - .fetchOne() - ?.toConversation() - - override fun findByUserId(userId: UUID): List = - dsl - .selectFrom(CONVERSATION) - .where(CONVERSATION.USER_ID.eq(userId)) - .fetch() - .map { it.toConversation() } - - override fun save(conversation: Conversation): Conversation { - val createdAt = conversation.createdAt.atOffset(ZoneOffset.UTC).toLocalDateTime() - val updatedAt = conversation.updatedAt.atOffset(ZoneOffset.UTC).toLocalDateTime() - dsl - .insertInto(CONVERSATION) - .set(CONVERSATION.ID, conversation.id.value) - .set(CONVERSATION.USER_ID, conversation.userId) - .set(CONVERSATION.TITLE, conversation.title) - .set(CONVERSATION.STATUS, conversation.status.name) - .set(CONVERSATION.CREATED_AT, createdAt) - .set(CONVERSATION.UPDATED_AT, updatedAt) - .onConflict(CONVERSATION.ID) - .doUpdate() - .set(CONVERSATION.TITLE, conversation.title) - .set(CONVERSATION.STATUS, conversation.status.name) - .set(CONVERSATION.UPDATED_AT, updatedAt) - .execute() - return conversation - } - - private fun Record.toConversation(): Conversation = - Conversation( - id = ConversationId(this[CONVERSATION.ID] as UUID), - userId = this[CONVERSATION.USER_ID] as UUID, - title = this[CONVERSATION.TITLE] as String, - status = ConversationStatus.valueOf(this[CONVERSATION.STATUS] as String), - createdAt = - (this[CONVERSATION.CREATED_AT] as java.time.LocalDateTime) - .toInstant(ZoneOffset.UTC), - updatedAt = - (this[CONVERSATION.UPDATED_AT] as java.time.LocalDateTime) - .toInstant(ZoneOffset.UTC), - ) -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/persistence/JooqMessageRepository.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/persistence/JooqMessageRepository.kt deleted file mode 100644 index 09a083a..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/persistence/JooqMessageRepository.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.persistence - -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.Message -import com.jorisjonkers.personalstack.agents.domain.model.MessageId -import com.jorisjonkers.personalstack.agents.domain.model.MessageRole -import com.jorisjonkers.personalstack.agents.domain.port.MessageRepository -import com.jorisjonkers.personalstack.agents.jooq.tables.Message.MESSAGE -import org.jooq.DSLContext -import org.jooq.Record -import org.springframework.stereotype.Repository -import java.time.ZoneOffset -import java.util.UUID - -@Repository -class JooqMessageRepository( - private val dsl: DSLContext, -) : MessageRepository { - override fun save(message: Message): Message { - val createdAt = message.createdAt.atOffset(ZoneOffset.UTC).toLocalDateTime() - dsl - .insertInto(MESSAGE) - .set(MESSAGE.ID, message.id.value) - .set(MESSAGE.CONVERSATION_ID, message.conversationId.value) - .set(MESSAGE.ROLE, message.role.name) - .set(MESSAGE.CONTENT, message.content) - .set(MESSAGE.CREATED_AT, createdAt) - .execute() - return message - } - - override fun findByConversationId(id: ConversationId): List = - dsl - .selectFrom(MESSAGE) - .where(MESSAGE.CONVERSATION_ID.eq(id.value)) - .orderBy(MESSAGE.CREATED_AT.asc()) - .fetch() - .map { it.toMessage() } - - private fun Record.toMessage(): Message = - Message( - id = MessageId(this[MESSAGE.ID] as UUID), - conversationId = ConversationId(this[MESSAGE.CONVERSATION_ID] as UUID), - role = MessageRole.valueOf(this[MESSAGE.ROLE] as String), - content = this[MESSAGE.CONTENT] as String, - createdAt = - (this[MESSAGE.CREATED_AT] as java.time.LocalDateTime) - .toInstant(ZoneOffset.UTC), - ) -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/ConversationController.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/ConversationController.kt deleted file mode 100644 index 880f070..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/ConversationController.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.web - -import com.jorisjonkers.personalstack.agents.application.command.ArchiveConversationCommand -import com.jorisjonkers.personalstack.agents.application.command.StartConversationCommand -import com.jorisjonkers.personalstack.agents.application.query.GetConversationQueryService -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.infrastructure.web.dto.ConversationResponse -import com.jorisjonkers.personalstack.agents.infrastructure.web.dto.CreateConversationRequest -import com.jorisjonkers.personalstack.common.command.CommandBus -import jakarta.validation.Valid -import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestHeader -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController -import java.util.UUID - -@RestController -@RequestMapping("/api/v1/conversations") -class ConversationController( - private val commandBus: CommandBus, - private val getConversationQueryService: GetConversationQueryService, -) { - @Deprecated( - "Replaced by the Conversation surface backed by the renamed model (#68). Removed once nothing calls it.", - ) - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - fun create( - @RequestHeader("X-User-Id") userId: String, - @Valid @RequestBody request: CreateConversationRequest, - ): ConversationResponse { - val userUuid = UUID.fromString(userId) - val conversationId = ConversationId(UUID.randomUUID()) - commandBus.dispatch( - StartConversationCommand( - conversationId = conversationId, - userId = userUuid, - title = request.title, - ), - ) - val created = getConversationQueryService.findById(conversationId) - return ConversationResponse.from(created) - } - - @Deprecated( - "Replaced by the Conversation surface backed by the renamed model (#68). Removed once nothing calls it.", - ) - @GetMapping("/{id}") - fun getById( - @PathVariable id: UUID, - ): ConversationResponse { - val conversation = getConversationQueryService.findById(ConversationId(id)) - return ConversationResponse.from(conversation) - } - - @Deprecated( - "Replaced by the Conversation surface backed by the renamed model (#68). Removed once nothing calls it.", - ) - @DeleteMapping("/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) - fun archive( - @PathVariable id: UUID, - @RequestHeader("X-User-Id") userId: String, - ) { - commandBus.dispatch(ArchiveConversationCommand(conversationId = ConversationId(id), userId = userId)) - } - - @Deprecated( - "Replaced by the Conversation surface backed by the renamed model (#68). Removed once nothing calls it.", - ) - @GetMapping - fun listByUser( - @RequestHeader("X-User-Id") userId: String, - ): List { - val userUuid = UUID.fromString(userId) - return getConversationQueryService - .findByUserId(userUuid) - .map { ConversationResponse.from(it) } - } -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/MessageController.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/MessageController.kt deleted file mode 100644 index de1c4cb..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/MessageController.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.web - -import com.jorisjonkers.personalstack.agents.application.command.SendMessageCommand -import com.jorisjonkers.personalstack.agents.application.query.GetMessageQueryService -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.MessageId -import com.jorisjonkers.personalstack.agents.domain.model.MessageRole -import com.jorisjonkers.personalstack.agents.infrastructure.web.dto.MessageResponse -import com.jorisjonkers.personalstack.agents.infrastructure.web.dto.SendMessageRequest -import com.jorisjonkers.personalstack.common.command.CommandBus -import jakarta.validation.Valid -import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestHeader -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController -import java.util.UUID - -@RestController -@RequestMapping("/api/v1/conversations/{conversationId}/messages") -class MessageController( - private val commandBus: CommandBus, - private val getMessageQueryService: GetMessageQueryService, -) { - @Deprecated( - "Replaced by the Conversation surface backed by the renamed model (#68). Removed once nothing calls it.", - ) - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - fun send( - @PathVariable conversationId: UUID, - @RequestHeader("X-User-Id") userId: String, - @Valid @RequestBody request: SendMessageRequest, - ): MessageResponse { - val convId = ConversationId(conversationId) - val messageId = MessageId(UUID.randomUUID()) - commandBus.dispatch( - SendMessageCommand( - messageId = messageId, - conversationId = convId, - userId = userId, - content = request.content, - role = MessageRole.USER, - ), - ) - val saved = - getMessageQueryService - .findByConversationId(convId) - .firstOrNull { it.id == messageId } - ?: error("Message not found after saving") - return MessageResponse.from(saved) - } - - @Deprecated( - "Replaced by the Conversation surface backed by the renamed model (#68). Removed once nothing calls it.", - ) - @GetMapping - fun list( - @PathVariable conversationId: UUID, - ): List { - val convId = ConversationId(conversationId) - return getMessageQueryService - .findByConversationId(convId) - .map { MessageResponse.from(it) } - } -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/ConversationResponse.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/ConversationResponse.kt deleted file mode 100644 index 46d7fa3..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/ConversationResponse.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.web.dto - -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import java.time.Instant -import java.util.UUID - -data class ConversationResponse( - val id: UUID, - val userId: UUID, - val title: String, - val status: String, - val createdAt: Instant, - val updatedAt: Instant, -) { - companion object { - fun from(conversation: Conversation): ConversationResponse = - ConversationResponse( - id = conversation.id.value, - userId = conversation.userId, - title = conversation.title, - status = conversation.status.name, - createdAt = conversation.createdAt, - updatedAt = conversation.updatedAt, - ) - } -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/CreateConversationRequest.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/CreateConversationRequest.kt deleted file mode 100644 index 42fb4cd..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/CreateConversationRequest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.web.dto - -import jakarta.validation.constraints.NotBlank -import jakarta.validation.constraints.Size - -data class CreateConversationRequest( - @field:NotBlank(message = "Title is required") - @field:Size(max = 200, message = "Title must not exceed 200 characters") - val title: String, -) diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/MessageResponse.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/MessageResponse.kt deleted file mode 100644 index f9700c0..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/MessageResponse.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.web.dto - -import com.jorisjonkers.personalstack.agents.domain.model.Message -import java.time.Instant -import java.util.UUID - -data class MessageResponse( - val id: UUID, - val conversationId: UUID, - val role: String, - val content: String, - val createdAt: Instant, -) { - companion object { - fun from(message: Message): MessageResponse = - MessageResponse( - id = message.id.value, - conversationId = message.conversationId.value, - role = message.role.name, - content = message.content, - createdAt = message.createdAt, - ) - } -} diff --git a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/SendMessageRequest.kt b/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/SendMessageRequest.kt deleted file mode 100644 index afba81f..0000000 --- a/api/src/main/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/dto/SendMessageRequest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.web.dto - -import jakarta.validation.constraints.NotBlank -import jakarta.validation.constraints.Size - -data class SendMessageRequest( - @field:NotBlank(message = "Content is required") - @field:Size(max = 10000, message = "Content must not exceed 10000 characters") - val content: String, -) diff --git a/api/src/main/resources/db/migration/V26__drop_legacy_conversation_and_message.sql b/api/src/main/resources/db/migration/V26__drop_legacy_conversation_and_message.sql new file mode 100644 index 0000000..a9caecc --- /dev/null +++ b/api/src/main/resources/db/migration/V26__drop_legacy_conversation_and_message.sql @@ -0,0 +1,11 @@ +-- The `conversation` / `message` tables backed the original assistant +-- surface (V1/V2). No frontend in this workspace calls +-- /api/v1/conversations any more -- agents-ui talks to the newer +-- /api/v1/chat-sessions surface exclusively. That surface takes over +-- the `conversations` name in the next migration, so the old rows are +-- dropped outright rather than migrated: they are unread and nothing +-- reads this table today. +-- +-- `message` first: it carries the FK to `conversation`. +DROP TABLE message; +DROP TABLE conversation; diff --git a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/command/ArchiveConversationCommandHandlerTest.kt b/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/command/ArchiveConversationCommandHandlerTest.kt deleted file mode 100644 index 39378e6..0000000 --- a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/command/ArchiveConversationCommandHandlerTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.command - -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import com.jorisjonkers.personalstack.common.exception.DomainException -import com.jorisjonkers.personalstack.common.exception.NotFoundException -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.jupiter.api.Test -import java.time.Instant -import java.util.UUID - -class ArchiveConversationCommandHandlerTest { - private val conversationRepository = mockk() - private val handler = ArchiveConversationCommandHandler(conversationRepository) - - @Test - fun `handle archives conversation when owner requests it`() { - val userId = UUID.randomUUID() - val conversationId = ConversationId(UUID.randomUUID()) - val conversation = buildConversation(id = conversationId, userId = userId) - val slot = slot() - - every { conversationRepository.findById(conversationId) } returns conversation - every { conversationRepository.save(capture(slot)) } answers { slot.captured } - - handler.handle(ArchiveConversationCommand(conversationId = conversationId, userId = userId.toString())) - - assertThat(slot.captured.status).isEqualTo(ConversationStatus.ARCHIVED) - } - - @Test - fun `handle throws NotFoundException when conversation does not exist`() { - val conversationId = ConversationId(UUID.randomUUID()) - - every { conversationRepository.findById(conversationId) } returns null - - assertThatThrownBy { - val cmd = ArchiveConversationCommand(conversationId = conversationId, userId = UUID.randomUUID().toString()) - handler.handle(cmd) - }.isInstanceOf(NotFoundException::class.java) - } - - @Test - fun `handle throws DomainException when user does not own the conversation`() { - val ownerId = UUID.randomUUID() - val conversationId = ConversationId(UUID.randomUUID()) - val conversation = buildConversation(id = conversationId, userId = ownerId) - val otherUserId = UUID.randomUUID().toString() - - every { conversationRepository.findById(conversationId) } returns conversation - - assertThatThrownBy { - handler.handle(ArchiveConversationCommand(conversationId = conversationId, userId = otherUserId)) - }.isInstanceOf(DomainException::class.java) - .hasMessageContaining("does not own") - } - - @Test - fun `handle throws DomainException when userId is not a valid UUID`() { - val conversationId = ConversationId(UUID.randomUUID()) - val conversation = buildConversation(id = conversationId) - - every { conversationRepository.findById(conversationId) } returns conversation - - assertThatThrownBy { - handler.handle(ArchiveConversationCommand(conversationId = conversationId, userId = "not-a-uuid")) - }.isInstanceOf(DomainException::class.java) - } - - private fun buildConversation( - id: ConversationId = ConversationId(UUID.randomUUID()), - userId: UUID = UUID.randomUUID(), - ): Conversation { - val now = Instant.now() - return Conversation( - id = id, - userId = userId, - title = "Test", - status = ConversationStatus.ACTIVE, - createdAt = now, - updatedAt = now, - ) - } -} diff --git a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/command/SendMessageCommandHandlerTest.kt b/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/command/SendMessageCommandHandlerTest.kt deleted file mode 100644 index 629f265..0000000 --- a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/command/SendMessageCommandHandlerTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.command - -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.agents.domain.model.Message -import com.jorisjonkers.personalstack.agents.domain.model.MessageId -import com.jorisjonkers.personalstack.agents.domain.model.MessageRole -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import com.jorisjonkers.personalstack.agents.domain.port.MessageRepository -import com.jorisjonkers.personalstack.common.exception.NotFoundException -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.jupiter.api.Test -import java.time.Instant -import java.util.UUID - -class SendMessageCommandHandlerTest { - private val conversationRepository = mockk() - private val messageRepository = mockk() - private val handler = SendMessageCommandHandler(conversationRepository, messageRepository) - - @Test - fun `handle saves a new message when conversation exists`() { - val conversationId = ConversationId(UUID.randomUUID()) - val messageId = MessageId(UUID.randomUUID()) - val command = - SendMessageCommand( - messageId = messageId, - conversationId = conversationId, - userId = UUID.randomUUID().toString(), - content = "Hello world", - role = MessageRole.USER, - ) - val slot = slot() - - every { conversationRepository.findById(conversationId) } returns buildConversation(conversationId) - every { messageRepository.save(capture(slot)) } answers { slot.captured } - - handler.handle(command) - - assertThat(slot.captured.id).isEqualTo(messageId) - assertThat(slot.captured.conversationId).isEqualTo(conversationId) - assertThat(slot.captured.content).isEqualTo("Hello world") - assertThat(slot.captured.role).isEqualTo(MessageRole.USER) - } - - @Test - fun `handle throws NotFoundException when conversation does not exist`() { - val conversationId = ConversationId(UUID.randomUUID()) - val command = - SendMessageCommand( - messageId = MessageId(UUID.randomUUID()), - conversationId = conversationId, - userId = UUID.randomUUID().toString(), - content = "Hello", - role = MessageRole.USER, - ) - - every { conversationRepository.findById(conversationId) } returns null - - assertThatThrownBy { handler.handle(command) } - .isInstanceOf(NotFoundException::class.java) - } - - @Test - fun `handle throws when content is blank`() { - val command = - SendMessageCommand( - messageId = MessageId(UUID.randomUUID()), - conversationId = ConversationId(UUID.randomUUID()), - userId = UUID.randomUUID().toString(), - content = " ", - role = MessageRole.USER, - ) - - assertThatThrownBy { handler.handle(command) } - .isInstanceOf(IllegalArgumentException::class.java) - - verify(exactly = 0) { messageRepository.save(any()) } - verify(exactly = 0) { conversationRepository.findById(any()) } - } - - private fun buildConversation(id: ConversationId): Conversation { - val now = Instant.now() - return Conversation( - id = id, - userId = UUID.randomUUID(), - title = "Test", - status = ConversationStatus.ACTIVE, - createdAt = now, - updatedAt = now, - ) - } -} diff --git a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/command/StartConversationCommandHandlerTest.kt b/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/command/StartConversationCommandHandlerTest.kt deleted file mode 100644 index 46db6a7..0000000 --- a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/command/StartConversationCommandHandlerTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.command - -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.jupiter.api.Test -import org.springframework.context.ApplicationEventPublisher -import java.util.UUID - -class StartConversationCommandHandlerTest { - private val conversationRepository = mockk() - private val eventPublisher = mockk(relaxed = true) - private val handler = StartConversationCommandHandler(conversationRepository, eventPublisher) - - @Test - fun `handle creates and saves a new conversation`() { - val userId = UUID.randomUUID() - val conversationId = ConversationId(UUID.randomUUID()) - val command = StartConversationCommand(conversationId = conversationId, userId = userId, title = "My Chat") - val slot = slot() - - every { conversationRepository.save(capture(slot)) } answers { slot.captured } - - handler.handle(command) - - assertThat(slot.captured.id).isEqualTo(conversationId) - assertThat(slot.captured.userId).isEqualTo(userId) - assertThat(slot.captured.title).isEqualTo("My Chat") - assertThat(slot.captured.status).isEqualTo(ConversationStatus.ACTIVE) - verify { eventPublisher.publishEvent(any()) } - } - - @Test - fun `handle trims whitespace from title`() { - val conversationId = ConversationId(UUID.randomUUID()) - val command = - StartConversationCommand( - conversationId = conversationId, - userId = UUID.randomUUID(), - title = " Padded Title ", - ) - val slot = slot() - - every { conversationRepository.save(capture(slot)) } answers { slot.captured } - - handler.handle(command) - - assertThat(slot.captured.title).isEqualTo("Padded Title") - } - - @Test - fun `handle throws when title is blank`() { - val command = - StartConversationCommand( - conversationId = ConversationId(UUID.randomUUID()), - userId = UUID.randomUUID(), - title = " ", - ) - - assertThatThrownBy { handler.handle(command) } - .isInstanceOf(IllegalArgumentException::class.java) - } -} diff --git a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetConversationQueryServiceTest.kt b/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetConversationQueryServiceTest.kt deleted file mode 100644 index 810f048..0000000 --- a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetConversationQueryServiceTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.query - -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.agents.domain.port.ConversationRepository -import com.jorisjonkers.personalstack.common.exception.NotFoundException -import io.mockk.every -import io.mockk.mockk -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.jupiter.api.Test -import java.time.Instant -import java.util.UUID - -class GetConversationQueryServiceTest { - private val conversationRepository = mockk() - private val service = GetConversationQueryService(conversationRepository) - - @Test - fun `findById returns conversation when found`() { - val conversationId = ConversationId(UUID.randomUUID()) - val conversation = - Conversation( - id = conversationId, - userId = UUID.randomUUID(), - title = "Test Conversation", - status = ConversationStatus.ACTIVE, - createdAt = Instant.now(), - updatedAt = Instant.now(), - ) - every { conversationRepository.findById(conversationId) } returns conversation - - val result = service.findById(conversationId) - - assertThat(result.title).isEqualTo("Test Conversation") - } - - @Test - fun `findById throws NotFoundException when not found`() { - val conversationId = ConversationId(UUID.randomUUID()) - every { conversationRepository.findById(conversationId) } returns null - - assertThatThrownBy { service.findById(conversationId) } - .isInstanceOf(NotFoundException::class.java) - } -} diff --git a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetMessageQueryServiceTest.kt b/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetMessageQueryServiceTest.kt deleted file mode 100644 index f06c82b..0000000 --- a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/application/query/GetMessageQueryServiceTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.jorisjonkers.personalstack.agents.application.query - -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.Message -import com.jorisjonkers.personalstack.agents.domain.model.MessageId -import com.jorisjonkers.personalstack.agents.domain.model.MessageRole -import com.jorisjonkers.personalstack.agents.domain.port.MessageRepository -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import java.time.Instant -import java.util.UUID - -class GetMessageQueryServiceTest { - private val messageRepository = mockk() - private val service = GetMessageQueryService(messageRepository) - - @Test - fun `findByConversationId returns messages`() { - val conversationId = ConversationId(UUID.randomUUID()) - val messages = - listOf( - buildMessage(conversationId = conversationId, content = "Hello"), - buildMessage(conversationId = conversationId, content = "World"), - ) - every { messageRepository.findByConversationId(conversationId) } returns messages - - val result = service.findByConversationId(conversationId) - - assertThat(result).hasSize(2) - assertThat(result[0].content).isEqualTo("Hello") - assertThat(result[1].content).isEqualTo("World") - } - - @Test - fun `findByConversationId returns empty list when no messages`() { - val conversationId = ConversationId(UUID.randomUUID()) - every { messageRepository.findByConversationId(conversationId) } returns emptyList() - - val result = service.findByConversationId(conversationId) - - assertThat(result).isEmpty() - } - - @Test - fun `findByConversationId delegates to repository`() { - val conversationId = ConversationId(UUID.randomUUID()) - every { messageRepository.findByConversationId(conversationId) } returns emptyList() - - service.findByConversationId(conversationId) - - verify(exactly = 1) { messageRepository.findByConversationId(conversationId) } - } - - private fun buildMessage( - conversationId: ConversationId, - content: String = "Test message", - ): Message = - Message( - id = MessageId(UUID.randomUUID()), - conversationId = conversationId, - role = MessageRole.USER, - content = content, - createdAt = Instant.now(), - ) -} diff --git a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/archunit/ArchitectureTest.kt b/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/archunit/ArchitectureTest.kt index ceb97d8..0eb2c82 100644 --- a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/archunit/ArchitectureTest.kt +++ b/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/archunit/ArchitectureTest.kt @@ -75,11 +75,16 @@ class ArchitectureTest { @Test fun `command handlers end with CommandHandler`() { + // Matched on @Service until the legacy Conversation/Message handlers + // (the only @Service ones) were dropped; every remaining handler is + // @Component, which made this rule check zero classes and fail + // outright (ArchUnit's failOnEmptyShould). Matching the CommandHandler + // interface directly is annotation-agnostic and keeps the rule live. classes() .that() .resideInAPackage("..application.command..") .and() - .areAnnotatedWith("org.springframework.stereotype.Service") + .implement(com.jorisjonkers.personalstack.common.command.CommandHandler::class.java) .should() .haveSimpleNameEndingWith("CommandHandler") .because("command handlers must follow *CommandHandler naming convention") diff --git a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/ConversationControllerTest.kt b/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/ConversationControllerTest.kt deleted file mode 100644 index 867ee4a..0000000 --- a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/ConversationControllerTest.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.web - -import com.fasterxml.jackson.databind.ObjectMapper -import com.jorisjonkers.personalstack.agents.application.query.GetConversationQueryService -import com.jorisjonkers.personalstack.agents.domain.model.Conversation -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.ConversationStatus -import com.jorisjonkers.personalstack.common.command.CommandBus -import com.jorisjonkers.personalstack.common.exception.NotFoundException -import com.jorisjonkers.personalstack.common.web.GlobalExceptionHandler -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.http.MediaType -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import org.springframework.test.web.servlet.setup.MockMvcBuilders -import java.time.Instant -import java.util.UUID - -class ConversationControllerTest { - private val commandBus = mockk(relaxed = true) - private val getConversationQueryService = mockk() - private val objectMapper = ObjectMapper() - private lateinit var mockMvc: MockMvc - - @BeforeEach - fun setUp() { - val controller = ConversationController(commandBus, getConversationQueryService) - mockMvc = - MockMvcBuilders - .standaloneSetup(controller) - .setControllerAdvice(GlobalExceptionHandler()) - .build() - } - - @Test - fun `POST creates conversation and returns 201`() { - val userId = UUID.randomUUID() - every { getConversationQueryService.findById(any()) } returns - buildConversation(userId = userId, title = "My Chat") - - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId.toString()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to "My Chat"))), - ).andExpect(status().isCreated) - .andExpect(jsonPath("$.title").value("My Chat")) - .andExpect(jsonPath("$.userId").value(userId.toString())) - .andExpect(jsonPath("$.status").value("ACTIVE")) - - verify { commandBus.dispatch(any()) } - } - - @Test - fun `POST with blank title returns 422`() { - val userId = UUID.randomUUID() - - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId.toString()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to ""))), - ).andExpect(status().isUnprocessableContent) - .andExpect(jsonPath("$.title").value("Validation Error")) - } - - @Test - fun `POST with title exceeding 200 chars returns 422`() { - val userId = UUID.randomUUID() - val longTitle = "a".repeat(201) - - mockMvc - .perform( - post("/api/v1/conversations") - .header("X-User-Id", userId.toString()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("title" to longTitle))), - ).andExpect(status().isUnprocessableContent) - .andExpect(jsonPath("$.title").value("Validation Error")) - } - - @Test - fun `GET by id returns conversation`() { - val conversationId = ConversationId(UUID.randomUUID()) - val conversation = buildConversation(id = conversationId, title = "Found It") - every { getConversationQueryService.findById(conversationId) } returns conversation - - mockMvc - .perform(get("/api/v1/conversations/${conversationId.value}")) - .andExpect(status().isOk) - .andExpect(jsonPath("$.id").value(conversationId.value.toString())) - .andExpect(jsonPath("$.title").value("Found It")) - } - - @Test - fun `GET by id with non-existent returns 404`() { - val conversationId = ConversationId(UUID.randomUUID()) - every { getConversationQueryService.findById(conversationId) } throws - NotFoundException("Conversation", conversationId.value.toString()) - - mockMvc - .perform(get("/api/v1/conversations/${conversationId.value}")) - .andExpect(status().isNotFound) - .andExpect(jsonPath("$.title").value("Resource Not Found")) - } - - @Test - fun `DELETE archives conversation and returns 204`() { - val userId = UUID.randomUUID() - val conversationId = UUID.randomUUID() - - mockMvc - .perform( - delete("/api/v1/conversations/$conversationId") - .header("X-User-Id", userId.toString()), - ).andExpect(status().isNoContent) - - verify { commandBus.dispatch(any()) } - } - - @Test - fun `GET list returns user conversations`() { - val userId = UUID.randomUUID() - val conversations = - listOf( - buildConversation(userId = userId, title = "First"), - buildConversation(userId = userId, title = "Second"), - ) - every { getConversationQueryService.findByUserId(userId) } returns conversations - - mockMvc - .perform( - get("/api/v1/conversations") - .header("X-User-Id", userId.toString()), - ).andExpect(status().isOk) - .andExpect(jsonPath("$.length()").value(2)) - .andExpect(jsonPath("$[0].title").value("First")) - .andExpect(jsonPath("$[1].title").value("Second")) - } - - @Test - fun `GET list without X-User-Id header returns error`() { - mockMvc - .perform(get("/api/v1/conversations")) - .andExpect(status().isInternalServerError) - } - - private fun buildConversation( - id: ConversationId = ConversationId(UUID.randomUUID()), - userId: UUID = UUID.randomUUID(), - title: String = "Test", - ): Conversation { - val now = Instant.now() - return Conversation( - id = id, - userId = userId, - title = title, - status = ConversationStatus.ACTIVE, - createdAt = now, - updatedAt = now, - ) - } -} diff --git a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/MessageControllerTest.kt b/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/MessageControllerTest.kt deleted file mode 100644 index 1f94aae..0000000 --- a/api/src/test/kotlin/com/jorisjonkers/personalstack/agents/infrastructure/web/MessageControllerTest.kt +++ /dev/null @@ -1,165 +0,0 @@ -package com.jorisjonkers.personalstack.agents.infrastructure.web - -import com.fasterxml.jackson.databind.ObjectMapper -import com.jorisjonkers.personalstack.agents.application.command.SendMessageCommand -import com.jorisjonkers.personalstack.agents.application.query.GetMessageQueryService -import com.jorisjonkers.personalstack.agents.domain.model.ConversationId -import com.jorisjonkers.personalstack.agents.domain.model.Message -import com.jorisjonkers.personalstack.agents.domain.model.MessageId -import com.jorisjonkers.personalstack.agents.domain.model.MessageRole -import com.jorisjonkers.personalstack.common.command.CommandBus -import com.jorisjonkers.personalstack.common.exception.NotFoundException -import com.jorisjonkers.personalstack.common.web.GlobalExceptionHandler -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.runs -import io.mockk.slot -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.http.MediaType -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import org.springframework.test.web.servlet.setup.MockMvcBuilders -import java.time.Instant -import java.util.UUID - -class MessageControllerTest { - private val commandBus = mockk(relaxed = true) - private val getMessageQueryService = mockk() - private val objectMapper = ObjectMapper() - private lateinit var mockMvc: MockMvc - - @BeforeEach - fun setUp() { - val controller = MessageController(commandBus, getMessageQueryService) - mockMvc = - MockMvcBuilders - .standaloneSetup(controller) - .setControllerAdvice(GlobalExceptionHandler()) - .build() - } - - @Test - fun `POST sends message and returns 201`() { - val conversationId = ConversationId(UUID.randomUUID()) - val userId = UUID.randomUUID() - - val commandSlot = slot() - every { commandBus.dispatch(capture(commandSlot)) } just runs - - every { getMessageQueryService.findByConversationId(conversationId) } answers { - val id = commandSlot.captured.messageId - listOf(buildMessage(id = id, conversationId = conversationId, content = "Hello world")) - } - - mockMvc - .perform( - post("/api/v1/conversations/${conversationId.value}/messages") - .header("X-User-Id", userId.toString()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "Hello world"))), - ).andExpect(status().isCreated) - .andExpect(jsonPath("$.content").value("Hello world")) - .andExpect(jsonPath("$.role").value("USER")) - } - - @Test - fun `POST with blank content returns 422`() { - val conversationId = UUID.randomUUID() - val userId = UUID.randomUUID() - - mockMvc - .perform( - post("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId.toString()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to ""))), - ).andExpect(status().isUnprocessableContent) - .andExpect(jsonPath("$.title").value("Validation Error")) - } - - @Test - fun `POST with content exceeding 10000 chars returns 422`() { - val conversationId = UUID.randomUUID() - val userId = UUID.randomUUID() - val longContent = "a".repeat(10001) - - mockMvc - .perform( - post("/api/v1/conversations/$conversationId/messages") - .header("X-User-Id", userId.toString()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to longContent))), - ).andExpect(status().isUnprocessableContent) - .andExpect(jsonPath("$.title").value("Validation Error")) - } - - @Test - fun `POST to non-existent conversation returns 404`() { - val conversationId = ConversationId(UUID.randomUUID()) - val userId = UUID.randomUUID() - - every { commandBus.dispatch(any()) } throws - NotFoundException("Conversation", conversationId.value.toString()) - - mockMvc - .perform( - post("/api/v1/conversations/${conversationId.value}/messages") - .header("X-User-Id", userId.toString()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(mapOf("content" to "Hello"))), - ).andExpect(status().isNotFound) - .andExpect(jsonPath("$.title").value("Resource Not Found")) - } - - @Test - fun `GET returns messages for conversation`() { - val conversationId = ConversationId(UUID.randomUUID()) - val messages = - listOf( - buildMessage(conversationId = conversationId, content = "Hello", role = MessageRole.USER), - buildMessage(conversationId = conversationId, content = "Hi there", role = MessageRole.ASSISTANT), - ) - - every { getMessageQueryService.findByConversationId(conversationId) } returns messages - - mockMvc - .perform(get("/api/v1/conversations/${conversationId.value}/messages")) - .andExpect(status().isOk) - .andExpect(jsonPath("$.length()").value(2)) - .andExpect(jsonPath("$[0].content").value("Hello")) - .andExpect(jsonPath("$[0].role").value("USER")) - .andExpect(jsonPath("$[1].content").value("Hi there")) - .andExpect(jsonPath("$[1].role").value("ASSISTANT")) - } - - @Test - fun `GET returns empty list for conversation with no messages`() { - val conversationId = ConversationId(UUID.randomUUID()) - - every { getMessageQueryService.findByConversationId(conversationId) } returns emptyList() - - mockMvc - .perform(get("/api/v1/conversations/${conversationId.value}/messages")) - .andExpect(status().isOk) - .andExpect(jsonPath("$.length()").value(0)) - } - - private fun buildMessage( - id: MessageId = MessageId(UUID.randomUUID()), - conversationId: ConversationId, - content: String = "Test message", - role: MessageRole = MessageRole.USER, - ): Message = - Message( - id = id, - conversationId = conversationId, - role = role, - content = content, - createdAt = Instant.now(), - ) -} diff --git a/client-spec/openapi/agents-api-client.json b/client-spec/openapi/agents-api-client.json index 687f431..f879361 100644 --- a/client-spec/openapi/agents-api-client.json +++ b/client-spec/openapi/agents-api-client.json @@ -689,149 +689,10 @@ } } }, - "/api/v1/conversations" : { - "get" : { - "tags" : [ "conversation-controller" ], - "operationId" : "listByUser", - "parameters" : [ { - "name" : "X-User-Id", - "in" : "header", - "required" : true, - "schema" : { - "type" : "string" - } - } ], - "responses" : { - "200" : { - "description" : "OK", - "content" : { - "*/*" : { - "schema" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/ConversationResponse" - } - } - } - } - } - }, - "deprecated" : true - }, - "post" : { - "tags" : [ "conversation-controller" ], - "operationId" : "create_3", - "parameters" : [ { - "name" : "X-User-Id", - "in" : "header", - "required" : true, - "schema" : { - "type" : "string" - } - } ], - "requestBody" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/CreateConversationRequest" - } - } - }, - "required" : true - }, - "responses" : { - "201" : { - "description" : "Created", - "content" : { - "*/*" : { - "schema" : { - "$ref" : "#/components/schemas/ConversationResponse" - } - } - } - } - }, - "deprecated" : true - } - }, - "/api/v1/conversations/{conversationId}/messages" : { - "get" : { - "tags" : [ "message-controller" ], - "operationId" : "list_3", - "parameters" : [ { - "name" : "conversationId", - "in" : "path", - "required" : true, - "schema" : { - "type" : "string", - "format" : "uuid" - } - } ], - "responses" : { - "200" : { - "description" : "OK", - "content" : { - "*/*" : { - "schema" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/MessageResponse" - } - } - } - } - } - }, - "deprecated" : true - }, - "post" : { - "tags" : [ "message-controller" ], - "operationId" : "send_1", - "parameters" : [ { - "name" : "conversationId", - "in" : "path", - "required" : true, - "schema" : { - "type" : "string", - "format" : "uuid" - } - }, { - "name" : "X-User-Id", - "in" : "header", - "required" : true, - "schema" : { - "type" : "string" - } - } ], - "requestBody" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SendMessageRequest" - } - } - }, - "required" : true - }, - "responses" : { - "201" : { - "description" : "Created", - "content" : { - "*/*" : { - "schema" : { - "$ref" : "#/components/schemas/MessageResponse" - } - } - } - } - }, - "deprecated" : true - } - }, "/api/v1/chat-sessions" : { "get" : { "tags" : [ "chat-session-controller" ], - "operationId" : "list_4", + "operationId" : "list_3", "parameters" : [ { "name" : "X-User-Id", "in" : "header", @@ -858,7 +719,7 @@ }, "post" : { "tags" : [ "chat-session-controller" ], - "operationId" : "create_4", + "operationId" : "create_3", "parameters" : [ { "name" : "X-User-Id", "in" : "header", @@ -1414,60 +1275,6 @@ } } }, - "/api/v1/conversations/{id}" : { - "get" : { - "tags" : [ "conversation-controller" ], - "operationId" : "getById", - "parameters" : [ { - "name" : "id", - "in" : "path", - "required" : true, - "schema" : { - "type" : "string", - "format" : "uuid" - } - } ], - "responses" : { - "200" : { - "description" : "OK", - "content" : { - "*/*" : { - "schema" : { - "$ref" : "#/components/schemas/ConversationResponse" - } - } - } - } - }, - "deprecated" : true - }, - "delete" : { - "tags" : [ "conversation-controller" ], - "operationId" : "archive", - "parameters" : [ { - "name" : "id", - "in" : "path", - "required" : true, - "schema" : { - "type" : "string", - "format" : "uuid" - } - }, { - "name" : "X-User-Id", - "in" : "header", - "required" : true, - "schema" : { - "type" : "string" - } - } ], - "responses" : { - "204" : { - "description" : "No Content" - } - }, - "deprecated" : true - } - }, "/api/v1/chat-sessions/{id}" : { "get" : { "tags" : [ "chat-session-controller" ], @@ -1497,7 +1304,7 @@ }, "delete" : { "tags" : [ "chat-session-controller" ], - "operationId" : "archive_1", + "operationId" : "archive", "parameters" : [ { "name" : "id", "in" : "path", @@ -2286,80 +2093,6 @@ }, "required" : [ "url" ] }, - "CreateConversationRequest" : { - "type" : "object", - "properties" : { - "title" : { - "type" : "string", - "maxLength" : 200, - "minLength" : 0 - } - }, - "required" : [ "title" ] - }, - "ConversationResponse" : { - "type" : "object", - "properties" : { - "id" : { - "type" : "string", - "format" : "uuid" - }, - "userId" : { - "type" : "string", - "format" : "uuid" - }, - "title" : { - "type" : "string" - }, - "status" : { - "type" : "string" - }, - "createdAt" : { - "type" : "string", - "format" : "date-time" - }, - "updatedAt" : { - "type" : "string", - "format" : "date-time" - } - }, - "required" : [ "createdAt", "id", "status", "title", "updatedAt", "userId" ] - }, - "SendMessageRequest" : { - "type" : "object", - "properties" : { - "content" : { - "type" : "string", - "maxLength" : 10000, - "minLength" : 0 - } - }, - "required" : [ "content" ] - }, - "MessageResponse" : { - "type" : "object", - "properties" : { - "id" : { - "type" : "string", - "format" : "uuid" - }, - "conversationId" : { - "type" : "string", - "format" : "uuid" - }, - "role" : { - "type" : "string" - }, - "content" : { - "type" : "string" - }, - "createdAt" : { - "type" : "string", - "format" : "date-time" - } - }, - "required" : [ "content", "conversationId", "createdAt", "id", "role" ] - }, "StartChatSessionRequest" : { "type" : "object", "properties" : { diff --git a/client-spec/openapi/agents-api.json b/client-spec/openapi/agents-api.json index f36688d..830ed20 100644 --- a/client-spec/openapi/agents-api.json +++ b/client-spec/openapi/agents-api.json @@ -689,149 +689,10 @@ } } }, - "/api/v1/conversations" : { - "get" : { - "tags" : [ "conversation-controller" ], - "operationId" : "listByUser", - "parameters" : [ { - "name" : "X-User-Id", - "in" : "header", - "required" : true, - "schema" : { - "type" : "string" - } - } ], - "responses" : { - "200" : { - "description" : "OK", - "content" : { - "*/*" : { - "schema" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/ConversationResponse" - } - } - } - } - } - }, - "deprecated" : true - }, - "post" : { - "tags" : [ "conversation-controller" ], - "operationId" : "create_3", - "parameters" : [ { - "name" : "X-User-Id", - "in" : "header", - "required" : true, - "schema" : { - "type" : "string" - } - } ], - "requestBody" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/CreateConversationRequest" - } - } - }, - "required" : true - }, - "responses" : { - "201" : { - "description" : "Created", - "content" : { - "*/*" : { - "schema" : { - "$ref" : "#/components/schemas/ConversationResponse" - } - } - } - } - }, - "deprecated" : true - } - }, - "/api/v1/conversations/{conversationId}/messages" : { - "get" : { - "tags" : [ "message-controller" ], - "operationId" : "list_3", - "parameters" : [ { - "name" : "conversationId", - "in" : "path", - "required" : true, - "schema" : { - "type" : "string", - "format" : "uuid" - } - } ], - "responses" : { - "200" : { - "description" : "OK", - "content" : { - "*/*" : { - "schema" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/MessageResponse" - } - } - } - } - } - }, - "deprecated" : true - }, - "post" : { - "tags" : [ "message-controller" ], - "operationId" : "send_1", - "parameters" : [ { - "name" : "conversationId", - "in" : "path", - "required" : true, - "schema" : { - "type" : "string", - "format" : "uuid" - } - }, { - "name" : "X-User-Id", - "in" : "header", - "required" : true, - "schema" : { - "type" : "string" - } - } ], - "requestBody" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SendMessageRequest" - } - } - }, - "required" : true - }, - "responses" : { - "201" : { - "description" : "Created", - "content" : { - "*/*" : { - "schema" : { - "$ref" : "#/components/schemas/MessageResponse" - } - } - } - } - }, - "deprecated" : true - } - }, "/api/v1/chat-sessions" : { "get" : { "tags" : [ "chat-session-controller" ], - "operationId" : "list_4", + "operationId" : "list_3", "parameters" : [ { "name" : "X-User-Id", "in" : "header", @@ -858,7 +719,7 @@ }, "post" : { "tags" : [ "chat-session-controller" ], - "operationId" : "create_4", + "operationId" : "create_3", "parameters" : [ { "name" : "X-User-Id", "in" : "header", @@ -1414,60 +1275,6 @@ } } }, - "/api/v1/conversations/{id}" : { - "get" : { - "tags" : [ "conversation-controller" ], - "operationId" : "getById", - "parameters" : [ { - "name" : "id", - "in" : "path", - "required" : true, - "schema" : { - "type" : "string", - "format" : "uuid" - } - } ], - "responses" : { - "200" : { - "description" : "OK", - "content" : { - "*/*" : { - "schema" : { - "$ref" : "#/components/schemas/ConversationResponse" - } - } - } - } - }, - "deprecated" : true - }, - "delete" : { - "tags" : [ "conversation-controller" ], - "operationId" : "archive", - "parameters" : [ { - "name" : "id", - "in" : "path", - "required" : true, - "schema" : { - "type" : "string", - "format" : "uuid" - } - }, { - "name" : "X-User-Id", - "in" : "header", - "required" : true, - "schema" : { - "type" : "string" - } - } ], - "responses" : { - "204" : { - "description" : "No Content" - } - }, - "deprecated" : true - } - }, "/api/v1/chat-sessions/{id}" : { "get" : { "tags" : [ "chat-session-controller" ], @@ -1497,7 +1304,7 @@ }, "delete" : { "tags" : [ "chat-session-controller" ], - "operationId" : "archive_1", + "operationId" : "archive", "parameters" : [ { "name" : "id", "in" : "path", @@ -2286,80 +2093,6 @@ }, "required" : [ "url" ] }, - "CreateConversationRequest" : { - "type" : "object", - "properties" : { - "title" : { - "type" : "string", - "maxLength" : 200, - "minLength" : 0 - } - }, - "required" : [ "title" ] - }, - "ConversationResponse" : { - "type" : "object", - "properties" : { - "id" : { - "type" : "string", - "format" : "uuid" - }, - "userId" : { - "type" : "string", - "format" : "uuid" - }, - "title" : { - "type" : "string" - }, - "status" : { - "type" : "string" - }, - "createdAt" : { - "type" : "string", - "format" : "date-time" - }, - "updatedAt" : { - "type" : "string", - "format" : "date-time" - } - }, - "required" : [ "createdAt", "id", "status", "title", "updatedAt", "userId" ] - }, - "SendMessageRequest" : { - "type" : "object", - "properties" : { - "content" : { - "type" : "string", - "maxLength" : 10000, - "minLength" : 0 - } - }, - "required" : [ "content" ] - }, - "MessageResponse" : { - "type" : "object", - "properties" : { - "id" : { - "type" : "string", - "format" : "uuid" - }, - "conversationId" : { - "type" : "string", - "format" : "uuid" - }, - "role" : { - "type" : "string" - }, - "content" : { - "type" : "string" - }, - "createdAt" : { - "type" : "string", - "format" : "date-time" - } - }, - "required" : [ "content", "conversationId", "createdAt", "id", "role" ] - }, "StartChatSessionRequest" : { "type" : "object", "properties" : {