feat(sessions): SessionSummary + SessionSummaryProjector

This commit is contained in:
2026-06-08 10:15:56 +04:00
parent b830528d31
commit 831ba003e0
3 changed files with 154 additions and 0 deletions
@@ -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)
}
}