fix(tui): update tests to match refactored InputMode and flat TuiState

Sync all TUI reducer and input tests with the state model refactor:
InputMode.None/WorkflowId/SteeringNote/Filter → ROUTER/NAVIGATE/STEER/FILTER;
InputState(mode, text) → TuiState(inputMode, inputBuffer); fix SessionStarted
effect assertion to expect ConnectSession.
This commit is contained in:
2026-05-20 00:48:14 +04:00
parent 7ac3a1ff79
commit a855eaee45
6 changed files with 197 additions and 178 deletions
@@ -8,110 +8,110 @@ import kotlin.test.assertNull
class KeyResolverTest { class KeyResolverTest {
@Test @Test
fun `n in None mode resolves to OpenNewSessionPrompt`() { fun `n in ROUTER mode resolves to OpenNewSessionPrompt`() {
val result = KeyResolver.resolve(KeyEvent.NewSession, InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.NewSession, InputMode.ROUTER, "")
assertEquals(Action.OpenNewSessionPrompt, result) assertEquals(Action.OpenNewSessionPrompt, result)
} }
@Test @Test
fun `command letters in input mode resolve to null`() { fun `command letters in text input mode resolve to null`() {
val result = KeyResolver.resolve(KeyEvent.NewSession, InputMode.WorkflowId, "") val result = KeyResolver.resolve(KeyEvent.NewSession, InputMode.FILTER, "")
assertNull(result) assertNull(result)
} }
@Test @Test
fun `CharInput in input mode resolves to AppendChar`() { fun `CharInput in text input mode resolves to AppendChar`() {
val result = KeyResolver.resolve(KeyEvent.CharInput('a'), InputMode.WorkflowId, "") val result = KeyResolver.resolve(KeyEvent.CharInput('a'), InputMode.FILTER, "")
assertEquals(Action.AppendChar('a'), result) assertEquals(Action.AppendChar('a'), result)
} }
@Test @Test
fun `Enter on blank input resolves to null`() { fun `Enter on blank input resolves to null`() {
val result = KeyResolver.resolve(KeyEvent.Enter, InputMode.WorkflowId, "") val result = KeyResolver.resolve(KeyEvent.Enter, InputMode.FILTER, "")
assertNull(result) assertNull(result)
} }
@Test @Test
fun `Enter on non-blank input resolves to SubmitInput`() { fun `Enter on non-blank input resolves to SubmitInput`() {
val result = KeyResolver.resolve(KeyEvent.Enter, InputMode.WorkflowId, "test") val result = KeyResolver.resolve(KeyEvent.Enter, InputMode.FILTER, "test")
assertEquals(Action.SubmitInput, result) assertEquals(Action.SubmitInput, result)
} }
@Test @Test
fun `Escape in input mode resolves to CancelInput`() { fun `Escape in text input mode resolves to CancelInput`() {
val result = KeyResolver.resolve(KeyEvent.Escape, InputMode.WorkflowId, "") val result = KeyResolver.resolve(KeyEvent.Escape, InputMode.FILTER, "")
assertEquals(Action.CancelInput, result) assertEquals(Action.CancelInput, result)
} }
@Test @Test
fun `Backspace in input mode resolves to Backspace action`() { fun `Backspace in text input mode resolves to Backspace action`() {
val result = KeyResolver.resolve(KeyEvent.Backspace, InputMode.WorkflowId, "") val result = KeyResolver.resolve(KeyEvent.Backspace, InputMode.FILTER, "")
assertEquals(Action.Backspace, result) assertEquals(Action.Backspace, result)
} }
@Test @Test
fun `Quit in None mode resolves to Quit`() { fun `Quit in ROUTER mode resolves to Quit`() {
val result = KeyResolver.resolve(KeyEvent.Quit, InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.Quit, InputMode.ROUTER, "")
assertEquals(Action.Quit, result) assertEquals(Action.Quit, result)
} }
@Test @Test
fun `Quit in input mode resolves to null`() { fun `Quit in text input mode resolves to null`() {
val result = KeyResolver.resolve(KeyEvent.Quit, InputMode.WorkflowId, "") val result = KeyResolver.resolve(KeyEvent.Quit, InputMode.FILTER, "")
assertNull(result) assertNull(result)
} }
@Test @Test
fun `Cancel in None mode resolves to CancelSelectedSession`() { fun `Cancel in ROUTER mode resolves to CancelSelectedSession`() {
val result = KeyResolver.resolve(KeyEvent.Cancel, InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.Cancel, InputMode.ROUTER, "")
assertEquals(Action.CancelSelectedSession, result) assertEquals(Action.CancelSelectedSession, result)
} }
@Test @Test
fun `Approve in None mode resolves to ApproveActive`() { fun `Approve in ROUTER mode resolves to ApproveActive`() {
val result = KeyResolver.resolve(KeyEvent.Approve, InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.Approve, InputMode.ROUTER, "")
assertEquals(Action.ApproveActive, result) assertEquals(Action.ApproveActive, result)
} }
@Test @Test
fun `Reject in None mode resolves to RejectActive`() { fun `Reject in ROUTER mode resolves to RejectActive`() {
val result = KeyResolver.resolve(KeyEvent.Reject, InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.Reject, InputMode.ROUTER, "")
assertEquals(Action.RejectActive, result) assertEquals(Action.RejectActive, result)
} }
@Test @Test
fun `Steer in None mode resolves to OpenSteeringPrompt`() { fun `Steer in ROUTER mode resolves to OpenSteeringPrompt`() {
val result = KeyResolver.resolve(KeyEvent.Steer, InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.Steer, InputMode.ROUTER, "")
assertEquals(Action.OpenSteeringPrompt, result) assertEquals(Action.OpenSteeringPrompt, result)
} }
@Test @Test
fun `NavUp in None mode resolves to NavigateUp`() { fun `NavUp in ROUTER mode resolves to NavigateUp`() {
val result = KeyResolver.resolve(KeyEvent.NavUp, InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.NavUp, InputMode.ROUTER, "")
assertEquals(Action.NavigateUp, result) assertEquals(Action.NavigateUp, result)
} }
@Test @Test
fun `NavDown in None mode resolves to NavigateDown`() { fun `NavDown in ROUTER mode resolves to NavigateDown`() {
val result = KeyResolver.resolve(KeyEvent.NavDown, InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.NavDown, InputMode.ROUTER, "")
assertEquals(Action.NavigateDown, result) assertEquals(Action.NavigateDown, result)
} }
@Test @Test
fun `Filter in None mode resolves to OpenFilter`() { fun `Filter in ROUTER mode resolves to OpenFilter`() {
val result = KeyResolver.resolve(KeyEvent.Filter, InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.Filter, InputMode.ROUTER, "")
assertEquals(Action.OpenFilter, result) assertEquals(Action.OpenFilter, result)
} }
@Test @Test
fun `Enter on whitespace-only input resolves to null`() { fun `Enter on whitespace-only input resolves to null`() {
val result = KeyResolver.resolve(KeyEvent.Enter, InputMode.WorkflowId, " ") val result = KeyResolver.resolve(KeyEvent.Enter, InputMode.FILTER, " ")
assertNull(result) assertNull(result)
} }
@Test @Test
fun `CharInput in None mode resolves to null`() { fun `CharInput in ROUTER mode resolves to null`() {
val result = KeyResolver.resolve(KeyEvent.CharInput('x'), InputMode.None, "") val result = KeyResolver.resolve(KeyEvent.CharInput('x'), InputMode.ROUTER, "")
assertNull(result) assertNull(result)
} }
} }
@@ -14,27 +14,27 @@ class TambouiKeyMapperTest {
// --- Ctrl-C always → Quit --- // --- Ctrl-C always → Quit ---
@Test @Test
fun `ctrl-c maps to Quit in InputMode None`() { fun `ctrl-c maps to Quit in InputMode ROUTER`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code) val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code)
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.None)) assertEquals(KeyEvent.Quit, mapKey(event, InputMode.ROUTER))
} }
@Test @Test
fun `ctrl-c maps to Quit in InputMode Filter`() { fun `ctrl-c maps to Quit in InputMode FILTER`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code) val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code)
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.Filter)) assertEquals(KeyEvent.Quit, mapKey(event, InputMode.FILTER))
} }
@Test @Test
fun `ctrl-c maps to Quit in InputMode WorkflowId`() { fun `ctrl-c maps to Quit in InputMode NAVIGATE`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code) val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code)
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.WorkflowId)) assertEquals(KeyEvent.Quit, mapKey(event, InputMode.NAVIGATE))
} }
@Test @Test
fun `ctrl-c maps to Quit in InputMode SteeringNote`() { fun `ctrl-c maps to Quit in InputMode STEER`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code) val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code)
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.SteeringNote)) assertEquals(KeyEvent.Quit, mapKey(event, InputMode.STEER))
} }
// --- Other Ctrl/Alt → null --- // --- Other Ctrl/Alt → null ---
@@ -42,166 +42,166 @@ class TambouiKeyMapperTest {
@Test @Test
fun `ctrl modifier non-ctrlC maps to null`() { fun `ctrl modifier non-ctrlC maps to null`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'a'.code) val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'a'.code)
assertNull(mapKey(event, InputMode.None)) assertNull(mapKey(event, InputMode.ROUTER))
} }
@Test @Test
fun `alt modifier maps to null`() { fun `alt modifier maps to null`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.ALT, 'q'.code) val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.ALT, 'q'.code)
assertNull(mapKey(event, InputMode.None)) assertNull(mapKey(event, InputMode.ROUTER))
} }
// --- KeyCode mappings in InputMode.None --- // --- KeyCode mappings in InputMode.ROUTER ---
@Test @Test
fun `UP maps to NavUp in InputMode None`() { fun `UP maps to NavUp in InputMode ROUTER`() {
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.None)) assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.ROUTER))
} }
@Test @Test
fun `DOWN maps to NavDown in InputMode None`() { fun `DOWN maps to NavDown in InputMode ROUTER`() {
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.None)) assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.ROUTER))
} }
@Test @Test
fun `ENTER maps to Enter in InputMode None`() { fun `ENTER maps to Enter in InputMode ROUTER`() {
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.None)) assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.ROUTER))
} }
@Test @Test
fun `ESCAPE maps to Escape in InputMode None`() { fun `ESCAPE maps to Escape in InputMode ROUTER`() {
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.None)) assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.ROUTER))
} }
@Test @Test
fun `BACKSPACE maps to Backspace in InputMode None`() { fun `BACKSPACE maps to Backspace in InputMode ROUTER`() {
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.None)) assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.ROUTER))
} }
@Test @Test
fun `TAB maps to Filter in InputMode None`() { fun `TAB maps to Filter in InputMode ROUTER`() {
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.None)) assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.ROUTER))
} }
// --- KeyCode mappings in InputMode.Filter --- // --- KeyCode mappings in InputMode.FILTER ---
@Test @Test
fun `UP maps to NavUp in InputMode Filter`() { fun `UP maps to NavUp in InputMode FILTER`() {
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.Filter)) assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.FILTER))
} }
@Test @Test
fun `DOWN maps to NavDown in InputMode Filter`() { fun `DOWN maps to NavDown in InputMode FILTER`() {
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.Filter)) assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.FILTER))
} }
@Test @Test
fun `ENTER maps to Enter in InputMode Filter`() { fun `ENTER maps to Enter in InputMode FILTER`() {
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.Filter)) assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.FILTER))
} }
@Test @Test
fun `ESCAPE maps to Escape in InputMode Filter`() { fun `ESCAPE maps to Escape in InputMode FILTER`() {
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.Filter)) assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.FILTER))
} }
@Test @Test
fun `BACKSPACE maps to Backspace in InputMode Filter`() { fun `BACKSPACE maps to Backspace in InputMode FILTER`() {
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.Filter)) assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.FILTER))
} }
@Test @Test
fun `TAB maps to Filter in InputMode Filter`() { fun `TAB maps to Filter in InputMode FILTER`() {
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.Filter)) assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.FILTER))
} }
// --- Semantic intents in InputMode.None --- // --- Semantic intents in InputMode.ROUTER ---
@Test @Test
fun `q maps to Quit in InputMode None`() { fun `q maps to Quit in InputMode ROUTER`() {
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent.ofChar('q'), InputMode.None)) assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent.ofChar('q'), InputMode.ROUTER))
} }
@Test @Test
fun `n maps to NewSession in InputMode None`() { fun `n maps to NewSession in InputMode ROUTER`() {
assertEquals(KeyEvent.NewSession, mapKey(TambouiKeyEvent.ofChar('n'), InputMode.None)) assertEquals(KeyEvent.NewSession, mapKey(TambouiKeyEvent.ofChar('n'), InputMode.ROUTER))
} }
@Test @Test
fun `c maps to Cancel in InputMode None`() { fun `c maps to Cancel in InputMode ROUTER`() {
assertEquals(KeyEvent.Cancel, mapKey(TambouiKeyEvent.ofChar('c'), InputMode.None)) assertEquals(KeyEvent.Cancel, mapKey(TambouiKeyEvent.ofChar('c'), InputMode.ROUTER))
} }
@Test @Test
fun `a maps to Approve in InputMode None`() { fun `a maps to Approve in InputMode ROUTER`() {
assertEquals(KeyEvent.Approve, mapKey(TambouiKeyEvent.ofChar('a'), InputMode.None)) assertEquals(KeyEvent.Approve, mapKey(TambouiKeyEvent.ofChar('a'), InputMode.ROUTER))
} }
@Test @Test
fun `r maps to Reject in InputMode None`() { fun `r maps to Reject in InputMode ROUTER`() {
assertEquals(KeyEvent.Reject, mapKey(TambouiKeyEvent.ofChar('r'), InputMode.None)) assertEquals(KeyEvent.Reject, mapKey(TambouiKeyEvent.ofChar('r'), InputMode.ROUTER))
} }
@Test @Test
fun `s maps to Steer in InputMode None`() { fun `s maps to Steer in InputMode ROUTER`() {
assertEquals(KeyEvent.Steer, mapKey(TambouiKeyEvent.ofChar('s'), InputMode.None)) assertEquals(KeyEvent.Steer, mapKey(TambouiKeyEvent.ofChar('s'), InputMode.ROUTER))
} }
@Test @Test
fun `slash maps to Filter in InputMode None`() { fun `slash maps to Filter in InputMode ROUTER`() {
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofChar('/'), InputMode.None)) assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofChar('/'), InputMode.ROUTER))
} }
@Test @Test
fun `other char maps to CharInput in InputMode None`() { fun `other char maps to CharInput in InputMode ROUTER`() {
assertEquals(KeyEvent.CharInput('x'), mapKey(TambouiKeyEvent.ofChar('x'), InputMode.None)) assertEquals(KeyEvent.CharInput('x'), mapKey(TambouiKeyEvent.ofChar('x'), InputMode.ROUTER))
} }
// --- Regression: semantic chars map to CharInput in non-None modes --- // --- Regression: semantic chars map to CharInput in non-ROUTER modes ---
@Test @Test
fun `q maps to CharInput in InputMode WorkflowId`() { fun `q maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.WorkflowId)) assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.NAVIGATE))
} }
@Test @Test
fun `n maps to CharInput in InputMode WorkflowId`() { fun `n maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('n'), mapKey(TambouiKeyEvent.ofChar('n'), InputMode.WorkflowId)) assertEquals(KeyEvent.CharInput('n'), mapKey(TambouiKeyEvent.ofChar('n'), InputMode.NAVIGATE))
} }
@Test @Test
fun `c maps to CharInput in InputMode WorkflowId`() { fun `c maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('c'), mapKey(TambouiKeyEvent.ofChar('c'), InputMode.WorkflowId)) assertEquals(KeyEvent.CharInput('c'), mapKey(TambouiKeyEvent.ofChar('c'), InputMode.NAVIGATE))
} }
@Test @Test
fun `a maps to CharInput in InputMode WorkflowId`() { fun `a maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('a'), mapKey(TambouiKeyEvent.ofChar('a'), InputMode.WorkflowId)) assertEquals(KeyEvent.CharInput('a'), mapKey(TambouiKeyEvent.ofChar('a'), InputMode.NAVIGATE))
} }
@Test @Test
fun `r maps to CharInput in InputMode WorkflowId`() { fun `r maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('r'), mapKey(TambouiKeyEvent.ofChar('r'), InputMode.WorkflowId)) assertEquals(KeyEvent.CharInput('r'), mapKey(TambouiKeyEvent.ofChar('r'), InputMode.NAVIGATE))
} }
@Test @Test
fun `s maps to CharInput in InputMode WorkflowId`() { fun `s maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('s'), mapKey(TambouiKeyEvent.ofChar('s'), InputMode.WorkflowId)) assertEquals(KeyEvent.CharInput('s'), mapKey(TambouiKeyEvent.ofChar('s'), InputMode.NAVIGATE))
} }
@Test @Test
fun `slash maps to CharInput in InputMode WorkflowId`() { fun `slash maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('/'), mapKey(TambouiKeyEvent.ofChar('/'), InputMode.WorkflowId)) assertEquals(KeyEvent.CharInput('/'), mapKey(TambouiKeyEvent.ofChar('/'), InputMode.NAVIGATE))
} }
@Test @Test
fun `q maps to CharInput in InputMode SteeringNote`() { fun `q maps to CharInput in InputMode STEER`() {
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.SteeringNote)) assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.STEER))
} }
@Test @Test
fun `q maps to CharInput in InputMode Filter`() { fun `q maps to CharInput in InputMode FILTER`() {
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.Filter)) assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.FILTER))
} }
// --- ISO control chars in CHAR event → null --- // --- ISO control chars in CHAR event → null ---
@@ -209,12 +209,12 @@ class TambouiKeyMapperTest {
@Test @Test
fun `ESC codepoint as CHAR event maps to null`() { fun `ESC codepoint as CHAR event maps to null`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x1B) val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x1B)
assertNull(mapKey(event, InputMode.None)) assertNull(mapKey(event, InputMode.ROUTER))
} }
@Test @Test
fun `NUL codepoint as CHAR event maps to null`() { fun `NUL codepoint as CHAR event maps to null`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x00) val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x00)
assertNull(mapKey(event, InputMode.None)) assertNull(mapKey(event, InputMode.ROUTER))
} }
} }
@@ -29,7 +29,7 @@ class ApprovalReducerTest {
private fun reduce( private fun reduce(
approval: ApprovalState = ApprovalState(), approval: ApprovalState = ApprovalState(),
inputMode: InputMode = InputMode.None, inputMode: InputMode = InputMode.ROUTER,
action: Action, action: Action,
) = ApprovalReducer.reduce(approval, inputMode, action) ) = ApprovalReducer.reduce(approval, inputMode, action)
@@ -72,7 +72,7 @@ class ApprovalReducerTest {
fun `SubmitInput in SteeringNote mode clears active approval`() { fun `SubmitInput in SteeringNote mode clears active approval`() {
val (state, effects) = reduce( val (state, effects) = reduce(
approval = ApprovalState(active = activeInfo), approval = ApprovalState(active = activeInfo),
inputMode = InputMode.SteeringNote, inputMode = InputMode.STEER,
action = Action.SubmitInput, action = Action.SubmitInput,
) )
assertNull(state.active) assertNull(state.active)
@@ -83,7 +83,7 @@ class ApprovalReducerTest {
fun `SubmitInput in non-SteeringNote mode is no-op`() { fun `SubmitInput in non-SteeringNote mode is no-op`() {
val (state, _) = reduce( val (state, _) = reduce(
approval = ApprovalState(active = activeInfo), approval = ApprovalState(active = activeInfo),
inputMode = InputMode.WorkflowId, inputMode = InputMode.NAVIGATE,
action = Action.SubmitInput, action = Action.SubmitInput,
) )
assertEquals(activeInfo, state.active) assertEquals(activeInfo, state.active)
@@ -4,15 +4,15 @@ import com.correx.apps.server.protocol.ApprovalDecision
import com.correx.apps.server.protocol.ClientMessage import com.correx.apps.server.protocol.ClientMessage
import com.correx.apps.tui.input.Action import com.correx.apps.tui.input.Action
import com.correx.apps.tui.state.ApprovalInfo import com.correx.apps.tui.state.ApprovalInfo
import com.correx.apps.tui.state.ApprovalState
import com.correx.apps.tui.state.InputMode import com.correx.apps.tui.state.InputMode
import com.correx.apps.tui.state.InputState import com.correx.apps.tui.state.TuiState
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class InputReducerTest { class InputReducerTest {
private val noApproval: ApprovalInfo? = null
private val activeApproval = ApprovalInfo( private val activeApproval = ApprovalInfo(
requestId = "req-1", requestId = "req-1",
sessionId = "sess-1", sessionId = "sess-1",
@@ -22,68 +22,83 @@ class InputReducerTest {
preview = null, preview = null,
) )
private fun reduce(input: InputState = InputState(), approval: ApprovalInfo? = noApproval, action: Action) = private fun reduce(
InputReducer.reduce(input, activeApproval = approval, action = action) state: TuiState = TuiState(),
action: Action,
) = InputReducer.reduce(state, action)
@Test @Test
fun `OpenNewSessionPrompt sets mode to WorkflowId with empty text`() { fun `OpenNewSessionPrompt sets mode to ROUTER with empty buffer`() {
val (state, effects) = reduce(action = Action.OpenNewSessionPrompt) val (state, effects) = reduce(action = Action.OpenNewSessionPrompt)
assertEquals(InputMode.WorkflowId, state.mode) assertEquals(InputMode.ROUTER, state.inputMode)
assertEquals("", state.text) assertEquals("", state.inputBuffer)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
} }
@Test @Test
fun `OpenSteeringPrompt sets mode to SteeringNote when approval active`() { fun `OpenSteeringPrompt sets mode to STEER when approval active`() {
val (state, effects) = reduce(approval = activeApproval, action = Action.OpenSteeringPrompt) val initial = TuiState(approval = ApprovalState(active = activeApproval))
assertEquals(InputMode.SteeringNote, state.mode) val (state, effects) = reduce(state = initial, action = Action.OpenSteeringPrompt)
assertEquals("", state.text) assertEquals(InputMode.STEER, state.inputMode)
assertEquals("", state.inputBuffer)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
} }
@Test @Test
fun `OpenSteeringPrompt is no-op when no active approval`() { fun `OpenSteeringPrompt is no-op when no active approval`() {
val initial = InputState() val initial = TuiState()
val (state, effects) = reduce(input = initial, approval = null, action = Action.OpenSteeringPrompt) val (state, effects) = reduce(state = initial, action = Action.OpenSteeringPrompt)
assertEquals(initial, state) assertEquals(initial, state)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
} }
@Test @Test
fun `AppendChar appends character to text`() { fun `AppendChar appends character to buffer`() {
val (state, effects) = reduce(input = InputState(InputMode.WorkflowId, "ab"), action = Action.AppendChar('c')) val (state, effects) = reduce(
assertEquals("abc", state.text) state = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "ab"),
action = Action.AppendChar('c'),
)
assertEquals("abc", state.inputBuffer)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
} }
@Test @Test
fun `Backspace drops last character`() { fun `Backspace drops last character`() {
val (state, effects) = reduce(input = InputState(InputMode.WorkflowId, "abc"), action = Action.Backspace)
assertEquals("ab", state.text)
assertTrue(effects.isEmpty())
}
@Test
fun `Backspace on empty text stays empty`() {
val (state, _) = reduce(input = InputState(InputMode.WorkflowId, ""), action = Action.Backspace)
assertEquals("", state.text)
}
@Test
fun `CancelInput resets to None with empty text`() {
val (state, effects) = reduce(input = InputState(InputMode.WorkflowId, "hello"), action = Action.CancelInput)
assertEquals(InputMode.None, state.mode)
assertEquals("", state.text)
assertTrue(effects.isEmpty())
}
@Test
fun `SubmitInput in WorkflowId mode emits StartSession effect`() {
val (state, effects) = reduce( val (state, effects) = reduce(
input = InputState(InputMode.WorkflowId, "my-workflow"), state = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "abc"),
action = Action.Backspace,
)
assertEquals("ab", state.inputBuffer)
assertTrue(effects.isEmpty())
}
@Test
fun `Backspace on empty buffer stays empty`() {
val (state, _) = reduce(
state = TuiState(inputMode = InputMode.ROUTER, inputBuffer = ""),
action = Action.Backspace,
)
assertEquals("", state.inputBuffer)
}
@Test
fun `CancelInput resets to ROUTER mode with empty buffer`() {
val (state, effects) = reduce(
state = TuiState(inputMode = InputMode.FILTER, inputBuffer = "hello"),
action = Action.CancelInput,
)
assertEquals(InputMode.ROUTER, state.inputMode)
assertEquals("", state.inputBuffer)
assertTrue(effects.isEmpty())
}
@Test
fun `SubmitInput in ROUTER mode emits StartSession effect`() {
val (state, effects) = reduce(
state = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "my-workflow"),
action = Action.SubmitInput, action = Action.SubmitInput,
) )
assertEquals(InputMode.None, state.mode) assertEquals(InputMode.ROUTER, state.inputMode)
assertEquals(1, effects.size) assertEquals(1, effects.size)
val effect = effects[0] as Effect.SendWs val effect = effects[0] as Effect.SendWs
val msg = effect.message as ClientMessage.StartSession val msg = effect.message as ClientMessage.StartSession
@@ -91,23 +106,26 @@ class InputReducerTest {
} }
@Test @Test
fun `SubmitInput in WorkflowId mode with blank text emits no effect`() { fun `SubmitInput in ROUTER mode with blank text emits no effect`() {
val (state, effects) = reduce( val (state, effects) = reduce(
input = InputState(InputMode.WorkflowId, " "), state = TuiState(inputMode = InputMode.ROUTER, inputBuffer = " "),
action = Action.SubmitInput, action = Action.SubmitInput,
) )
assertEquals(InputMode.None, state.mode) assertEquals(InputMode.ROUTER, state.inputMode)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
} }
@Test @Test
fun `SubmitInput in SteeringNote mode with active approval emits ApprovalResponse STEER`() { fun `SubmitInput in STEER mode with active approval emits ApprovalResponse STEER`() {
val (state, effects) = reduce( val (state, effects) = reduce(
input = InputState(InputMode.SteeringNote, "steer this way"), state = TuiState(
approval = activeApproval, inputMode = InputMode.STEER,
inputBuffer = "steer this way",
approval = ApprovalState(active = activeApproval),
),
action = Action.SubmitInput, action = Action.SubmitInput,
) )
assertEquals(InputMode.None, state.mode) assertEquals(InputMode.ROUTER, state.inputMode)
assertEquals(1, effects.size) assertEquals(1, effects.size)
val effect = effects[0] as Effect.SendWs val effect = effects[0] as Effect.SendWs
val msg = effect.message as ClientMessage.ApprovalResponse val msg = effect.message as ClientMessage.ApprovalResponse
@@ -117,12 +135,12 @@ class InputReducerTest {
} }
@Test @Test
fun `SubmitInput in Filter mode clears input with no effects`() { fun `SubmitInput in FILTER mode clears input with no effects`() {
val (state, effects) = reduce( val (state, effects) = reduce(
input = InputState(InputMode.Filter, "myfilter"), state = TuiState(inputMode = InputMode.FILTER, inputBuffer = "myfilter"),
action = Action.SubmitInput, action = Action.SubmitInput,
) )
assertEquals(InputMode.None, state.mode) assertEquals(InputMode.ROUTER, state.inputMode)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
} }
} }
@@ -3,7 +3,6 @@ package com.correx.apps.tui.reducer
import com.correx.apps.server.protocol.ClientMessage import com.correx.apps.server.protocol.ClientMessage
import com.correx.apps.tui.input.Action import com.correx.apps.tui.input.Action
import com.correx.apps.tui.state.InputMode import com.correx.apps.tui.state.InputMode
import com.correx.apps.tui.state.InputState
import com.correx.apps.tui.state.TuiState import com.correx.apps.tui.state.TuiState
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
@@ -14,11 +13,11 @@ class RootReducerTest {
private val fixedClock: () -> Long = { 1000L } private val fixedClock: () -> Long = { 1000L }
@Test @Test
fun `SubmitInput WorkflowId emits StartSession effect and resets input`() { fun `SubmitInput in ROUTER mode emits StartSession effect and resets buffer`() {
val initial = TuiState(input = InputState(InputMode.WorkflowId, "my-workflow")) val initial = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "my-workflow")
val (state, effects) = RootReducer.reduce(initial, Action.SubmitInput, fixedClock) val (state, effects) = RootReducer.reduce(initial, Action.SubmitInput, fixedClock)
assertEquals(InputMode.None, state.input.mode) assertEquals(InputMode.ROUTER, state.inputMode)
assertEquals("", state.input.text) assertEquals("", state.inputBuffer)
val wsEffects = effects.filterIsInstance<Effect.SendWs>() val wsEffects = effects.filterIsInstance<Effect.SendWs>()
assertEquals(1, wsEffects.size) assertEquals(1, wsEffects.size)
val msg = wsEffects[0].message as ClientMessage.StartSession val msg = wsEffects[0].message as ClientMessage.StartSession
@@ -31,7 +30,8 @@ class RootReducerTest {
val (state, effects) = RootReducer.reduce(initial, Action.Connected, fixedClock) val (state, effects) = RootReducer.reduce(initial, Action.Connected, fixedClock)
assertTrue(state.connection.connected) assertTrue(state.connection.connected)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
assertEquals(initial.input, state.input) assertEquals(initial.inputMode, state.inputMode)
assertEquals(initial.inputBuffer, state.inputBuffer)
assertEquals(initial.sessions, state.sessions) assertEquals(initial.sessions, state.sessions)
assertEquals(initial.approval, state.approval) assertEquals(initial.approval, state.approval)
assertEquals(initial.provider, state.provider) assertEquals(initial.provider, state.provider)
@@ -51,15 +51,15 @@ class RootReducerTest {
) )
val (state2, _) = RootReducer.reduce(state1, Action.ServerEventReceived(startMsg2), fixedClock) val (state2, _) = RootReducer.reduce(state1, Action.ServerEventReceived(startMsg2), fixedClock)
// selected is s1 (first); navigating down should move to s2 // selected is s1 (first); navigating down should move to s2
val (state3, _) = RootReducer.reduce(state2, Action.NavigateDown, fixedClock) val (state3, _) = RootReducer.reduce(state2.copy(inputMode = InputMode.NAVIGATE), Action.NavigateDown, fixedClock)
assertEquals("s2", state3.sessions.selectedId) assertEquals("s2", state3.sessions.selectedId)
} }
@Test @Test
fun `AppendChar followed by Backspace returns to original text`() { fun `AppendChar followed by Backspace returns to original buffer`() {
val initial = TuiState(input = InputState(InputMode.WorkflowId, "ab")) val initial = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "ab")
val (s1, _) = RootReducer.reduce(initial, Action.AppendChar('c'), fixedClock) val (s1, _) = RootReducer.reduce(initial, Action.AppendChar('c'), fixedClock)
val (s2, _) = RootReducer.reduce(s1, Action.Backspace, fixedClock) val (s2, _) = RootReducer.reduce(s1, Action.Backspace, fixedClock)
assertEquals("ab", s2.input.text) assertEquals("ab", s2.inputBuffer)
} }
} }
@@ -27,7 +27,7 @@ class SessionsReducerTest {
private fun reduce( private fun reduce(
sessions: SessionsState = SessionsState(), sessions: SessionsState = SessionsState(),
inputMode: InputMode = InputMode.None, inputMode: InputMode = InputMode.ROUTER,
inputText: String = "", inputText: String = "",
action: Action, action: Action,
) = SessionsReducer.reduce(sessions, inputMode, inputText, action, fixedClock) ) = SessionsReducer.reduce(sessions, inputMode, inputText, action, fixedClock)
@@ -35,7 +35,7 @@ class SessionsReducerTest {
@Test @Test
fun `NavigateUp wraps from top to bottom`() { fun `NavigateUp wraps from top to bottom`() {
val s = SessionsState(sessions = listOf(session("a"), session("b"), session("c")), selectedId = "a") val s = SessionsState(sessions = listOf(session("a"), session("b"), session("c")), selectedId = "a")
val (state, effects) = reduce(sessions = s, action = Action.NavigateUp) val (state, effects) = reduce(sessions = s, inputMode = InputMode.NAVIGATE, action = Action.NavigateUp)
assertEquals("c", state.selectedId) assertEquals("c", state.selectedId)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
} }
@@ -43,14 +43,14 @@ class SessionsReducerTest {
@Test @Test
fun `NavigateDown advances selection`() { fun `NavigateDown advances selection`() {
val s = SessionsState(sessions = listOf(session("a"), session("b")), selectedId = "a") val s = SessionsState(sessions = listOf(session("a"), session("b")), selectedId = "a")
val (state, _) = reduce(sessions = s, action = Action.NavigateDown) val (state, _) = reduce(sessions = s, inputMode = InputMode.NAVIGATE, action = Action.NavigateDown)
assertEquals("b", state.selectedId) assertEquals("b", state.selectedId)
} }
@Test @Test
fun `NavigateDown wraps from bottom to top`() { fun `NavigateDown wraps from bottom to top`() {
val s = SessionsState(sessions = listOf(session("a"), session("b")), selectedId = "b") val s = SessionsState(sessions = listOf(session("a"), session("b")), selectedId = "b")
val (state, _) = reduce(sessions = s, action = Action.NavigateDown) val (state, _) = reduce(sessions = s, inputMode = InputMode.NAVIGATE, action = Action.NavigateDown)
assertEquals("a", state.selectedId) assertEquals("a", state.selectedId)
} }
@@ -58,7 +58,7 @@ class SessionsReducerTest {
fun `SubmitInput in Filter mode copies inputText into filter`() { fun `SubmitInput in Filter mode copies inputText into filter`() {
val s = SessionsState() val s = SessionsState()
val (state, effects) = reduce( val (state, effects) = reduce(
sessions = s, inputMode = InputMode.Filter, inputText = "myfilter", action = Action.SubmitInput, sessions = s, inputMode = InputMode.FILTER, inputText = "myfilter", action = Action.SubmitInput,
) )
assertEquals("myfilter", state.filter) assertEquals("myfilter", state.filter)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
@@ -68,7 +68,7 @@ class SessionsReducerTest {
fun `SubmitInput in Filter mode with empty text clears filter`() { fun `SubmitInput in Filter mode with empty text clears filter`() {
val s = SessionsState(filter = "old") val s = SessionsState(filter = "old")
val (state, effects) = reduce( val (state, effects) = reduce(
sessions = s, inputMode = InputMode.Filter, inputText = "", action = Action.SubmitInput, sessions = s, inputMode = InputMode.FILTER, inputText = "", action = Action.SubmitInput,
) )
assertEquals("", state.filter) assertEquals("", state.filter)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
@@ -77,7 +77,7 @@ class SessionsReducerTest {
@Test @Test
fun `CancelInput in Filter mode clears filter`() { fun `CancelInput in Filter mode clears filter`() {
val s = SessionsState(filter = "something") val s = SessionsState(filter = "something")
val (state, effects) = reduce(sessions = s, inputMode = InputMode.Filter, action = Action.CancelInput) val (state, effects) = reduce(sessions = s, inputMode = InputMode.FILTER, action = Action.CancelInput)
assertEquals("", state.filter) assertEquals("", state.filter)
assertTrue(effects.isEmpty()) assertTrue(effects.isEmpty())
} }
@@ -85,7 +85,7 @@ class SessionsReducerTest {
@Test @Test
fun `CancelInput in non-Filter mode is no-op`() { fun `CancelInput in non-Filter mode is no-op`() {
val s = SessionsState(filter = "something") val s = SessionsState(filter = "something")
val (state, _) = reduce(sessions = s, inputMode = InputMode.WorkflowId, action = Action.CancelInput) val (state, _) = reduce(sessions = s, inputMode = InputMode.ROUTER, action = Action.CancelInput)
assertEquals("something", state.filter) assertEquals("something", state.filter)
} }
@@ -96,7 +96,8 @@ class SessionsReducerTest {
assertEquals(1, state.sessions.size) assertEquals(1, state.sessions.size)
assertEquals("s1", state.sessions[0].id) assertEquals("s1", state.sessions[0].id)
assertEquals("s1", state.selectedId) assertEquals("s1", state.selectedId)
assertTrue(effects.isEmpty()) assertEquals(1, effects.size)
assertTrue(effects[0] is Effect.ConnectSession)
} }
@Test @Test