feat(backend): persist tasks and generated message history

- add EntityTask domain and CRUD API backed by PostgreSQL

- relate generated messages directly to tasks and delete on task removal

- move preview generation to backend Llama endpoint

- migrate frontend task APIs from localStorage to backend endpoints

- update tests and CLAUDE rules for backend-owned LLM/persistence
This commit is contained in:
2026-03-27 02:46:56 -03:00
parent f2a16b5cf6
commit ebcea643c4
20 changed files with 1181 additions and 244 deletions

View File

@@ -0,0 +1,84 @@
package com.condado.newsletter.service
import com.condado.newsletter.dto.TaskPreviewEntityDto
import com.condado.newsletter.dto.TaskPreviewGenerateRequestDto
import com.condado.newsletter.dto.TaskPreviewTaskDto
import com.condado.newsletter.model.EntityTask
import com.condado.newsletter.model.GeneratedMessageHistory
import com.condado.newsletter.model.VirtualEntity
import com.condado.newsletter.repository.EntityTaskRepository
import com.condado.newsletter.repository.GeneratedMessageHistoryRepository
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.junit.jupiter.api.Test
import java.util.UUID
class TaskGeneratedMessageServiceTest {
private val generatedMessageHistoryRepository: GeneratedMessageHistoryRepository = mockk()
private val entityTaskRepository: EntityTaskRepository = mockk()
private val llamaPreviewService: LlamaPreviewService = mockk()
private val service = TaskGeneratedMessageService(
generatedMessageHistoryRepository = generatedMessageHistoryRepository,
entityTaskRepository = entityTaskRepository,
llamaPreviewService = llamaPreviewService
)
@Test
fun should_generateAndPersistMessage_when_generateAndSaveCalled() {
val taskId = UUID.randomUUID()
val entity = VirtualEntity(name = "Entity", email = "e@x.com", jobTitle = "Ops").apply { id = UUID.randomUUID() }
val task = EntityTask(
virtualEntity = entity,
name = "Task",
prompt = "Prompt",
scheduleCron = "0 9 * * 1",
emailLookback = "last_week"
).apply { id = taskId }
val captured = slot<GeneratedMessageHistory>()
every { llamaPreviewService.generate(any()) } returns "SUBJECT: Test\nBODY:\nGenerated by backend"
every { entityTaskRepository.findById(taskId) } returns java.util.Optional.of(task)
every { generatedMessageHistoryRepository.countByTask_Id(taskId) } returns 0
every { generatedMessageHistoryRepository.save(capture(captured)) } answers {
captured.captured.apply {
id = UUID.fromString("00000000-0000-0000-0000-000000000001")
}
}
val response = service.generateAndSave(taskId, sampleRequest())
assertThat(response.id).isEqualTo(UUID.fromString("00000000-0000-0000-0000-000000000001"))
assertThat(response.taskId).isEqualTo(taskId)
assertThat(response.label).isEqualTo("Message #1")
assertThat(response.content).contains("Generated by backend")
assertThat(captured.captured.task.id).isEqualTo(taskId)
verify(exactly = 1) { llamaPreviewService.generate(any()) }
verify(exactly = 1) { generatedMessageHistoryRepository.save(any()) }
}
private fun sampleRequest() = TaskPreviewGenerateRequestDto(
entity = TaskPreviewEntityDto(
id = UUID.randomUUID().toString(),
name = "Entity A",
email = "entity@example.com",
jobTitle = "Ops",
personality = "Formal",
scheduleCron = "0 9 * * 1",
contextWindowDays = 3,
active = true
),
task = TaskPreviewTaskDto(
entityId = UUID.randomUUID().toString(),
name = "Morning Blast",
prompt = "Talk about coffee",
scheduleCron = "0 8 * * 1-5",
emailLookback = "last_week"
)
)
}