fix(debug): Persist recording session across app restarts

The debug recording trigger file now stores the session directory path and start timestamp. On app restart, the recorder resumes into the same session directory and log file instead of creating a new one, preserving the original start time so the short-recording guard doesn't reset.
This commit is contained in:
darken
2026-03-09 17:34:29 +00:00
committed by Matthias Urhahn
parent 3423786f06
commit 8675d73a98
3 changed files with 229 additions and 11 deletions
@@ -33,5 +33,10 @@ class RecorderModuleStateTest : BaseTest() {
fun `currentLogPath is null`() {
RecorderModule.State().currentLogPath shouldBe null
}
@Test
fun `persistedLogDir is null`() {
RecorderModule.State().persistedLogDir shouldBe null
}
}
}
@@ -0,0 +1,134 @@
package eu.darken.capod.common.debug.recording.core
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import testhelpers.BaseTest
import java.io.File
class RecorderModuleTriggerFileTest : BaseTest() {
@TempDir
lateinit var tempDir: File
private val now = 1_700_000_000_000L
@AfterEach
fun cleanup() {
tempDir.listFiles()?.forEach { it.deleteRecursively() }
}
@Nested
inner class ParseTriggerContent {
@Test
fun `valid content returns dir and timestamp`() {
val sessionDir = File(tempDir, "capod_session").also { it.mkdirs() }
val content = "${sessionDir.absolutePath}\n$now"
val result = RecorderModule.parseTriggerContent(content, now = now)
result shouldBe (sessionDir to now)
}
@Test
fun `empty content returns null`() {
RecorderModule.parseTriggerContent("", now = now).shouldBeNull()
}
@Test
fun `blank content returns null`() {
RecorderModule.parseTriggerContent(" \n ", now = now).shouldBeNull()
}
@Test
fun `single line returns null`() {
RecorderModule.parseTriggerContent("/some/path", now = now).shouldBeNull()
}
@Test
fun `non-numeric timestamp returns null`() {
val sessionDir = File(tempDir, "capod_session").also { it.mkdirs() }
val content = "${sessionDir.absolutePath}\nnotanumber"
RecorderModule.parseTriggerContent(content, now = now).shouldBeNull()
}
@Test
fun `zero timestamp returns null`() {
val sessionDir = File(tempDir, "capod_session").also { it.mkdirs() }
val content = "${sessionDir.absolutePath}\n0"
RecorderModule.parseTriggerContent(content, now = now).shouldBeNull()
}
@Test
fun `negative timestamp returns null`() {
val sessionDir = File(tempDir, "capod_session").also { it.mkdirs() }
val content = "${sessionDir.absolutePath}\n-1000"
RecorderModule.parseTriggerContent(content, now = now).shouldBeNull()
}
@Test
fun `future timestamp beyond skew returns null`() {
val sessionDir = File(tempDir, "capod_session").also { it.mkdirs() }
val futureTs = now + 120_000L
val content = "${sessionDir.absolutePath}\n$futureTs"
RecorderModule.parseTriggerContent(content, now = now).shouldBeNull()
}
@Test
fun `future timestamp within skew is accepted`() {
val sessionDir = File(tempDir, "capod_session").also { it.mkdirs() }
val nearFutureTs = now + 30_000L
val content = "${sessionDir.absolutePath}\n$nearFutureTs"
val result = RecorderModule.parseTriggerContent(content, now = now)
result shouldBe (sessionDir to nearFutureTs)
}
@Test
fun `non-existent directory returns null`() {
val content = "/nonexistent/path/capod_session\n$now"
RecorderModule.parseTriggerContent(content, now = now).shouldBeNull()
}
@Test
fun `content with trailing whitespace is handled`() {
val sessionDir = File(tempDir, "capod_session").also { it.mkdirs() }
val content = " ${sessionDir.absolutePath}\n$now \n"
val result = RecorderModule.parseTriggerContent(content, now = now)
result shouldBe (sessionDir to now)
}
@Test
fun `old timestamp is accepted`() {
val sessionDir = File(tempDir, "capod_session").also { it.mkdirs() }
val oldTs = 1_000L
val content = "${sessionDir.absolutePath}\n$oldTs"
val result = RecorderModule.parseTriggerContent(content, now = now)
result shouldBe (sessionDir to oldTs)
}
@Test
fun `extra lines are ignored`() {
val sessionDir = File(tempDir, "capod_session").also { it.mkdirs() }
val content = "${sessionDir.absolutePath}\n$now\nextra\ndata"
val result = RecorderModule.parseTriggerContent(content, now = now)
result shouldBe (sessionDir to now)
}
}
}