Skip to content

Refactor HandleFindStop into smaller, tested functions#9

Open
aaronbrethorst wants to merge 1 commit into
mainfrom
fix-1
Open

Refactor HandleFindStop into smaller, tested functions#9
aaronbrethorst wants to merge 1 commit into
mainfrom
fix-1

Conversation

@aaronbrethorst

@aaronbrethorst aaronbrethorst commented Jun 16, 2026

Copy link
Copy Markdown
Member

Closes #1

Summary

  • Split the ~93-line HandleFindStop (handlers/voice/find_stop.go) into four focused helpers — bindFindStopRequest, tryHandleDisambiguationChoice, resolveStopID, respondForStopID — reducing the handler itself to a 22-line top-to-bottom pipeline: bind → maybe-handle-disambiguation → resolve stop ID → respond.
  • Replaced the magic 9 DTMF ceiling (open-coded in 4 places) with a maxVoiceChoices const and a clampChoiceCount helper.
  • Added in-package characterization tests (handlers/voice/find_stop_test.go, package voice) covering every branch. Combined with the existing handlers-package tests, HandleFindStop is at 100% statement coverage and the extracted helpers are well covered (80–100%).
  • Behavior is unchanged — pure refactor. The decomposition was verified line-by-line against the original, including the subtle "single digit with no active session falls through to be treated as a stop ID" path.

Test plan

  • make fmt / make vet / make lint (0 issues) / make test all pass
  • Golden path: single-stop match → arrivals + menu TwiML
  • Disambiguation: multiple matches prompt, choice selects a stop, out-of-range choice rejected
  • Edge cases: invalid phone (short-circuits before lookup), invalid call SID (non-fatal), empty digits, invalid stop-ID format, lookup error, zero matches, single-digit-without-session, >9 matches clamp to 9

Review notes (non-blocking follow-ups, intentionally out of scope here)

  • bindFindStopRequest overlaps with the existing preprocessRequest helper, but they are not interchangeable: preprocessRequest also fires analytics (TrackVoiceRequest), which the original HandleFindStop never did. Unifying them is a deliberate behavior decision (should find-stop calls be tracked?) — left for a follow-up to keep this PR behavior-preserving.
  • handleVoiceDisambiguationChoice still re-fetches the session and re-validates the choice range that tryHandleDisambiguationChoice already checked; that downstream range check is now effectively unreachable on the real path (a pre-existing redundancy, not introduced here). Could be collapsed by passing the session through.
  • The new voice-package tests overlap with several HandleFindStop cases in handlers/voice_menu_test.go, which could be retired in favor of these.

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened input validation and error handling in voice-based stop search to improve reliability
    • Enhanced disambiguation workflow for multiple matching stops with consistent handling capped at nine options
    • Refined stop ID resolution with improved edge case handling
  • Tests

    • Added comprehensive test suite covering voice stop search functionality, including error scenarios, disambiguation flows, and multiple-stop matching

Split the ~93-line HandleFindStop into four focused helpers
(bindFindStopRequest, tryHandleDisambiguationChoice, resolveStopID,
respondForStopID), reducing the handler to a 22-line pipeline. Replace
the repeated max-9 DTMF clamp with a maxVoiceChoices const and
clampChoiceCount helper.

Add in-package characterization tests covering every branch; combined
with the existing handlers-package tests, HandleFindStop is at 100%
statement coverage and the extracted helpers are well covered.

Behavior is unchanged.

Closes #1
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

HandleFindStop in handlers/voice/find_stop.go is decomposed into bindFindStopRequest, tryHandleDisambiguationChoice, resolveStopID, and respondForStopID. Disambiguation option clamping is consolidated via a maxVoiceChoices = 9 constant and clampChoiceCount helper. A new test file adds mockOBAClient, test wiring, and scenario tests covering all handler and helper paths.

Changes

HandleFindStop Refactor and Test Coverage

Layer / File(s) Summary
HandleFindStop decomposition and resolveStopID
handlers/voice/find_stop.go
HandleFindStop is refactored into helper-driven steps: bindFindStopRequest returns (req, ok) and centralizes validation including non-fatal CallSid logging; resolveStopID extracts digits, handles the empty case with localized messaging, validates stop ID format, and returns (stopID, ok) to gate respondForStopID.
maxVoiceChoices constant and clampChoiceCount consolidation
handlers/voice/find_stop.go
Adds maxVoiceChoices = 9 and clampChoiceCount helper. Updates parseDisambiguationChoice to enforce the clamped 1–9 range, handleVoiceDisambiguationChoice to compute effectiveMax via clampChoiceCount, and formatVoiceDisambiguationMessage to cap the rendered stop options loop and truncation condition using the shared constant.
Test mock, helpers, and Gin wiring
handlers/voice/find_stop_test.go
Adds mockOBAClient (Testify mock) implementing the OBA client interface, helpers to build OneBusAwayResponse and wire the happy-path mock call sequence, setupFindStopHandler for Gin routing, and postFindStop for form-encoded requests.
Handler scenario tests
handlers/voice/find_stop_test.go
Tests cover invalid phone (no OBA lookup), non-fatal CallSid handling, empty digits prompt, invalid stop ID format, OBA error, no matching stops, disambiguation <Gather> output, single-stop route output, more-than-nine clamp, valid disambiguation selection, out-of-range selection, and single-digit-without-session treated as a stop ID.
Direct helper unit tests
handlers/voice/find_stop_test.go
Unit tests for parseDisambiguationChoice (invalid inputs → 0, valid digits → numeric value), handleVoiceDisambiguationChoice with no session (asserts "No active selection"), and getAndFormatVoiceArrivalsWithSession OBA error handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring work: decomposing HandleFindStop into smaller, tested helper functions.
Linked Issues check ✅ Passed The PR fully addresses the linked issue by decomposing the ~170-line handler into four focused functions (22 lines main, 80–100% coverage for helpers), exceeding the 100-line target and 'extremely high' coverage requirement.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the refactoring objective; disambiguation constant consolidation and helper functions directly support the stated decomposition goal with no unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
handlers/voice/find_stop_test.go (1)

315-329: ⚡ Quick win

Strengthen the lookup-error helper test with an output assertion.

This test only verifies that the mock was called. Add one observable response assertion (e.g., expected fallback/error prompt in body) so regressions in error rendering don’t slip through.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@handlers/voice/find_stop_test.go` around lines 315 - 329, The test
TestGetAndFormatVoiceArrivals_LookupError verifies that the mock client was
called but does not assert anything about the actual response output. Add an
assertion after the handler call to check that the response body (accessible via
the httptest.ResponseRecorder w) contains the expected fallback or error prompt
message. This ensures that when a lookup error occurs, the handler properly
renders an error response to the user, not just that the mock was invoked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@handlers/voice/find_stop_test.go`:
- Around line 110-118: The test TestHandleFindStop_InvalidPhoneNumber currently
only verifies that the response body is non-empty but does not assert the HTTP
status code returned by the handler. Add an assertion to check the HTTP status
code on the response writer w to make the test deterministic and catch cases
where the handler might return an incorrect status while still having a
non-empty body. Reference the status code from the response writer object that
is populated by the postFindStop call.

In `@handlers/voice/find_stop.go`:
- Around line 287-289: The truncation message "Only showing first 9 options. "
at line 288 contains a hard-coded "9" that can drift from the maxVoiceChoices
constant if it is ever changed. Replace the literal "9" in this message string
with the actual maxVoiceChoices value using string formatting (e.g., sprintf or
similar formatting function) so the displayed number stays synchronized with the
constant, eliminating the magic-number coupling.

---

Nitpick comments:
In `@handlers/voice/find_stop_test.go`:
- Around line 315-329: The test TestGetAndFormatVoiceArrivals_LookupError
verifies that the mock client was called but does not assert anything about the
actual response output. Add an assertion after the handler call to check that
the response body (accessible via the httptest.ResponseRecorder w) contains the
expected fallback or error prompt message. This ensures that when a lookup error
occurs, the handler properly renders an error response to the user, not just
that the mock was invoked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fa67a2c6-1816-4e0c-8ebb-0ab3969cfe40

📥 Commits

Reviewing files that changed from the base of the PR and between ef8d3f7 and 12260ce.

📒 Files selected for processing (2)
  • handlers/voice/find_stop.go
  • handlers/voice/find_stop_test.go

Comment on lines +110 to +118
func TestHandleFindStop_InvalidPhoneNumber(t *testing.T) {
r, mockClient, _ := setupFindStopHandler()

w := postFindStop(r, "abc", "12345")

// Phone validation fails before any OBA lookup happens.
mockClient.AssertNotCalled(t, "FindAllMatchingStops", mock.Anything)
assert.NotEqual(t, "", w.Body.String())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the HTTP status in this invalid-phone test.

Line 117 currently checks only non-empty body. This can pass even if the handler returns the wrong status code. Add a StatusOK assertion to keep this scenario deterministic.

Suggested patch
 func TestHandleFindStop_InvalidPhoneNumber(t *testing.T) {
 	r, mockClient, _ := setupFindStopHandler()

 	w := postFindStop(r, "abc", "12345")

 	// Phone validation fails before any OBA lookup happens.
 	mockClient.AssertNotCalled(t, "FindAllMatchingStops", mock.Anything)
+	assert.Equal(t, http.StatusOK, w.Code)
 	assert.NotEqual(t, "", w.Body.String())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestHandleFindStop_InvalidPhoneNumber(t *testing.T) {
r, mockClient, _ := setupFindStopHandler()
w := postFindStop(r, "abc", "12345")
// Phone validation fails before any OBA lookup happens.
mockClient.AssertNotCalled(t, "FindAllMatchingStops", mock.Anything)
assert.NotEqual(t, "", w.Body.String())
}
func TestHandleFindStop_InvalidPhoneNumber(t *testing.T) {
r, mockClient, _ := setupFindStopHandler()
w := postFindStop(r, "abc", "12345")
// Phone validation fails before any OBA lookup happens.
mockClient.AssertNotCalled(t, "FindAllMatchingStops", mock.Anything)
assert.Equal(t, http.StatusOK, w.Code)
assert.NotEqual(t, "", w.Body.String())
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@handlers/voice/find_stop_test.go` around lines 110 - 118, The test
TestHandleFindStop_InvalidPhoneNumber currently only verifies that the response
body is non-empty but does not assert the HTTP status code returned by the
handler. Add an assertion to check the HTTP status code on the response writer w
to make the test deterministic and catch cases where the handler might return an
incorrect status while still having a non-empty body. Reference the status code
from the response writer object that is populated by the postFindStop call.

Comment on lines +287 to 289
if len(stops) > maxVoiceChoices {
msg += "Only showing first 9 options. "
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace the remaining hard-coded 9 in truncation messaging.

Line 288 still hard-codes 9 ("Only showing first 9 options. "), which can drift from maxVoiceChoices if the constant changes. Please format this message from maxVoiceChoices (or localized string args) so the refactor fully removes magic-number coupling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@handlers/voice/find_stop.go` around lines 287 - 289, The truncation message
"Only showing first 9 options. " at line 288 contains a hard-coded "9" that can
drift from the maxVoiceChoices constant if it is ever changed. Replace the
literal "9" in this message string with the actual maxVoiceChoices value using
string formatting (e.g., sprintf or similar formatting function) so the
displayed number stays synchronized with the constant, eliminating the
magic-number coupling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor HandleFindStop

1 participant