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