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 {
@Test
fun `n in None mode resolves to OpenNewSessionPrompt`() {
val result = KeyResolver.resolve(KeyEvent.NewSession, InputMode.None, "")
fun `n in ROUTER mode resolves to OpenNewSessionPrompt`() {
val result = KeyResolver.resolve(KeyEvent.NewSession, InputMode.ROUTER, "")
assertEquals(Action.OpenNewSessionPrompt, result)
}
@Test
fun `command letters in input mode resolve to null`() {
val result = KeyResolver.resolve(KeyEvent.NewSession, InputMode.WorkflowId, "")
fun `command letters in text input mode resolve to null`() {
val result = KeyResolver.resolve(KeyEvent.NewSession, InputMode.FILTER, "")
assertNull(result)
}
@Test
fun `CharInput in input mode resolves to AppendChar`() {
val result = KeyResolver.resolve(KeyEvent.CharInput('a'), InputMode.WorkflowId, "")
fun `CharInput in text input mode resolves to AppendChar`() {
val result = KeyResolver.resolve(KeyEvent.CharInput('a'), InputMode.FILTER, "")
assertEquals(Action.AppendChar('a'), result)
}
@Test
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)
}
@Test
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)
}
@Test
fun `Escape in input mode resolves to CancelInput`() {
val result = KeyResolver.resolve(KeyEvent.Escape, InputMode.WorkflowId, "")
fun `Escape in text input mode resolves to CancelInput`() {
val result = KeyResolver.resolve(KeyEvent.Escape, InputMode.FILTER, "")
assertEquals(Action.CancelInput, result)
}
@Test
fun `Backspace in input mode resolves to Backspace action`() {
val result = KeyResolver.resolve(KeyEvent.Backspace, InputMode.WorkflowId, "")
fun `Backspace in text input mode resolves to Backspace action`() {
val result = KeyResolver.resolve(KeyEvent.Backspace, InputMode.FILTER, "")
assertEquals(Action.Backspace, result)
}
@Test
fun `Quit in None mode resolves to Quit`() {
val result = KeyResolver.resolve(KeyEvent.Quit, InputMode.None, "")
fun `Quit in ROUTER mode resolves to Quit`() {
val result = KeyResolver.resolve(KeyEvent.Quit, InputMode.ROUTER, "")
assertEquals(Action.Quit, result)
}
@Test
fun `Quit in input mode resolves to null`() {
val result = KeyResolver.resolve(KeyEvent.Quit, InputMode.WorkflowId, "")
fun `Quit in text input mode resolves to null`() {
val result = KeyResolver.resolve(KeyEvent.Quit, InputMode.FILTER, "")
assertNull(result)
}
@Test
fun `Cancel in None mode resolves to CancelSelectedSession`() {
val result = KeyResolver.resolve(KeyEvent.Cancel, InputMode.None, "")
fun `Cancel in ROUTER mode resolves to CancelSelectedSession`() {
val result = KeyResolver.resolve(KeyEvent.Cancel, InputMode.ROUTER, "")
assertEquals(Action.CancelSelectedSession, result)
}
@Test
fun `Approve in None mode resolves to ApproveActive`() {
val result = KeyResolver.resolve(KeyEvent.Approve, InputMode.None, "")
fun `Approve in ROUTER mode resolves to ApproveActive`() {
val result = KeyResolver.resolve(KeyEvent.Approve, InputMode.ROUTER, "")
assertEquals(Action.ApproveActive, result)
}
@Test
fun `Reject in None mode resolves to RejectActive`() {
val result = KeyResolver.resolve(KeyEvent.Reject, InputMode.None, "")
fun `Reject in ROUTER mode resolves to RejectActive`() {
val result = KeyResolver.resolve(KeyEvent.Reject, InputMode.ROUTER, "")
assertEquals(Action.RejectActive, result)
}
@Test
fun `Steer in None mode resolves to OpenSteeringPrompt`() {
val result = KeyResolver.resolve(KeyEvent.Steer, InputMode.None, "")
fun `Steer in ROUTER mode resolves to OpenSteeringPrompt`() {
val result = KeyResolver.resolve(KeyEvent.Steer, InputMode.ROUTER, "")
assertEquals(Action.OpenSteeringPrompt, result)
}
@Test
fun `NavUp in None mode resolves to NavigateUp`() {
val result = KeyResolver.resolve(KeyEvent.NavUp, InputMode.None, "")
fun `NavUp in ROUTER mode resolves to NavigateUp`() {
val result = KeyResolver.resolve(KeyEvent.NavUp, InputMode.ROUTER, "")
assertEquals(Action.NavigateUp, result)
}
@Test
fun `NavDown in None mode resolves to NavigateDown`() {
val result = KeyResolver.resolve(KeyEvent.NavDown, InputMode.None, "")
fun `NavDown in ROUTER mode resolves to NavigateDown`() {
val result = KeyResolver.resolve(KeyEvent.NavDown, InputMode.ROUTER, "")
assertEquals(Action.NavigateDown, result)
}
@Test
fun `Filter in None mode resolves to OpenFilter`() {
val result = KeyResolver.resolve(KeyEvent.Filter, InputMode.None, "")
fun `Filter in ROUTER mode resolves to OpenFilter`() {
val result = KeyResolver.resolve(KeyEvent.Filter, InputMode.ROUTER, "")
assertEquals(Action.OpenFilter, result)
}
@Test
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)
}
@Test
fun `CharInput in None mode resolves to null`() {
val result = KeyResolver.resolve(KeyEvent.CharInput('x'), InputMode.None, "")
fun `CharInput in ROUTER mode resolves to null`() {
val result = KeyResolver.resolve(KeyEvent.CharInput('x'), InputMode.ROUTER, "")
assertNull(result)
}
}
@@ -14,27 +14,27 @@ class TambouiKeyMapperTest {
// --- Ctrl-C always → Quit ---
@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)
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.None))
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.ROUTER))
}
@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)
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.Filter))
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.FILTER))
}
@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)
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.WorkflowId))
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.NAVIGATE))
}
@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)
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.SteeringNote))
assertEquals(KeyEvent.Quit, mapKey(event, InputMode.STEER))
}
// --- Other Ctrl/Alt → null ---
@@ -42,166 +42,166 @@ class TambouiKeyMapperTest {
@Test
fun `ctrl modifier non-ctrlC maps to null`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'a'.code)
assertNull(mapKey(event, InputMode.None))
assertNull(mapKey(event, InputMode.ROUTER))
}
@Test
fun `alt modifier maps to null`() {
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
fun `UP maps to NavUp in InputMode None`() {
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.None))
fun `UP maps to NavUp in InputMode ROUTER`() {
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.ROUTER))
}
@Test
fun `DOWN maps to NavDown in InputMode None`() {
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.None))
fun `DOWN maps to NavDown in InputMode ROUTER`() {
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.ROUTER))
}
@Test
fun `ENTER maps to Enter in InputMode None`() {
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.None))
fun `ENTER maps to Enter in InputMode ROUTER`() {
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.ROUTER))
}
@Test
fun `ESCAPE maps to Escape in InputMode None`() {
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.None))
fun `ESCAPE maps to Escape in InputMode ROUTER`() {
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.ROUTER))
}
@Test
fun `BACKSPACE maps to Backspace in InputMode None`() {
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.None))
fun `BACKSPACE maps to Backspace in InputMode ROUTER`() {
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.ROUTER))
}
@Test
fun `TAB maps to Filter in InputMode None`() {
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.None))
fun `TAB maps to Filter in InputMode ROUTER`() {
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.ROUTER))
}
// --- KeyCode mappings in InputMode.Filter ---
// --- KeyCode mappings in InputMode.FILTER ---
@Test
fun `UP maps to NavUp in InputMode Filter`() {
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.Filter))
fun `UP maps to NavUp in InputMode FILTER`() {
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.FILTER))
}
@Test
fun `DOWN maps to NavDown in InputMode Filter`() {
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.Filter))
fun `DOWN maps to NavDown in InputMode FILTER`() {
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.FILTER))
}
@Test
fun `ENTER maps to Enter in InputMode Filter`() {
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.Filter))
fun `ENTER maps to Enter in InputMode FILTER`() {
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.FILTER))
}
@Test
fun `ESCAPE maps to Escape in InputMode Filter`() {
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.Filter))
fun `ESCAPE maps to Escape in InputMode FILTER`() {
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.FILTER))
}
@Test
fun `BACKSPACE maps to Backspace in InputMode Filter`() {
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.Filter))
fun `BACKSPACE maps to Backspace in InputMode FILTER`() {
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.FILTER))
}
@Test
fun `TAB maps to Filter in InputMode Filter`() {
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.Filter))
fun `TAB maps to Filter in InputMode FILTER`() {
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.FILTER))
}
// --- Semantic intents in InputMode.None ---
// --- Semantic intents in InputMode.ROUTER ---
@Test
fun `q maps to Quit in InputMode None`() {
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent.ofChar('q'), InputMode.None))
fun `q maps to Quit in InputMode ROUTER`() {
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent.ofChar('q'), InputMode.ROUTER))
}
@Test
fun `n maps to NewSession in InputMode None`() {
assertEquals(KeyEvent.NewSession, mapKey(TambouiKeyEvent.ofChar('n'), InputMode.None))
fun `n maps to NewSession in InputMode ROUTER`() {
assertEquals(KeyEvent.NewSession, mapKey(TambouiKeyEvent.ofChar('n'), InputMode.ROUTER))
}
@Test
fun `c maps to Cancel in InputMode None`() {
assertEquals(KeyEvent.Cancel, mapKey(TambouiKeyEvent.ofChar('c'), InputMode.None))
fun `c maps to Cancel in InputMode ROUTER`() {
assertEquals(KeyEvent.Cancel, mapKey(TambouiKeyEvent.ofChar('c'), InputMode.ROUTER))
}
@Test
fun `a maps to Approve in InputMode None`() {
assertEquals(KeyEvent.Approve, mapKey(TambouiKeyEvent.ofChar('a'), InputMode.None))
fun `a maps to Approve in InputMode ROUTER`() {
assertEquals(KeyEvent.Approve, mapKey(TambouiKeyEvent.ofChar('a'), InputMode.ROUTER))
}
@Test
fun `r maps to Reject in InputMode None`() {
assertEquals(KeyEvent.Reject, mapKey(TambouiKeyEvent.ofChar('r'), InputMode.None))
fun `r maps to Reject in InputMode ROUTER`() {
assertEquals(KeyEvent.Reject, mapKey(TambouiKeyEvent.ofChar('r'), InputMode.ROUTER))
}
@Test
fun `s maps to Steer in InputMode None`() {
assertEquals(KeyEvent.Steer, mapKey(TambouiKeyEvent.ofChar('s'), InputMode.None))
fun `s maps to Steer in InputMode ROUTER`() {
assertEquals(KeyEvent.Steer, mapKey(TambouiKeyEvent.ofChar('s'), InputMode.ROUTER))
}
@Test
fun `slash maps to Filter in InputMode None`() {
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofChar('/'), InputMode.None))
fun `slash maps to Filter in InputMode ROUTER`() {
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofChar('/'), InputMode.ROUTER))
}
@Test
fun `other char maps to CharInput in InputMode None`() {
assertEquals(KeyEvent.CharInput('x'), mapKey(TambouiKeyEvent.ofChar('x'), InputMode.None))
fun `other char maps to CharInput in InputMode ROUTER`() {
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
fun `q maps to CharInput in InputMode WorkflowId`() {
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.WorkflowId))
fun `q maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.NAVIGATE))
}
@Test
fun `n maps to CharInput in InputMode WorkflowId`() {
assertEquals(KeyEvent.CharInput('n'), mapKey(TambouiKeyEvent.ofChar('n'), InputMode.WorkflowId))
fun `n maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('n'), mapKey(TambouiKeyEvent.ofChar('n'), InputMode.NAVIGATE))
}
@Test
fun `c maps to CharInput in InputMode WorkflowId`() {
assertEquals(KeyEvent.CharInput('c'), mapKey(TambouiKeyEvent.ofChar('c'), InputMode.WorkflowId))
fun `c maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('c'), mapKey(TambouiKeyEvent.ofChar('c'), InputMode.NAVIGATE))
}
@Test
fun `a maps to CharInput in InputMode WorkflowId`() {
assertEquals(KeyEvent.CharInput('a'), mapKey(TambouiKeyEvent.ofChar('a'), InputMode.WorkflowId))
fun `a maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('a'), mapKey(TambouiKeyEvent.ofChar('a'), InputMode.NAVIGATE))
}
@Test
fun `r maps to CharInput in InputMode WorkflowId`() {
assertEquals(KeyEvent.CharInput('r'), mapKey(TambouiKeyEvent.ofChar('r'), InputMode.WorkflowId))
fun `r maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('r'), mapKey(TambouiKeyEvent.ofChar('r'), InputMode.NAVIGATE))
}
@Test
fun `s maps to CharInput in InputMode WorkflowId`() {
assertEquals(KeyEvent.CharInput('s'), mapKey(TambouiKeyEvent.ofChar('s'), InputMode.WorkflowId))
fun `s maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('s'), mapKey(TambouiKeyEvent.ofChar('s'), InputMode.NAVIGATE))
}
@Test
fun `slash maps to CharInput in InputMode WorkflowId`() {
assertEquals(KeyEvent.CharInput('/'), mapKey(TambouiKeyEvent.ofChar('/'), InputMode.WorkflowId))
fun `slash maps to CharInput in InputMode NAVIGATE`() {
assertEquals(KeyEvent.CharInput('/'), mapKey(TambouiKeyEvent.ofChar('/'), InputMode.NAVIGATE))
}
@Test
fun `q maps to CharInput in InputMode SteeringNote`() {
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.SteeringNote))
fun `q maps to CharInput in InputMode STEER`() {
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.STEER))
}
@Test
fun `q maps to CharInput in InputMode Filter`() {
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.Filter))
fun `q maps to CharInput in InputMode FILTER`() {
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.FILTER))
}
// --- ISO control chars in CHAR event → null ---
@@ -209,12 +209,12 @@ class TambouiKeyMapperTest {
@Test
fun `ESC codepoint as CHAR event maps to null`() {
val event = TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x1B)
assertNull(mapKey(event, InputMode.None))
assertNull(mapKey(event, InputMode.ROUTER))
}
@Test
fun `NUL codepoint as CHAR event maps to null`() {
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(
approval: ApprovalState = ApprovalState(),
inputMode: InputMode = InputMode.None,
inputMode: InputMode = InputMode.ROUTER,
action: Action,
) = ApprovalReducer.reduce(approval, inputMode, action)
@@ -72,7 +72,7 @@ class ApprovalReducerTest {
fun `SubmitInput in SteeringNote mode clears active approval`() {
val (state, effects) = reduce(
approval = ApprovalState(active = activeInfo),
inputMode = InputMode.SteeringNote,
inputMode = InputMode.STEER,
action = Action.SubmitInput,
)
assertNull(state.active)
@@ -83,7 +83,7 @@ class ApprovalReducerTest {
fun `SubmitInput in non-SteeringNote mode is no-op`() {
val (state, _) = reduce(
approval = ApprovalState(active = activeInfo),
inputMode = InputMode.WorkflowId,
inputMode = InputMode.NAVIGATE,
action = Action.SubmitInput,
)
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.tui.input.Action
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.InputState
import com.correx.apps.tui.state.TuiState
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class InputReducerTest {
private val noApproval: ApprovalInfo? = null
private val activeApproval = ApprovalInfo(
requestId = "req-1",
sessionId = "sess-1",
@@ -22,68 +22,83 @@ class InputReducerTest {
preview = null,
)
private fun reduce(input: InputState = InputState(), approval: ApprovalInfo? = noApproval, action: Action) =
InputReducer.reduce(input, activeApproval = approval, action = action)
private fun reduce(
state: TuiState = TuiState(),
action: Action,
) = InputReducer.reduce(state, action)
@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)
assertEquals(InputMode.WorkflowId, state.mode)
assertEquals("", state.text)
assertEquals(InputMode.ROUTER, state.inputMode)
assertEquals("", state.inputBuffer)
assertTrue(effects.isEmpty())
}
@Test
fun `OpenSteeringPrompt sets mode to SteeringNote when approval active`() {
val (state, effects) = reduce(approval = activeApproval, action = Action.OpenSteeringPrompt)
assertEquals(InputMode.SteeringNote, state.mode)
assertEquals("", state.text)
fun `OpenSteeringPrompt sets mode to STEER when approval active`() {
val initial = TuiState(approval = ApprovalState(active = activeApproval))
val (state, effects) = reduce(state = initial, action = Action.OpenSteeringPrompt)
assertEquals(InputMode.STEER, state.inputMode)
assertEquals("", state.inputBuffer)
assertTrue(effects.isEmpty())
}
@Test
fun `OpenSteeringPrompt is no-op when no active approval`() {
val initial = InputState()
val (state, effects) = reduce(input = initial, approval = null, action = Action.OpenSteeringPrompt)
val initial = TuiState()
val (state, effects) = reduce(state = initial, action = Action.OpenSteeringPrompt)
assertEquals(initial, state)
assertTrue(effects.isEmpty())
}
@Test
fun `AppendChar appends character to text`() {
val (state, effects) = reduce(input = InputState(InputMode.WorkflowId, "ab"), action = Action.AppendChar('c'))
assertEquals("abc", state.text)
fun `AppendChar appends character to buffer`() {
val (state, effects) = reduce(
state = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "ab"),
action = Action.AppendChar('c'),
)
assertEquals("abc", state.inputBuffer)
assertTrue(effects.isEmpty())
}
@Test
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(
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,
)
assertEquals(InputMode.None, state.mode)
assertEquals(InputMode.ROUTER, state.inputMode)
assertEquals(1, effects.size)
val effect = effects[0] as Effect.SendWs
val msg = effect.message as ClientMessage.StartSession
@@ -91,23 +106,26 @@ class InputReducerTest {
}
@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(
input = InputState(InputMode.WorkflowId, " "),
state = TuiState(inputMode = InputMode.ROUTER, inputBuffer = " "),
action = Action.SubmitInput,
)
assertEquals(InputMode.None, state.mode)
assertEquals(InputMode.ROUTER, state.inputMode)
assertTrue(effects.isEmpty())
}
@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(
input = InputState(InputMode.SteeringNote, "steer this way"),
approval = activeApproval,
state = TuiState(
inputMode = InputMode.STEER,
inputBuffer = "steer this way",
approval = ApprovalState(active = activeApproval),
),
action = Action.SubmitInput,
)
assertEquals(InputMode.None, state.mode)
assertEquals(InputMode.ROUTER, state.inputMode)
assertEquals(1, effects.size)
val effect = effects[0] as Effect.SendWs
val msg = effect.message as ClientMessage.ApprovalResponse
@@ -117,12 +135,12 @@ class InputReducerTest {
}
@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(
input = InputState(InputMode.Filter, "myfilter"),
state = TuiState(inputMode = InputMode.FILTER, inputBuffer = "myfilter"),
action = Action.SubmitInput,
)
assertEquals(InputMode.None, state.mode)
assertEquals(InputMode.ROUTER, state.inputMode)
assertTrue(effects.isEmpty())
}
}
@@ -3,7 +3,6 @@ package com.correx.apps.tui.reducer
import com.correx.apps.server.protocol.ClientMessage
import com.correx.apps.tui.input.Action
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.assertTrue
@@ -14,11 +13,11 @@ class RootReducerTest {
private val fixedClock: () -> Long = { 1000L }
@Test
fun `SubmitInput WorkflowId emits StartSession effect and resets input`() {
val initial = TuiState(input = InputState(InputMode.WorkflowId, "my-workflow"))
fun `SubmitInput in ROUTER mode emits StartSession effect and resets buffer`() {
val initial = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "my-workflow")
val (state, effects) = RootReducer.reduce(initial, Action.SubmitInput, fixedClock)
assertEquals(InputMode.None, state.input.mode)
assertEquals("", state.input.text)
assertEquals(InputMode.ROUTER, state.inputMode)
assertEquals("", state.inputBuffer)
val wsEffects = effects.filterIsInstance<Effect.SendWs>()
assertEquals(1, wsEffects.size)
val msg = wsEffects[0].message as ClientMessage.StartSession
@@ -31,7 +30,8 @@ class RootReducerTest {
val (state, effects) = RootReducer.reduce(initial, Action.Connected, fixedClock)
assertTrue(state.connection.connected)
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.approval, state.approval)
assertEquals(initial.provider, state.provider)
@@ -51,15 +51,15 @@ class RootReducerTest {
)
val (state2, _) = RootReducer.reduce(state1, Action.ServerEventReceived(startMsg2), fixedClock)
// 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)
}
@Test
fun `AppendChar followed by Backspace returns to original text`() {
val initial = TuiState(input = InputState(InputMode.WorkflowId, "ab"))
fun `AppendChar followed by Backspace returns to original buffer`() {
val initial = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "ab")
val (s1, _) = RootReducer.reduce(initial, Action.AppendChar('c'), 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(
sessions: SessionsState = SessionsState(),
inputMode: InputMode = InputMode.None,
inputMode: InputMode = InputMode.ROUTER,
inputText: String = "",
action: Action,
) = SessionsReducer.reduce(sessions, inputMode, inputText, action, fixedClock)
@@ -35,7 +35,7 @@ class SessionsReducerTest {
@Test
fun `NavigateUp wraps from top to bottom`() {
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)
assertTrue(effects.isEmpty())
}
@@ -43,14 +43,14 @@ class SessionsReducerTest {
@Test
fun `NavigateDown advances selection`() {
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)
}
@Test
fun `NavigateDown wraps from bottom to top`() {
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)
}
@@ -58,7 +58,7 @@ class SessionsReducerTest {
fun `SubmitInput in Filter mode copies inputText into filter`() {
val s = SessionsState()
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)
assertTrue(effects.isEmpty())
@@ -68,7 +68,7 @@ class SessionsReducerTest {
fun `SubmitInput in Filter mode with empty text clears filter`() {
val s = SessionsState(filter = "old")
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)
assertTrue(effects.isEmpty())
@@ -77,7 +77,7 @@ class SessionsReducerTest {
@Test
fun `CancelInput in Filter mode clears filter`() {
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)
assertTrue(effects.isEmpty())
}
@@ -85,7 +85,7 @@ class SessionsReducerTest {
@Test
fun `CancelInput in non-Filter mode is no-op`() {
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)
}
@@ -96,7 +96,8 @@ class SessionsReducerTest {
assertEquals(1, state.sessions.size)
assertEquals("s1", state.sessions[0].id)
assertEquals("s1", state.selectedId)
assertTrue(effects.isEmpty())
assertEquals(1, effects.size)
assertTrue(effects[0] is Effect.ConnectSession)
}
@Test