8 files changed
+517
-2
No files matched your search
@@ -99,6 +99,108 @@ class HermesApiServiceTest {
|
||||
assertEquals("GET", recordedRequest.method)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCronJobRuns_requestsBoundedHistoryAndParsesResponse() =
|
||||
runTest {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody(
|
||||
"""
|
||||
{
|
||||
"runs": [
|
||||
{
|
||||
"id": "cron_job_1_1700000000",
|
||||
"source": "cron",
|
||||
"model": "gpt",
|
||||
"title": "Backup",
|
||||
"started_at": 1700000000.5,
|
||||
"ended_at": 1700000020.0,
|
||||
"last_active": 1700000020.0,
|
||||
"is_active": false,
|
||||
"archived": true,
|
||||
"message_count": 2,
|
||||
"tool_call_count": 1,
|
||||
"input_tokens": 123,
|
||||
"output_tokens": 45,
|
||||
"preview": "done",
|
||||
"profile": "worker"
|
||||
}
|
||||
],
|
||||
"limit": 20
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
|
||||
val response = apiService.getCronJobRuns("job_1", limit = 20)
|
||||
|
||||
assertTrue(response.isSuccessful)
|
||||
val run = response.body()?.runs?.single()
|
||||
assertNotNull(run)
|
||||
assertEquals("cron_job_1_1700000000", run?.id)
|
||||
assertEquals("worker", run?.profile)
|
||||
assertEquals(true, run?.archived)
|
||||
assertEquals(2, run?.message_count)
|
||||
assertEquals("done", run?.preview)
|
||||
|
||||
val recordedRequest = mockWebServer.takeRequest()
|
||||
assertEquals("/api/cron/jobs/job_1/runs?limit=20", recordedRequest.path)
|
||||
assertEquals("GET", recordedRequest.method)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCronJobRuns_emptyResponse() =
|
||||
runTest {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""{"runs":[],"limit":20}"""),
|
||||
)
|
||||
|
||||
val response = apiService.getCronJobRuns("job_1")
|
||||
|
||||
assertTrue(response.isSuccessful)
|
||||
assertTrue(response.body()?.runs?.isEmpty() == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCronJobRuns_missingOptionalFieldsUsesDefaults() =
|
||||
runTest {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""{"runs":[{"id":"cron_job_1_1"}],"limit":20}"""),
|
||||
)
|
||||
|
||||
val run =
|
||||
apiService
|
||||
.getCronJobRuns("job_1")
|
||||
.body()
|
||||
?.runs
|
||||
?.single()
|
||||
|
||||
assertNotNull(run)
|
||||
assertNull(run?.started_at)
|
||||
assertEquals(false, run?.is_active)
|
||||
assertEquals(false, run?.archived)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCronJobRuns_missingRequiredIdThrows() {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""{"runs":[{"started_at":1.0}],"limit":20}"""),
|
||||
)
|
||||
|
||||
assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
runTest {
|
||||
apiService.getCronJobRuns("job_1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPauseCronJob_sendsPostRequest() =
|
||||
runTest {
|
||||
|
||||
@@ -125,6 +125,20 @@ class ProfileScopeInterceptorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cronRunHistory_preservesLimitAndFollowsActiveProfileAcrossSwitches() {
|
||||
val client = clientFor("alpha")
|
||||
|
||||
val alphaUrl = requestedUrl(client, "api/cron/jobs/job-1/runs?limit=20")
|
||||
assertEquals("alpha", alphaUrl.queryParameter("profile"))
|
||||
assertEquals("20", alphaUrl.queryParameter("limit"))
|
||||
|
||||
AuthManager.setActiveProfileId("beta")
|
||||
val betaUrl = requestedUrl(client, "api/cron/jobs/job-1/runs?limit=20")
|
||||
assertEquals("beta", betaUrl.queryParameter("profile"))
|
||||
assertEquals("20", betaUrl.queryParameter("limit"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skillsEndpoint_isScoped() {
|
||||
val client = clientFor("alpha")
|
||||
|
||||
@@ -6,6 +6,8 @@ import com.m57.hermescontrol.data.model.CronBlueprintField
|
||||
import com.m57.hermescontrol.data.model.CronBlueprintListResponse
|
||||
import com.m57.hermescontrol.data.model.CronJob
|
||||
import com.m57.hermescontrol.data.model.CronJobFireError
|
||||
import com.m57.hermescontrol.data.model.CronRun
|
||||
import com.m57.hermescontrol.data.model.CronRunHistoryResponse
|
||||
import com.m57.hermescontrol.data.model.DeliveryTarget
|
||||
import com.m57.hermescontrol.data.model.DeliveryTargetsResponse
|
||||
import com.m57.hermescontrol.data.model.InstantiateBlueprintRequest
|
||||
@@ -19,6 +21,7 @@ import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkAll
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
@@ -125,6 +128,77 @@ class CronJobsViewModelTest {
|
||||
assertEquals(listOf("origin", "local", "telegram"), editor.deliveryOptions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openRunHistory loads bounded recent runs`() {
|
||||
coEvery { mockApi.getCronJobRuns("j1", 20) } returns
|
||||
Response.success(
|
||||
CronRunHistoryResponse(
|
||||
runs =
|
||||
listOf(
|
||||
CronRun(
|
||||
id = "cron_j1_1",
|
||||
started_at = 1_700_000_000.0,
|
||||
is_active = true,
|
||||
profile = "default",
|
||||
),
|
||||
),
|
||||
limit = 20,
|
||||
),
|
||||
)
|
||||
val vm = createViewModel()
|
||||
|
||||
vm.openRunHistory(CronJob(id = "j1", name = "Morning"))
|
||||
settle()
|
||||
|
||||
val history = vm.uiState.value.runHistoryState
|
||||
assertTrue(history.isOpen)
|
||||
assertFalse(history.isLoading)
|
||||
assertEquals("j1", history.jobId)
|
||||
assertEquals("Morning", history.jobName)
|
||||
assertEquals(listOf("cron_j1_1"), history.runs.map { it.id })
|
||||
coVerify(exactly = 1) { mockApi.getCronJobRuns("j1", 20) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openRunHistory explains unsupported backend`() {
|
||||
coEvery { mockApi.getCronJobRuns("j1", 20) } returns
|
||||
Response.error(404, "{}".toResponseBody())
|
||||
val vm = createViewModel()
|
||||
|
||||
vm.openRunHistory(CronJob(id = "j1", name = "Morning"))
|
||||
settle()
|
||||
|
||||
val history = vm.uiState.value.runHistoryState
|
||||
assertFalse(history.isLoading)
|
||||
assertTrue(history.runs.isEmpty())
|
||||
assertTrue(history.errorMessage.orEmpty().contains("unavailable"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `scope reset cancels in flight run history and clears rows`() {
|
||||
val response = CompletableDeferred<Response<CronRunHistoryResponse>>()
|
||||
coEvery { mockApi.getCronJobRuns("j1", 20) } coAnswers { response.await() }
|
||||
val vm = createViewModel()
|
||||
|
||||
vm.openRunHistory(CronJob(id = "j1", name = "Morning"))
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
vm.clearScopeOwnedState()
|
||||
response.complete(
|
||||
Response.success(
|
||||
CronRunHistoryResponse(
|
||||
runs = listOf(CronRun(id = "cron_j1_stale", profile = "old-profile")),
|
||||
),
|
||||
),
|
||||
)
|
||||
settle()
|
||||
|
||||
val history = vm.uiState.value.runHistoryState
|
||||
assertFalse(history.isOpen)
|
||||
assertFalse(history.isLoading)
|
||||
assertTrue(history.runs.isEmpty())
|
||||
assertNull(history.jobId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selectBlueprint seeds slot defaults`() {
|
||||
val vm = createViewModel()
|
||||
|
||||
Reference in new issue
Block a user