feat(sessions): SessionSummary + SessionSummaryProjector
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package com.correx.core.sessions
|
||||
|
||||
import com.correx.core.events.types.SessionId
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SessionSummary(
|
||||
val sessionId: SessionId,
|
||||
val intent: String?,
|
||||
val workflowId: String?,
|
||||
val status: String,
|
||||
val createdAt: Instant?,
|
||||
val lastActivityAt: Instant?,
|
||||
val stageCount: Int,
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.correx.core.sessions
|
||||
|
||||
import com.correx.core.events.events.InitialIntentEvent
|
||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||
import com.correx.core.events.events.WorkflowFailedEvent
|
||||
import com.correx.core.events.events.WorkflowStartedEvent
|
||||
import com.correx.core.events.types.SessionId
|
||||
|
||||
class SessionSummaryProjector {
|
||||
fun project(sessionId: SessionId, events: List<StoredEvent>): SessionSummary {
|
||||
var intent: String? = null
|
||||
var workflowId: String? = null
|
||||
var stageCount = 0
|
||||
var status = "UNKNOWN"
|
||||
val createdAt = events.firstOrNull()?.metadata?.timestamp
|
||||
val lastActivityAt = events.lastOrNull()?.metadata?.timestamp
|
||||
|
||||
events.forEach { event ->
|
||||
when (val p = event.payload) {
|
||||
is InitialIntentEvent -> intent = p.intent
|
||||
is WorkflowStartedEvent -> {
|
||||
workflowId = p.workflowId
|
||||
status = SessionStatus.ACTIVE.name
|
||||
}
|
||||
is TransitionExecutedEvent -> stageCount++
|
||||
is OrchestrationPausedEvent -> status = SessionStatus.PAUSED.name
|
||||
is WorkflowCompletedEvent -> status = SessionStatus.COMPLETED.name
|
||||
is WorkflowFailedEvent -> status = SessionStatus.FAILED.name
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
return SessionSummary(
|
||||
sessionId = sessionId,
|
||||
intent = intent,
|
||||
workflowId = workflowId,
|
||||
status = status,
|
||||
createdAt = createdAt,
|
||||
lastActivityAt = lastActivityAt,
|
||||
stageCount = stageCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.correx.core.sessions
|
||||
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.InitialIntentEvent
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
import com.correx.core.events.types.EventId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.types.TransitionId
|
||||
import kotlinx.datetime.Instant
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SessionSummaryProjectorTest {
|
||||
|
||||
private val projector = SessionSummaryProjector()
|
||||
private val sessionId = SessionId("s1")
|
||||
|
||||
private fun stored(
|
||||
payload: com.correx.core.events.events.EventPayload,
|
||||
eventId: String = "e1",
|
||||
timestamp: Instant = Instant.parse("2026-01-01T00:00:00Z"),
|
||||
sequence: Long = 1L,
|
||||
) = StoredEvent(
|
||||
metadata = EventMetadata(
|
||||
eventId = EventId(eventId),
|
||||
sessionId = sessionId,
|
||||
timestamp = timestamp,
|
||||
schemaVersion = 1,
|
||||
causationId = null,
|
||||
correlationId = null,
|
||||
),
|
||||
sequence = sequence,
|
||||
sessionSequence = sequence,
|
||||
payload = payload,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `empty event list produces zeroed summary`() {
|
||||
val summary = projector.project(sessionId, emptyList())
|
||||
|
||||
assertEquals(sessionId, summary.sessionId)
|
||||
assertNull(summary.intent)
|
||||
assertNull(summary.workflowId)
|
||||
assertNull(summary.createdAt)
|
||||
assertNull(summary.lastActivityAt)
|
||||
assertEquals(0, summary.stageCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `InitialIntentEvent populates intent`() {
|
||||
val event = stored(InitialIntentEvent(sessionId, "add auth layer"), eventId = "e1")
|
||||
|
||||
val summary = projector.project(sessionId, listOf(event))
|
||||
|
||||
assertEquals("add auth layer", summary.intent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two TransitionExecutedEvents yield stageCount of two`() {
|
||||
val t1 = stored(
|
||||
TransitionExecutedEvent(sessionId, StageId("s1"), StageId("s2"), TransitionId("t1")),
|
||||
eventId = "e1", sequence = 1L,
|
||||
)
|
||||
val t2 = stored(
|
||||
TransitionExecutedEvent(sessionId, StageId("s2"), StageId("s3"), TransitionId("t2")),
|
||||
eventId = "e2", sequence = 2L,
|
||||
)
|
||||
|
||||
val summary = projector.project(sessionId, listOf(t1, t2))
|
||||
|
||||
assertEquals(2, summary.stageCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createdAt is first event timestamp and lastActivityAt is last event timestamp`() {
|
||||
val first = Instant.parse("2026-01-01T10:00:00Z")
|
||||
val last = Instant.parse("2026-01-01T11:00:00Z")
|
||||
val e1 = stored(InitialIntentEvent(sessionId, "intent"), eventId = "e1", timestamp = first, sequence = 1L)
|
||||
val e2 = stored(
|
||||
TransitionExecutedEvent(sessionId, StageId("a"), StageId("b"), TransitionId("t1")),
|
||||
eventId = "e2", timestamp = last, sequence = 2L,
|
||||
)
|
||||
|
||||
val summary = projector.project(sessionId, listOf(e1, e2))
|
||||
|
||||
assertEquals(first, summary.createdAt)
|
||||
assertEquals(last, summary.lastActivityAt)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user