feat(server,cli): session replay inspection — timeline + determinism digest
This commit is contained in:
@@ -3,6 +3,7 @@ package com.correx.apps.cli
|
||||
import com.correx.apps.cli.commands.ApproveCommand
|
||||
import com.correx.apps.cli.commands.EventsCommand
|
||||
import com.correx.apps.cli.commands.ProviderCommand
|
||||
import com.correx.apps.cli.commands.ReplayCommand
|
||||
import com.correx.apps.cli.commands.RunCommand
|
||||
import com.correx.apps.cli.commands.SessionCommand
|
||||
import com.correx.apps.cli.commands.StatusCommand
|
||||
@@ -27,4 +28,5 @@ fun buildCli(): CorrexCli = CorrexCli().subcommands(
|
||||
ProviderCommand(),
|
||||
UndoCommand(),
|
||||
EventsCommand(),
|
||||
ReplayCommand(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.correx.apps.cli.commands
|
||||
|
||||
import com.correx.apps.cli.CorrexCli
|
||||
import com.correx.apps.cli.DEFAULT_PORT
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.ProgramResult
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.options.default
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
data class TimelineEntryDto(
|
||||
val sequence: Long,
|
||||
val type: String,
|
||||
val stageId: String?,
|
||||
val summary: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReplayReportDto(
|
||||
val sessionId: String,
|
||||
val eventCount: Long,
|
||||
val timeline: List<TimelineEntryDto>,
|
||||
val digest: String,
|
||||
val deterministic: Boolean,
|
||||
)
|
||||
|
||||
private const val SEPARATOR_WIDTH = 110
|
||||
|
||||
private fun renderReport(report: ReplayReportDto): String {
|
||||
val lines = mutableListOf<String>()
|
||||
lines += "Session: ${report.sessionId}"
|
||||
lines += "Events: ${report.eventCount}"
|
||||
lines += "Digest: ${report.digest}"
|
||||
lines += "Deterministic: ${report.deterministic}"
|
||||
lines += ""
|
||||
lines += "%-6s %-35s %-20s %s".format("seq", "type", "stageId", "summary")
|
||||
lines += "-".repeat(SEPARATOR_WIDTH)
|
||||
for (entry in report.timeline) {
|
||||
lines += "%-6d %-35s %-20s %s".format(
|
||||
entry.sequence,
|
||||
entry.type,
|
||||
entry.stageId ?: "-",
|
||||
entry.summary,
|
||||
)
|
||||
}
|
||||
return lines.joinToString("\n")
|
||||
}
|
||||
|
||||
class ReplayCommand : CliktCommand(name = "replay") {
|
||||
private val sessionId by argument("sessionId")
|
||||
private val host by option("--host").default("localhost")
|
||||
private val port by option("--port").default("$DEFAULT_PORT")
|
||||
|
||||
private val replayJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
override fun run(): Unit = runBlocking {
|
||||
val portInt = port.toIntOrNull() ?: DEFAULT_PORT
|
||||
val outputJson = (currentContext.findRoot().command as? CorrexCli)?.json ?: false
|
||||
val url = "http://$host:$portInt/sessions/$sessionId/replay"
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(replayJson) }
|
||||
}
|
||||
|
||||
val nonDeterministic = runCatching {
|
||||
if (outputJson) {
|
||||
val body = client.get(url).bodyAsText()
|
||||
println(body)
|
||||
val parsed = replayJson.decodeFromString<ReplayReportDto>(body)
|
||||
!parsed.deterministic
|
||||
} else {
|
||||
val report = client.get(url).body<ReplayReportDto>()
|
||||
println(renderReport(report))
|
||||
!report.deterministic
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
System.err.println("Error fetching replay: ${e.message}")
|
||||
false
|
||||
}
|
||||
|
||||
client.close()
|
||||
if (nonDeterministic) throw ProgramResult(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user