Skip to content

resolve interlining tripId mismatch in trips-for-route - #1256

Merged
burma-shave merged 21 commits into
OneBusAway:mainfrom
3rabiii:fix-trips-for-route-gap8
Aug 6, 2026
Merged

resolve interlining tripId mismatch in trips-for-route#1256
burma-shave merged 21 commits into
OneBusAway:mainfrom
3rabiii:fix-trips-for-route-gap8

Conversation

@3rabiii

@3rabiii 3rabiii commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR resolves a critical spec compliance issue in the trips-for-route endpoint regarding interlined blocks. Previously, the handler incorrectly assigned the currently executing trip to the outer tripId field, even if that trip belonged to a different route due to interlining.

According to the OBA specification, the outer tripId must consistently reflect the queried route's trip, while status.activeTripId should reflect the trip the vehicle is currently executing on the ground.

Changes Made

  • Batch Resolution for Interlining: Introduced a mechanism to collect interlined block IDs and fetch their associated trips via a single batch query (GetTripsByBlockIDs), successfully avoiding an N+1 query problem.
  • State Separation: Constructed an O(1) translation map (blockTripForRoute) to map block IDs to the correct queried-route trip ID. The outer tripId is now properly overridden using this map, while BuildTripStatus remains untouched to accurately populate status.activeTripId.
  • Robust Integration Testing: Added TestTripsForRouteHandler_InterlinedBlock. This test constructs an in-memory GTFS zip fixture featuring a single block spanning two distinct routes. By manipulating a mock clock, it enforces strict assertions to guarantee that the outer tripId matches the queried route, while the inner activeTripId reflects the simulated active trip.

Closes: #1254


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected trip identifiers and schedule details for interlined trips so results match the requested route.
    • Improved real-time status and situation information for interlined trips.
    • Enhanced overnight, cross-day, and looping-trip handling.
    • Improved selection across service days and schedule gaps.
    • Added graceful fallback when timing information or matching trips are unavailable.
  • Tests

    • Added coverage for interlined, overnight, looping-route, cross-day, gap, service-day, and missing-time scenarios.

Update trips-for-route handler to correctly distinguish between the
queried route's trip and the currently executing active trip for
interlined blocks, strictly adhering to OBA spec requirements.

- Added a batch DB query (GetTripsByBlockIDs) to fetch related trips
  for interlined blocks without introducing an N+1 query bottleneck.
- Implemented an O(1) lookup map to resolve the queried route's trip
  ID for the outer envelope.
- Retained the actual executing trip for status.activeTripId.
- Added TestTripsForRouteHandler_InterlinedBlock with a custom
  in-memory GTFS fixture to guarantee strict spec compliance.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3067f7b5-bc16-4265-8b62-c36e2bd30205

📥 Commits

Reviewing files that changed from the base of the PR and between 7ec577a and 1e0a2d6.

📒 Files selected for processing (2)
  • internal/restapi/trips_for_route_handler.go
  • internal/restapi/trips_for_route_handler_test.go

📝 Walkthrough

Walkthrough

The trips-for-route handler resolves queried-route trip IDs for interlined blocks while retaining the vehicle’s active trip ID. Tests add reusable GTFS fixtures for service-day, overnight, looping-route, midpoint, and fallback scenarios.

Changes

Interlined trips-for-route behavior

Layer / File(s) Summary
Block mapping and candidate loading
internal/restapi/trips_for_route_handler.go
The handler maps routes and agencies, then loads queried-route block-trip candidates across current and previous service days.
Interlined trip resolution and response identity
internal/restapi/trips_for_route_handler.go
The handler prefers matching service IDs and otherwise selects the nearest schedule-window midpoint. It uses the resolved trip for schedule and response identity while retaining the active trip for real-time status.
Interline fixture and endpoint validation
internal/restapi/trips_for_route_handler_test.go
Tests use shared GTFS fixtures and cover interlined schedules, references, overnight and looping blocks, service IDs, cross-day blocks, missing times, and missing candidates.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: aaronbrethorst, burma-shave

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant tripsForRouteHandler
  participant GTFSManager
  Client->>tripsForRouteHandler: Request trips-for-route
  tripsForRouteHandler->>GTFSManager: Load queried-route block trips
  GTFSManager-->>tripsForRouteHandler: Return service-day candidates and time windows
  tripsForRouteHandler->>tripsForRouteHandler: Resolve queried TripId
  tripsForRouteHandler->>tripsForRouteHandler: Build status from active trip
  tripsForRouteHandler-->>Client: Return schedule, TripId, and activeTripId
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the fix for the interlining tripId mismatch in the trips-for-route endpoint.
Linked Issues check ✅ Passed The implementation separates the queried-route trip from status.activeTripId and aligns schedule and situation IDs with the queried-route trip, satisfying issue #1254.
Out of Scope Changes check ✅ Passed The code and tests directly support interlined-trip resolution, identity handling, and related fallback scenarios for issue #1254.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@internal/restapi/trips_for_route_handler.go`:
- Around line 317-341: The interlined block-trip lookup currently loses
ServiceID and may select the wrong queried-route trip. In
internal/restapi/trips_for_route_handler.go lines 317-341, update
blockTripForRoute resolution to retain the trip selected for the requested
route, or key candidates by both block ID and service ID before resolving
fetchedTrip. In internal/restapi/trips_for_route_handler_test.go lines 505-524,
add an after-midnight fixture where current and previous service IDs share a
block and assert the outer trip ID matches the queried-route trip for the
correct service.
- Around line 330-336: Update the error branch following GetTripsByBlockIDs in
the trips-for-route handler to call serverErrorResponse with the lookup error
and immediately return. Do not log and continue processing, so interline
resolution failures produce an error response instead of returning a successful
response with an incorrect trip ID.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1da74de7-3dc2-4faf-9687-28b9bf5bacf2

📥 Commits

Reviewing files that changed from the base of the PR and between 9d99978 and b449572.

📒 Files selected for processing (2)
  • internal/restapi/trips_for_route_handler.go
  • internal/restapi/trips_for_route_handler_test.go

Comment thread internal/restapi/trips_for_route_handler.go Outdated
Comment thread internal/restapi/trips_for_route_handler.go Outdated
3rabiii added 2 commits July 29, 2026 22:57
Refactored the `blockTripForRoute` mapping to use a composite struct
(`blockServiceKey` containing `BlockID` and `ServiceID`) rather than
just the block ID. This guarantees accurate trip resolution and
prevents cross-day trip overwrites when block IDs are reused across
different service days.

- Reverted to a single, performant `GetTripsByBlockIDs` batch query
  combining current and previous service IDs.
- Introduced `TestTripsForRouteHandler_OvernightInterlinedBlock`
  with a detailed after-midnight GTFS fixture to enforce strict
  spec compliance for active trips spanning multiple service days.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@internal/restapi/trips_for_route_handler_test.go`:
- Line 623: Add a Go doc comment immediately before
TestTripsForRouteHandler_OvernightInterlinedBlock, beginning exactly with that
function name and briefly describing the test.

In `@internal/restapi/trips_for_route_handler.go`:
- Around line 347-351: Preserve the queried trip identity when a block revisits
the route: update the blockTripForRoute handling in trips_for_route_handler.go
so the trip that caused the block to be selected is retained instead of always
keeping the first (BlockID, ServiceID) candidate. In
internal/restapi/trips_for_route_handler.go lines 347-351, apply this selection
consistently; in internal/restapi/trips_for_route_handler_test.go lines 623-645,
add a fixture with two queried-route trips in one block/service and assert the
outer tripId matches the selecting trip.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: aa2b4695-31a1-4937-95ee-7c8b40d54898

📥 Commits

Reviewing files that changed from the base of the PR and between b449572 and e5fadfa.

📒 Files selected for processing (2)
  • internal/restapi/trips_for_route_handler.go
  • internal/restapi/trips_for_route_handler_test.go

Comment thread internal/restapi/trips_for_route_handler_test.go
Comment thread internal/restapi/trips_for_route_handler.go Outdated
3rabiii added 2 commits July 30, 2026 16:50
Update blockTripForRoute to map block keys to a slice of trips with
their respective time windows. Add time window overlap logic to find
the exact queried-route trip overlapping with the active trip.

Previously, the map stored only the first queried-route trip found.
If a block visited the queried route, left for another route, and
returned, the outer tripId would incorrectly default to the first
match rather than the trip actually overlapping the active window.

Add TestTripsForRouteHandler_LoopingRouteBlock fixture and missing
doc comments for exported test functions.
Run go fmt on the handler file to fix failing CI formatting checks.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@internal/restapi/trips_for_route_handler.go`:
- Around line 404-417: Update the candidate selection around blockTripForRoute
and entryTripID so a non-overlapping candidate is never implicitly chosen via
bestIdx’s default value. Preserve the trip that selected the block, or implement
an explicit deterministic fallback when no entry overlaps the active trip; add
an integration test covering a gap between multiple queried-route trips.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: da665b6e-91bb-4431-ad9f-80afab2d13cc

📥 Commits

Reviewing files that changed from the base of the PR and between e5fadfa and 9e4e714.

📒 Files selected for processing (2)
  • internal/restapi/trips_for_route_handler.go
  • internal/restapi/trips_for_route_handler_test.go

Comment thread internal/restapi/trips_for_route_handler.go Outdated
@aaronbrethorst

Copy link
Copy Markdown
Member

Code review

Found 2 issues:

  1. The interlining tripId selection silently falls back to the wrong trip whenever there is a layover gap. The overlap test e.MinArrivalTime <= activeMax && e.MaxDepartureTime >= activeMin compares a queried-route trip's window against the active trip's window — but trips within a single block never overlap in time by construction (one vehicle), so it can only be true when two trips are exactly contiguous (e.MaxDepartureTime == activeMin). The repo itself models the gaps that break this (block_layover in gtfsdb/schema.sql, plus GetActiveLayoverBlockIDsForRoute used earlier in this same handler). When no candidate overlaps, the loop never breaks and bestIdx stays 0, so entryTripID becomes the earliest queried-route trip of the whole block/service day rather than the trip that caused the block to be selected. Example: block A1(07:00–07:50) → B(08:05–08:35) → A2(08:40–09:40), query at 08:30 — the block is selected via A2 (08:00–08:40 window), neither A1 nor A2 overlaps B, so the entry reports A1, hours off. The new TestTripsForRouteHandler_LoopingRouteBlock only passes because its fixture makes tfr-loop-b end and tfr-loop-c start at exactly 10:30; add any layover and it selects tfr-loop-a. The spec requires "the trip on the queried route that caused this block to be selected", so this should carry through the selecting trip from the block-index/layover window queries above rather than re-deriving it from an overlap heuristic — or at minimum use an explicit, documented nearest-trip fallback instead of index 0.

// Override tripId to queried-route trip for interlined blocks.
entryTripID := tripID
entryAgencyID := activeAgencyID
if fetchedTrip.RouteID != routeID && fetchedTrip.BlockID.Valid {
key := blockServiceKey{BlockID: fetchedTrip.BlockID.String, ServiceID: fetchedTrip.ServiceID}
if entries, ok := blockTripForRoute[key]; ok && len(entries) > 0 {
bestIdx := 0
activeMin := fetchedTrip.MinArrivalTime.Int64
activeMax := fetchedTrip.MaxDepartureTime.Int64
// Among queried-route trips in the same block+service, find the one
// whose time window overlaps with the active trip. This handles blocks
// that visit the queried route multiple times (e.g. route A → B → A).
for i, e := range entries {
if e.MinArrivalTime <= activeMax && e.MaxDepartureTime >= activeMin {
bestIdx = i
break
}
}
entryTripID = entries[bestIdx].ID
entryAgencyID = agencyID
}
}

  1. tripId now identifies the queried-route trip, but schedule and situationIds in the same entry still describe the active trip, so a single entry mixes two trip identities. buildScheduleForTrip(ctx, tripID, ...) (line 381) and GetSituationIDsForTrip(r.Context(), tripID) (line 427) remain keyed on the active trip while TripId is built from entryTripID. Per the wiki spec, data.list[].schedule.stopTimes is "Ordered array of scheduled stop times for this trip" and schedule.previousTripId is "the preceding trip in this vehicle's block" — both relative to the entry's own trip. With the new TestTripsForRouteHandler_InterlinedBlock fixture the response says tripId: tfr-agency_tfr-trip-a while schedule.stopTimes are trip-b's 11:55/12:05 times and schedule.previousTripId resolves to tfr-trip-a — i.e. the entry claims its own tripId is the previous trip in its block. The test requests includeSchedule=true but asserts nothing about the schedule, so this is uncovered. Java OBA builds tripId, schedule and situationIds from the same BlockTripInstance and lets only status.activeTripId diverge; these three should move together.

if includeSchedule {
var schedErr error
schedule, schedErr = api.buildScheduleForTrip(ctx, tripID, activeAgencyID, currentTime, currentLocation)
if schedErr != nil {
api.serverErrorResponse(w, r, schedErr)
return
}
collectStopIDsFromSchedule(schedule, stopIDsMap)
}
if includeStatus {
var statusErr error
status, statusErr = api.BuildTripStatus(ctx, activeAgencyID, tripID, nil, todayMidnight, currentTime)
if statusErr != nil {
api.Logger.Warn("BuildTripStatus failed", "trip_id", tripID, "error", statusErr)
status = nil
}
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@aaronbrethorst aaronbrethorst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You've identified the right problem and set up the right structure for it. The spec is explicit that these are two different things — data.list[].tripId is "the trip on the queried route that caused this block to be selected," while status.activeTripId is "the trip the vehicle is currently executing" — and before this PR we conflated them. Establishing that split, and keeping BuildTripStatus on the active trip, is correct.

The (BlockID, ServiceID) composite key is a genuine improvement too. Keying on block alone would collide across service days, and the overnight test shows you thought about that case rather than assuming it away.

Two things need fixing before I can merge it.

1. The overlap test can't succeed for the case it's meant to handle, and the fallback is silently wrong.

bestIdx := 0
activeMin := fetchedTrip.MinArrivalTime.Int64
activeMax := fetchedTrip.MaxDepartureTime.Int64
for i, e := range entries {
    if e.MinArrivalTime <= activeMax && e.MaxDepartureTime >= activeMin {
        bestIdx = i
        break
    }
}
entryTripID = entries[bestIdx].ID

A block is one vehicle running trips in sequence, so two trips in the same block never overlap in time. The condition is a standard inclusive interval-overlap test, which means it can only be satisfied at exact contiguity — the queried-route trip ending at precisely the instant the active trip begins. Add any layover and it fails: a trip ending 10:25 against an active trip starting 10:30 gives 10:25 >= 10:30 → false.

We model layovers explicitly — there's a block_layover table in the schema, and this same handler calls GetActiveLayoverBlockIDsForRoute a couple hundred lines up — so this isn't a hypothetical.

When nothing matches, the loop never breaks and bestIdx stays at its initial 0. Since the query orders by t.block_id, t.min_arrival_time, t.id, entries[0] is the earliest queried-route trip in that block and service day. So the common case doesn't fall back to something neutral; it emits a specific wrong trip that can be hours from the one that actually caused block selection. And it does so silently.

TestTripsForRouteHandler_LoopingRouteBlock passes only because the fixture has tfr-loop-b ending and tfr-loop-c starting at exactly 10:30:00. Nudge one of those by a minute and the test fails — worth doing as a way to see the bug.

What you want is "the queried-route trip in this block whose window contains or is nearest to the query window" — nearest-by-time with a deterministic tie-break — not overlap. And there shouldn't be a silent fallback: if no candidate can be chosen, that's worth a log line at minimum.

I should flag that CodeRabbit raised this same bestIdx fallback on the last review round, and the only commit after it was Format trips for route handler. Easy for a bot comment to get lost in the noise, but this one was right.

2. tripId now disagrees with schedule and situationIds in the same entry.

schedule is built from tripID (the active trip) at the buildScheduleForTrip call, SituationIds likewise from tripID, but TripId is now entryTripID. When those differ — precisely the interlining case this PR exists for — the entry claims to be trip A while carrying trip B's stop times and alerts.

The spec calls schedule.stopTimes "scheduled stop times for this trip," meaning the entry's trip. It gets worse with schedule.previousTripId, defined as "the preceding trip in this vehicle's block": built from the active trip, that resolves to the queried-route trip — so in your own InterlinedBlock fixture the entry would list itself as its own predecessor.

Java derives tripId, schedule, and situationIds from a single BlockTripInstance and lets only status.activeTripId diverge. Moving all three onto entryTripID would match.

The test requests includeSchedule=true but never asserts on the schedule, which is why this didn't surface. An assertion that schedule.stopTimes belong to the trip named in tripId would catch it and guard the fix.

Smaller things, none blocking:

  • allServiceIDs := serviceIDs followed by append(allServiceIDs, prevServiceIDs...) aliases serviceIDs' backing array. Harmless today because serviceIDs isn't read afterward, but it's a trap for the next edit — an explicit copy costs nothing.
  • The GetTripsByBlockIDs failure both logs a Warn and returns a 500. Beyond the double reporting, it's the only DB lookup in this handler that hard-fails; layoverBlocks, prevServiceIDs, nullBlockTrips, and GetTripsInBlock all degrade gracefully. Worth being consistent one way or the other.
  • entryAgencyID = agencyID uses the agency parsed from the request path rather than the queried route's actual agency_id, bypassing the routeAgencyMap the handler already built. Same answer in practice, but the map is right there.
  • The comments claiming the selected trip's window "overlaps with the active trip" describe something block trips can't do. Whatever selection rule you land on, please make the comments match it — these will mislead the next reader.
  • The three fixture builders are near-identical ~50-line copies differing only in CSV payload and zip name; one parameterized helper would be easier to extend.

One pre-existing issue your new test happens to walk right past: ServiceDate is always todayMidnight.UnixMilli(), but the spec says trips extending past midnight take the previous calendar day. TestTripsForRouteHandler_OvernightInterlinedBlock exercises exactly that path and asserts nothing about serviceDate. Not this PR's job to fix, but since you've built the fixture, adding the assertion (even as a skipped/known-failing case) would be a gift to whoever picks it up.

The structure here is good and I'd like to land it. Fix the trip selection and the schedule/situationIds binding and I'll re-review quickly.

3rabiii added 2 commits July 31, 2026 17:09
Replace the flawed overlap test for interlined trips with a nearest
midpoint calculation, as sequential block trips cannot overlap.
Add a warning log when no queried-route trip candidate is found.

Bind schedule and situationIds to the resolved entryTripID rather than
the active tripID to prevent mixing trip identities in the response.
Retain status binding on the active trip per the specification.

Refactor test fixtures into a parameterized helper, and add schedule
assertions along with layover gap tests to guarantee accurate mapping.
@3rabiii
3rabiii requested a review from aaronbrethorst July 31, 2026 14:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/restapi/trips_for_route_handler.go (2)

354-359: ⚠️ Potential issue | 🟠 Major

Do not return a successful response when queried-trip resolution fails.

GetTripsByBlockIDs is required to resolve data.list[].tripId to the queried-route trip. This branch only logs the database error. The empty-candidate branch also leaves entryTripID equal to the active cross-route trip. The handler can return HTTP 200 with the wrong outer trip ID. Return the lookup error through serverErrorResponse and do not emit an active-trip TripId when no queried-route candidate exists.

As per coding guidelines, “When a database lookup fails, return 404 via sendNotFound only for errors.Is(err, sql.ErrNoRows); route all other errors through serverErrorResponse as 500.”

Proposed fix
 		if err != nil {
-			// Degrade gracefully: skip interlining resolution for these blocks.
-			api.Logger.Warn("trips-for-route: failed to fetch block trips for interlining", "error", err)
+			api.serverErrorResponse(w, r, err)
+			return
 		}

Also applies to: 421-427

🤖 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 `@internal/restapi/trips_for_route_handler.go` around lines 354 - 359, The
interlining trip lookup must not degrade to a successful response: in the
GetTripsByBlockIDs error branch, return sql.ErrNoRows through sendNotFound and
all other errors through serverErrorResponse. In the empty-candidate handling
around entryTripID, avoid retaining the active cross-route trip ID; only emit
TripId when a queried-route candidate was resolved.

Source: Coding guidelines


361-370: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Include the interlined trip in fetchedTrips.

buildTripReferences pre-populates presentTrips from fetchedTrips, so interlined entries currently use the active trip’s reference instead of the quoted-route trip reference, and interlined schedules may use the active trip’s previous trip. Add fetchedTrips = append(fetchedTrips, selectedInterlineTrip) before building references, and add an interlined includeTrip=true test.

🤖 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 `@internal/restapi/trips_for_route_handler.go` around lines 361 - 370, Append
selectedInterlineTrip to fetchedTrips before invoking buildTripReferences so
presentTrips uses the quoted-route trip reference and current interlined
schedule. Add an interlined-trip test verifying includeTrip=true and the
selected trip is included in fetchedTrips.
🤖 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.

Outside diff comments:
In `@internal/restapi/trips_for_route_handler.go`:
- Around line 354-359: The interlining trip lookup must not degrade to a
successful response: in the GetTripsByBlockIDs error branch, return
sql.ErrNoRows through sendNotFound and all other errors through
serverErrorResponse. In the empty-candidate handling around entryTripID, avoid
retaining the active cross-route trip ID; only emit TripId when a queried-route
candidate was resolved.
- Around line 361-370: Append selectedInterlineTrip to fetchedTrips before
invoking buildTripReferences so presentTrips uses the quoted-route trip
reference and current interlined schedule. Add an interlined-trip test verifying
includeTrip=true and the selected trip is included in fetchedTrips.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fd6e7f53-53e1-421d-94ca-cfab77de3bfa

📥 Commits

Reviewing files that changed from the base of the PR and between 7ec577a and 8f02ea7.

📒 Files selected for processing (2)
  • internal/restapi/trips_for_route_handler.go
  • internal/restapi/trips_for_route_handler_test.go

Restore missing test cases to ensure proper coverage for block trip selection:
- Restore TestTripsForRouteHandler_InterlinedBlock, OvernightInterlinedBlock,
  and LoopingRouteBlock.
- Add a new GapCase test using the existing gapFiles fixture to test layovers.
- Update all tests to use the standard createTestApiWithGTFSFixture helper.

This resolves the undefined function compile error, eliminates dead fixture
code, and fully covers the midpoint-selection logic.
@3rabiii
3rabiii force-pushed the fix-trips-for-route-gap8 branch from 8f02ea7 to ddffa70 Compare August 2, 2026 17:36
3rabiii added 5 commits August 2, 2026 21:17
- Return 404 on sql.ErrNoRows and 500 on other DB errors instead of silent fallback.
- Return 404 when no queried-route trip candidate is found.
- Append the resolved interlined trip to fetchedTrips for accurate references.
- Add test to verify interlined trip references.
# Conflicts:
#	internal/restapi/trips_for_route_handler.go
Two code paths returned 404 for the entire response whenever a single
interlined block's queried-route trip couldn't be resolved: one after
a real GetTripsByBlockIDs error, another when no matching trip was
found for a block+service key. Both discarded every other correctly
resolved entry in the response.

The trips-for-route spec guarantees 200 OK even for unknown routes and
out-of-service-area conditions, with no error or 404 case defined.
Skip just the unresolvable entry instead, matching this handler's own
pattern for other per-entry resolution failures (e.g. BuildTripStatus
errors already log and continue rather than aborting).

The GetTripsByBlockIDs error branch also had a dead sql.ErrNoRows
check: it's a sqlc :many query, which returns an empty result with a
nil error rather than ErrNoRows.
hms() and loopingRouteWithGapFiles() are leftover from earlier
iterations of this test file's fixtures and are no longer called
anywhere; gapFiles() supersedes the latter.
blockTripForRoute keyed candidates by (BlockID, ServiceID), requiring
a block's active trip and its queried-route partner to share the
exact same service_id string. GTFS allows more than one service_id to
be active on a single calendar date, and nothing requires a block's
trips to agree on which one they're tagged with, so a legitimately
interlined block could fail to resolve and its entry would be
silently dropped.

Key on BlockID alone instead, matching how allLinkedBlocks already
treats blocks earlier in this handler. Both queries feeding this
lookup are already scoped to today's and yesterday's active service
IDs, so this doesn't reopen the cross-day collision the service-scoped
key was originally added to prevent.
tripsForRouteHandler had grown two new local types, a batched query,
and a nested midpoint-distance search inlined directly into an
already 300+ line function. Extract that into two named helpers:

- buildBlockTripForRoute batch-fetches queried-route trips for the
  blocks fetchedTrips are interlined through.
- resolveInterlinedEntryTripID picks, from those candidates, the one
  nearest a given active trip.

No behavior change: this is a pure code-motion refactor so the
handler reads as a sequence of named steps instead of mixing
orchestration with the resolution mechanics inline.
resolveInterlinedEntryTripID picked the queried-route candidate whose
time window was nearest the active trip's, searched across every
candidate sharing the block ID regardless of service_id. An agency
reusing a block ID across two unrelated service_ids (e.g. yesterday's
and today's schedules) could therefore return a candidate from the
wrong calendar day whenever that candidate's time-of-day happened to
be numerically closer to the active trip's than the real match.

Prefer candidates that share the active trip's exact service_id
first: trips under one service_id recur together on every date it's
active, so they can never be a cross-day collision. Only fall back to
the broader nearest-midpoint search when no same-service_id candidate
exists, preserving resolution for the legitimate case of two distinct
service_ids both active on the same calendar day.

Also skip candidates (and the active trip itself) whose cached
MinArrivalTime/MaxDepartureTime are NULL, which happens for a trip
with no stop_times rows. Reading these sql.NullInt64 fields via
.Int64 without checking .Valid silently treated such a trip as
starting at midnight, corrupting the nearest-midpoint comparison.
When no queried-route trip could be found anywhere in an interlined
block, the handler skipped the entry entirely rather than emit one
with a misleading tripId. That trades a documented guarantee (one
entry per active block) for a stronger one no client actually needs:
legacy OBA always reports the active trip's own ID here (tripId and
status.activeTripId are the same field in the Java implementation),
and this handler already defaults entryTripID to the active trip's ID
before interlining resolution runs.

Let that default stand when resolution fails instead of skipping the
entry, matching legacy behavior for this one case while keeping the
route-aware tripId this PR adds for every block that does resolve.
Schedule is built from entryTripID (the resolved queried-route trip),
not the active trip, but no test asserted on schedule contents for an
interlined block — only tripId and status.activeTripId were checked.
Assert that schedule.stopTimes reflect tfr-trip-a's own 11:20/11:50
stop times rather than the active trip tfr-trip-b's 11:55/12:05,
covering the schedule/tripId binding this PR introduces.
@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

@burma-shave
burma-shave dismissed aaronbrethorst’s stale review August 6, 2026 00:14

Comments have been addressed

@burma-shave
burma-shave merged commit 6b88683 into OneBusAway:main Aug 6, 2026
8 checks passed
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.

Interlining contract not honored for tripId vs status.activeTripId in trips-for-route

3 participants