Integration tests - #9
Conversation
`mvn install -P include-integration-tests` was failing before any test could run due to a cascade of bitrot. This commit gets the fork VM healthy and the suite executing; two tests still fail on stale 2016 assertions (RecoverFromDetourTest, PredictionAccuracyIntegrationTest) but those are real test-expectation issues, not infrastructure. - PlaybackModule: match current GtfsData ctor (18 args, not 16) and stage GTFS to a temp dir with rewritten calendar.txt end_dates, so GtfsData.isCalendarActiveInTheFuture stops calling System.exit(-1) and killing the surefire fork. Mirrors transitclockPipelineTests' CoreHarness. - pom.xml: add test-scoped commons-lang3 (transitclockCore shades it, so consumers don't see it transitively). Bump surefire/failsafe to 3.5.4 for readable failure output. - hsql_integration_test_hibernate.cfg.xml: hbm2ddl.auto create -> update. Core's constructor calls clearSessionFactory() after setupGtfs loads the GTFS; with "create" the factory rebuild drops every table, leaving BlockAssigner with serviceIds=[]. - ehcache.xml: shadow the core copy which hardcodes persistence at /usr/local/transitclock/cache/ (not writable under CI/dev). Uses target/ehcache-integration-store, same pattern as pipeline tests. - AVL/pred CSVs: convert yyyy-MM-dd -> MM-dd-yyyy. Time.parse expects MM-dd-yyyy; ISO strings were being parsed as year 0195 and no vehicle could match any service.
Both rely on 2016 WMATA AVL/GTFS data plus (for the prediction test) a 2016 baseline that predates ~10 years of predictor changes, so their failing assertions no longer represent regressions against the current code. Re-enable once tools/wmata_capture lands and we can rebuild fixtures/baselines against the current engine. - RecoverFromDetourTest.test: #7 - PredictionAccuracyIntegrationTest.testPredictions: #8
Single-file Python (PEP 723) capture tool that polls WMATA's GTFS-RT VehiclePositions feed and writes per-vehicle CSVs in the exact shape BatchCsvAvlFeedModule ingests (header: vehicleId,time,assignmentId, assignmentType,heading,latitude,longitude; time in MM-dd-yyyy HH:mm:ss Eastern). Also fetches and unpacks the static GTFS once so the capture output is a self-contained fixture bundle. Requires uv; dependencies (requests, gtfs-realtime-bindings) declared inline via PEP 723 so `uv run capture.py ...` is the only setup step. WMATA_API_KEY is read from the environment only; the script refuses to accept it on the command line and prints a pointer to .env.example. Output/ and .env are gitignored so a long background capture can't accidentally leak AVL data or the API key into VCS. Intended use: regenerate the 2016 fixtures used by the now-@ignore'd RecoverFromDetourTest (#7) and PredictionAccuracyIntegrationTest (#8). The README walks through the smoke test, a realistic 4-hour AM-peak capture, and the promote-to-fixtures steps.
Running unit + pipeline + integration tiers used to require two separate invocations. The new root-pom profile layers both transitclockIntegration and transitclockPipelineTests into the reactor so a single `mvn install -P run-all-tests` exercises everything. README.md and CLAUDE.md call out the new command alongside the existing per-tier profiles.
Both classes extended junit.framework.TestCase, so surefire routed them through JUnit38ClassRunner — which discovers tests by "test*" naming and ignores JUnit 4 annotations including @ignore. The tests kept running and kept failing, defeating the disable. Convert both to pure JUnit 4 by dropping `extends TestCase`, switching to `org.junit.Assert.*` static imports, and (for the prediction test) marking the former JUnit 3 `setUp()` with `@Before`. After this the two tests report as Skipped instead of Failure. The other two integration tests (EffectiveScheduleDifferenceDuringLayover, GenerateEffectiveScheduleDifference) still extend TestCase and continue to run under the JUnit 3 runner — not touching them.
Mirrors the existing pipeline-tests step: separate invocation of `mvn -pl transitclockIntegration -am -P include-integration-tests test` so an integration failure is attributed distinctly from the unit and pipeline tiers. All three tiers now run on every push/PR to the main branches.
📝 WalkthroughWalkthroughAdds a WMATA capture CLI to record GTFS/GTFS‑RT fixtures, CI and Maven support to run integration tests (new profile and CI step), integration test migration to JUnit4 with some tests ignored, and integration-test-specific cache/DB staging and config updates; docs and .gitignore entries included. Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as capture.py (CLI)
participant WMATA as WMATA API
participant FS as FileSystem
participant State as Capture State
participant Tests as Integration Tests
CLI->>WMATA: download static GTFS (api_key)
WMATA-->>FS: GTFS tar.gz
CLI->>FS: extract GTFS -> output_dir/gtfs
CLI->>CLI: build trip_id -> block_id map
loop polling loop (with exponential backoff)
CLI->>WMATA: fetch GTFS-RT VehiclePositions
WMATA-->>CLI: FeedMessage
CLI->>State: filter by route/vehicle, map assignment
CLI->>State: deduplicate by (vehicle, timestamp)
CLI->>FS: append CSV row to avl/<route>_<vehicle>.csv
CLI->>FS: flush writers (periodic)
end
CLI->>FS: append capture.log + summary
CLI->>Tests: user promotes captured GTFS/AVL into integration test resources
Tests->>FS: consume GTFS + AVL CSVs during integration test run
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
`source .env` doesn't export plain KEY=VALUE lines, so users who followed the README literally hit "WMATA_API_KEY is not set" even with a populated .env. Fix by reading tools/wmata_capture/.env at script startup with a minimal built-in parser — no new dependencies, no shell gymnastics required. An already-exported WMATA_API_KEY still wins over the file, so explicit exports keep working. README and .env.example updated to match the new ergonomics.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
pom.xml (1)
71-77: Profile may need-amflag or core modules.The
run-all-testsprofile only includestransitclockIntegrationandtransitclockPipelineTests, but both depend ontransitclockCore. Unlikeinclude-integration-tests(which lists all core modules), this profile requires the-am(also-make) flag to build dependencies, or it will fail when used standalone.Consider either:
- Documenting that
mvn install -P run-all-tests -amis required, or- Adding core modules to match
include-integration-tests🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pom.xml` around lines 71 - 77, The run-all-tests profile (profile id "run-all-tests") only lists modules transitclockIntegration and transitclockPipelineTests but those depend on transitclockCore, so update the build to ensure dependencies are built: either document that callers must invoke Maven with -am (e.g., "mvn install -P run-all-tests -am") or modify the run-all-tests profile to include the core modules (mirror what include-integration-tests does by adding transitclockCore and other core modules) so the profile can be executed standalone without -am.transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java (1)
250-251: Temp directory cleanup may not work as expected.
staged.toFile().deleteOnExit()only deletes the directory itself on JVM exit, and only if it's empty. The files copied/created inside won't be deleted, so the directory delete will fail silently. Over many test runs, this could accumulate orphaned temp directories.Consider registering each file for deletion or using a shutdown hook that recursively deletes the staged directory.
♻️ Suggested fix to ensure cleanup
Path staged = Files.createTempDirectory("playback-gtfs-"); - staged.toFile().deleteOnExit(); + // Register shutdown hook for recursive cleanup + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + Files.walk(staged) + .sorted(java.util.Comparator.reverseOrder()) + .forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException ignored) {} }); + } catch (IOException ignored) {} + }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java` around lines 250 - 251, The temp-dir created via Files.createTempDirectory("playback-gtfs-") and assigned to staged is only scheduled for deletion with staged.toFile().deleteOnExit(), which fails if the directory is non-empty; change PlaybackModule to instead register a JVM shutdown hook that recursively deletes the staged directory (walk the file tree and delete files then directories) or explicitly register each created temp file for deletionOnExit; locate the staged variable and replace the single deleteOnExit() call with a shutdown hook that performs Files.walkFileTree (or equivalent recursive delete) to remove all contents and the directory on JVM exit.README.md (1)
60-64: Add language specifier to fenced code block.For consistency with the other code blocks in this file (which use language specifiers), add
bashorshto the code fence.✏️ Suggested fix
To run **everything** (unit + pipeline + integration) in one go: -``` +```bash mvn install -P run-all-tests</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@README.mdaround lines 60 - 64, Update the fenced code block that contains
"mvn install -P run-all-tests" to include a language specifier (use "bash" or
"sh") so the opening backticks become ```bash; this keeps the README's code
block style consistent with the other examples and enables proper syntax
highlighting for the mvn command.</details> </blockquote></details> <details> <summary>tools/wmata_capture/README.md (1)</summary><blockquote> `107-119`: **Clarify the baseline regeneration workflow wording.** Line 117 is a bit confusing: "Restore `@Ignore` removal into a real code change, not a test run." I believe the intent is to permanently remove the `@Ignore` annotation once the new baseline is generated, but the current wording could be misread. Consider rephrasing. <details> <summary>✏️ Suggested clarification</summary> ```diff - - Restore `@Ignore` removal into a real code change, not a test run. + - Permanently remove the `@Ignore` annotation (commit the change rather than just un-ignoring for a local test run). ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@tools/wmata_capture/README.md` around lines 107 - 119, Reword the confusing sentence about `@Ignore` in the README step for regenerating prediction baselines: update the sentence referencing PredictionAccuracyIntegrationTest and `@Ignore` to clearly state the intended workflow (temporarily remove `@Ignore` to run the test and generate the CSV baseline, then re-apply `@Ignore` in the test and make any permanent code changes in a separate commit/PR), so readers understand that `@Ignore` removal is only for the temporary test run and permanent changes should be made in a proper code change referencing PredictionAccuracyIntegrationTest and the generated baseline. ``` </details> </blockquote></details> <details> <summary>tools/wmata_capture/capture.py (1)</summary><blockquote> `267-267`: **Remove redundant `int()` cast.** In Python 3, `round()` returns an `int` when called with a single argument, making the outer `int()` unnecessary. <details> <summary>✏️ Suggested fix</summary> ```diff - heading = int(round(getattr(pos, "bearing", 0.0) or 0.0)) + heading = round(getattr(pos, "bearing", 0.0) or 0.0) ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@tools/wmata_capture/capture.py` at line 267, The assignment to heading wraps round(...) with an unnecessary int() cast; replace heading = int(round(getattr(pos, "bearing", 0.0) or 0.0)) with heading = round(getattr(pos, "bearing", 0.0) or 0.0) to remove the redundant int() while keeping the same behavior for the heading variable (use the existing getattr(pos, "bearing", 0.0) expression to locate the code). ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against the current code and only fix it if needed.
Inline comments:
In@transitclockIntegration/src/main/resources/ehcache.xml:
- Line 16: The ehcache configuration references a non-existent thread pool via
the attribute thread-pool="pool-disk" on the ehcache:disk-store element; either
add a section that declares a thread-pool with alias="pool-disk"
(e.g., thread-pool alias="pool-disk" with appropriate min-size/max-size) or
remove the thread-pool="pool-disk" attribute from the ehcache:disk-store element
to rely on Ehcache defaults so the configuration is consistent.
Nitpick comments:
In@pom.xml:
- Around line 71-77: The run-all-tests profile (profile id "run-all-tests") only
lists modules transitclockIntegration and transitclockPipelineTests but those
depend on transitclockCore, so update the build to ensure dependencies are
built: either document that callers must invoke Maven with -am (e.g., "mvn
install -P run-all-tests -am") or modify the run-all-tests profile to include
the core modules (mirror what include-integration-tests does by adding
transitclockCore and other core modules) so the profile can be executed
standalone without -am.In
@README.md:
- Around line 60-64: Update the fenced code block that contains "mvn install -P
run-all-tests" to include a language specifier (use "bash" or "sh") so the
opening backticks become ```bash; this keeps the README's code block style
consistent with the other examples and enables proper syntax highlighting for
the mvn command.In
@tools/wmata_capture/capture.py:
- Line 267: The assignment to heading wraps round(...) with an unnecessary int()
cast; replace heading = int(round(getattr(pos, "bearing", 0.0) or 0.0)) with
heading = round(getattr(pos, "bearing", 0.0) or 0.0) to remove the redundant
int() while keeping the same behavior for the heading variable (use the existing
getattr(pos, "bearing", 0.0) expression to locate the code).In
@tools/wmata_capture/README.md:
- Around line 107-119: Reword the confusing sentence about
@Ignorein the README
step for regenerating prediction baselines: update the sentence referencing
PredictionAccuracyIntegrationTest and@Ignoreto clearly state the intended
workflow (temporarily remove@Ignoreto run the test and generate the CSV
baseline, then re-apply@Ignorein the test and make any permanent code changes
in a separate commit/PR), so readers understand that@Ignoreremoval is only for
the temporary test run and permanent changes should be made in a proper code
change referencing PredictionAccuracyIntegrationTest and the generated baseline.In
@transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java:
- Around line 250-251: The temp-dir created via
Files.createTempDirectory("playback-gtfs-") and assigned to staged is only
scheduled for deletion with staged.toFile().deleteOnExit(), which fails if the
directory is non-empty; change PlaybackModule to instead register a JVM shutdown
hook that recursively deletes the staged directory (walk the file tree and
delete files then directories) or explicitly register each created temp file for
deletionOnExit; locate the staged variable and replace the single deleteOnExit()
call with a shutdown hook that performs Files.walkFileTree (or equivalent
recursive delete) to remove all contents and the directory on JVM exit.</details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: Organization UI **Review profile**: CHILL **Plan**: Pro Plus **Run ID**: `d0a48a80-e740-4061-aa65-8c0e3c693d4c` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 08f65709d159f46494fbe1ad5fc4005387f8850f and 18ba5e5af7e7c8b37faf36e5c03b7acd85d01fce. </details> <details> <summary>⛔ Files ignored due to path filters (4)</summary> * `transitclockIntegration/src/test/resources/avl/3T_3757.csv` is excluded by `!**/*.csv` * `transitclockIntegration/src/test/resources/avl/5A_8062.csv` is excluded by `!**/*.csv` * `transitclockIntegration/src/test/resources/avl/S2_2113.csv` is excluded by `!**/*.csv` * `transitclockIntegration/src/test/resources/pred/S2_2113.csv` is excluded by `!**/*.csv` </details> <details> <summary>📒 Files selected for processing (14)</summary> * `.github/workflows/ci.yml` * `.gitignore` * `CLAUDE.md` * `README.md` * `pom.xml` * `tools/wmata_capture/.env.example` * `tools/wmata_capture/README.md` * `tools/wmata_capture/capture.py` * `transitclockIntegration/pom.xml` * `transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java` * `transitclockIntegration/src/main/resources/ehcache.xml` * `transitclockIntegration/src/main/resources/hsql_integration_test_hibernate.cfg.xml` * `transitclockIntegration/src/test/java/org/transitclock/integration_tests/RecoverFromDetourTest.java` * `transitclockIntegration/src/test/java/org/transitclock/integration_tests/prediction/PredictionAccuracyIntegrationTest.java` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| xmlns:ehcache='http://www.ehcache.org/v3' | ||
| xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.1.xsd"> | ||
| <ehcache:persistence directory="target/ehcache-integration-store" /> | ||
| <ehcache:disk-store thread-pool="pool-disk" /> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Ehcache 3 disk-store thread-pool configuration
💡 Result:
In Ehcache 3, the disk store (OffHeapDiskStore) performs asynchronous disk writes using configurable thread pools. Configuration is done via PooledExecutionService for thread pools, with specific settings for disk stores at CacheManager or cache levels. Key configuration points: 1. Define thread pools using PooledExecutionServiceConfigurationBuilder: - pool(alias, minSize, maxSize) 2. At CacheManager level: - .withDefaultDiskStoreThreadPool("poolAlias") - Or via OffHeapDiskStoreProviderConfiguration(threadPoolAlias) 3. At cache level: - CacheConfigurationBuilder.withDiskStoreThreadPool("poolAlias", writerConcurrency) Example (Java API): CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder .using(PooledExecutionServiceConfigurationBuilder.newPooledExecutionServiceConfigurationBuilder .defaultPool("dflt", 0, 10) .pool("defaultDiskPool", 1, 3) .build) .withDefaultDiskStoreThreadPool("defaultDiskPool") .withCache("myCache", CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, ResourcePoolsBuilder.newResourcePoolsBuilder .heap(10, EntryUnit.ENTRIES) .disk(10L, MemoryUnit.MB)) .withDiskStoreThreadPool("customPool", 2)) // optional per-cache override .build(true); XML example: 10 10 By default, disk stores use a dynamic thread pool shrinking/growing up to the OS CPU count. Disk writes are asynchronous, handled by threads like 'Ehcache [default]-0'. Writer concurrency controls parallel writer threads. This applies to Ehcache 3.x, including recent versions like 3.8 as referenced in discussions.
Citations:
- 1: https://www.ehcache.org/documentation/3.0/thread-pools.html
- 2: https://www.ehcache.org/documentation/3.3/thread-pools.html
- 3: https://stackoverflow.com/questions/56182030/when-adding-new-entries-in-cache-the-disk-store-put-seems-not-asynchronous
🏁 Script executed:
find . -name "ehcache.xml" -path "*/transitclockIntegration/*"Repository: OneBusAway/thetransitclock
Length of output: 126
🏁 Script executed:
cat -n ./transitclockIntegration/src/main/resources/ehcache.xmlRepository: OneBusAway/thetransitclock
Length of output: 5181
🏁 Script executed:
find . -name "ehcache.xml" -path "*/transitclockCore/*"Repository: OneBusAway/thetransitclock
Length of output: 52
🏁 Script executed:
find . -name "ehcache.xml" | head -20Repository: OneBusAway/thetransitclock
Length of output: 231
🏁 Script executed:
cat -n ./transitclock/src/main/resources/ehcache.xmlRepository: OneBusAway/thetransitclock
Length of output: 4775
🏁 Script executed:
rg -i "thread-pool\|PooledExecutionService" --type java --type xml | head -30Repository: OneBusAway/thetransitclock
Length of output: 52
Define the referenced thread pool or remove the explicit reference.
The <ehcache:disk-store thread-pool="pool-disk" /> references a thread pool named pool-disk that is never declared in this configuration file. According to Ehcache 3 specifications, thread pools must be defined using a <thread-pools> element (e.g., <thread-pool alias="pool-disk" min-size="1" max-size="3"/>). While Ehcache may fall back to its default dynamic thread pool if none is explicitly configured, the explicit reference creates an inconsistency. Either define the pool or remove the thread-pool attribute to rely on Ehcache defaults.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@transitclockIntegration/src/main/resources/ehcache.xml` at line 16, The
ehcache configuration references a non-existent thread pool via the attribute
thread-pool="pool-disk" on the ehcache:disk-store element; either add a
<thread-pools> section that declares a thread-pool with alias="pool-disk" (e.g.,
thread-pool alias="pool-disk" with appropriate min-size/max-size) or remove the
thread-pool="pool-disk" attribute from the ehcache:disk-store element to rely on
Ehcache defaults so the configuration is consistent.
- PlaybackModule: File.deleteOnExit() only removes *empty* directories, so the staged GTFS copy leaked across runs. Replaced with a shutdown hook that recursively deletes the staging root on JVM exit. - tools/wmata_capture/capture.py: dropped a redundant int() around round() — round(float) already returns int in Python 3. - tools/wmata_capture/README.md: reworded the confusing "restore @ignore removal into a real code change" bullet; the intent is simply that un-ignoring the test is part of the refresh, not a local throwaway. Skipped the rest: the ehcache pool-disk reference mirrors a pre-existing pattern in transitclock/ and transitclockPipelineTests/, so changing it only in the integration file would diverge from the "shadow of core" design. The run-all-tests `-am` comment is incorrect — the root pom's top-level <modules> list is always in the reactor, so core builds alongside the profile's extras (verified earlier). And the README code fence already matches the file's style (no fences in this README use language specifiers).
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
tools/wmata_capture/README.md (1)
53-56: Clarify the required working directory for the run commands.
uv run capture.pyonly works when invoked fromtools/wmata_capture/. If run from repo root, it should beuv run tools/wmata_capture/capture.py.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tools/wmata_capture/README.md` around lines 53 - 56, The README's run example is ambiguous about the required working directory: update the instructions to state that `uv run capture.py` must be executed from within the tools/wmata_capture/ directory (because .env is auto-loaded from tools/wmata_capture/.env), and show the alternative invocation `uv run tools/wmata_capture/capture.py` to use from the repo root; reference the script name capture.py and the tools/wmata_capture/ directory so readers know which path to use in each case.tools/wmata_capture/capture.py (1)
148-150: Consider validating ZIP member paths for defense-in-depth.Line 149 uses
extractall()on remote content. While ZIP-slip is a valid vulnerability class, the risk is mitigated here by downloading from a hardcoded, official WMATA API endpoint rather than untrusted archives. If you wish to follow defense-in-depth practices, the proposed path-validation helper would harden this further—but it is not critical given the trusted source.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tools/wmata_capture/capture.py` around lines 148 - 150, The code currently calls zf.extractall(gtfs_dir) on remote ZIP content; add defense-in-depth by validating ZIP members before extraction to prevent ZIP-slip. Implement a helper (e.g., safe_extract or is_within_directory) that iterates ZipFile.namelist()/infolist(), rejects or normalizes entries that contain absolute paths or path traversal components (".."), and only writes members whose resolved destination path is inside gtfs_dir; then replace zf.extractall(gtfs_dir) with this validated extraction routine in the same scope where ZipFile is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tools/wmata_capture/capture.py`:
- Around line 468-479: The CLI allows invalid numeric values; update the
argument validation so --poll-interval must be > 0 and --duration-hours must be
>= 0: add or use a small validator function (referenced where ap.add_argument is
called for "--poll-interval" and "--duration-hours") or validate after parsing
(e.g., in main/arg parsing block that defines ap) and exit with an error if
poll-interval <= 0 or duration-hours < 0, giving a clear message indicating
which flag is invalid.
- Around line 169-174: The log call reports block_id availability using "yes" if
mapping else "no", which is wrong when the block_id column exists but has no
values; change the code to detect the presence of the block_id column from the
CSV header instead of checking mapping length. When parsing trips.csv, record a
boolean like trips_has_block_id = ('block_id' in reader.fieldnames) (or set it
where the header is examined), then use that flag in the log.info call (replace
"yes" if mapping else "no" with "yes" if trips_has_block_id else "no") while
keeping mapping and trips_csv as-is.
- Around line 260-275: The code currently uses getattr() so unset protobuf
fields are indistinguishable from default values; instead check presence with
protobuf HasField to avoid writing bogus 0,0 coords or treating missing
timestamps as real zeros: call vehicle_msg.HasField("position") before accessing
position/latitude/longitude/bearing (and only then read pos =
vehicle_msg.position), and call vehicle_msg.HasField("timestamp") (or HasField
on the timestamp field name used in the proto) before using
vehicle_msg.timestamp so you can correctly fall back to header TS when absent;
update the logic around vehicle_msg, position, latitude/longitude and timestamp
to use these presence checks (referencing vehicle_msg, position, latitude,
longitude, bearing, timestamp and HasField).
In `@tools/wmata_capture/README.md`:
- Line 131: The README contains a non-runnable awk placeholder "$<block-col>" —
replace that placeholder with the actual CSV column index number used for the
block column, or modify the awk invocation to accept a variable (e.g., pass -v
col=N and use print $col) so the command is executable; update the example line
that currently shows "awk -F, '{print $<block-col>}'" to use either a concrete
column number or the -v variable form so readers can copy-and-run it.
---
Nitpick comments:
In `@tools/wmata_capture/capture.py`:
- Around line 148-150: The code currently calls zf.extractall(gtfs_dir) on
remote ZIP content; add defense-in-depth by validating ZIP members before
extraction to prevent ZIP-slip. Implement a helper (e.g., safe_extract or
is_within_directory) that iterates ZipFile.namelist()/infolist(), rejects or
normalizes entries that contain absolute paths or path traversal components
(".."), and only writes members whose resolved destination path is inside
gtfs_dir; then replace zf.extractall(gtfs_dir) with this validated extraction
routine in the same scope where ZipFile is used.
In `@tools/wmata_capture/README.md`:
- Around line 53-56: The README's run example is ambiguous about the required
working directory: update the instructions to state that `uv run capture.py`
must be executed from within the tools/wmata_capture/ directory (because .env is
auto-loaded from tools/wmata_capture/.env), and show the alternative invocation
`uv run tools/wmata_capture/capture.py` to use from the repo root; reference the
script name capture.py and the tools/wmata_capture/ directory so readers know
which path to use in each case.
🪄 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: 8a575848-eb95-40f6-8317-674c628476a6
📒 Files selected for processing (5)
.gitignoretools/wmata_capture/.env.exampletools/wmata_capture/README.mdtools/wmata_capture/capture.pytransitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java
✅ Files skipped from review due to trivial changes (2)
- .gitignore
- tools/wmata_capture/.env.example
🚧 Files skipped from review as they are similar to previous changes (1)
- transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java
| log.info( | ||
| "Loaded %d trip→block mappings from %s (trips.txt had block_id: %s)", | ||
| len(mapping), | ||
| trips_csv, | ||
| "yes" if mapping else "no", | ||
| ) |
There was a problem hiding this comment.
Fix the block_id availability log signal.
Line 173 uses mapping size to report whether trips.txt had a block_id column, which is misleading when the column exists but values are empty.
📝 Proposed fix
log.info(
"Loaded %d trip→block mappings from %s (trips.txt had block_id: %s)",
len(mapping),
trips_csv,
- "yes" if mapping else "no",
+ "yes" if has_block else "no",
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/wmata_capture/capture.py` around lines 169 - 174, The log call reports
block_id availability using "yes" if mapping else "no", which is wrong when the
block_id column exists but has no values; change the code to detect the presence
of the block_id column from the CSV header instead of checking mapping length.
When parsing trips.csv, record a boolean like trips_has_block_id = ('block_id'
in reader.fieldnames) (or set it where the header is examined), then use that
flag in the log.info call (replace "yes" if mapping else "no" with "yes" if
trips_has_block_id else "no") while keeping mapping and trips_csv as-is.
| pos = getattr(vehicle_msg, "position", None) | ||
| if pos is None: | ||
| return | ||
| lat = getattr(pos, "latitude", None) | ||
| lon = getattr(pos, "longitude", None) | ||
| if lat is None or lon is None: | ||
| return | ||
| heading = round(getattr(pos, "bearing", 0.0) or 0.0) | ||
|
|
||
| ts = int(getattr(vehicle_msg, "timestamp", 0) or 0) | ||
| if ts <= 0: | ||
| # No vehicle-reported timestamp — fall back to the feed header's ts. | ||
| # Caller doesn't have access here, so skip; header ts is used only | ||
| # as a last resort in the main loop. | ||
| return | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the file structure
find . -name "capture.py" -path "*/wmata_capture/*" | head -5Repository: OneBusAway/thetransitclock
Length of output: 102
🏁 Script executed:
# Read the actual file content around the specified lines
cat -n tools/wmata_capture/capture.py | sed -n '250,285p'Repository: OneBusAway/thetransitclock
Length of output: 1507
🏁 Script executed:
# Check the imports and message type definitions
cat -n tools/wmata_capture/capture.py | head -50Repository: OneBusAway/thetransitclock
Length of output: 1794
🏁 Script executed:
# Look for vehicle_msg declaration and type information
cat -n tools/wmata_capture/capture.py | grep -B5 -A5 "vehicle_msg"Repository: OneBusAway/thetransitclock
Length of output: 1813
🏁 Script executed:
# Look for protobuf imports and message type definitions
cat -n tools/wmata_capture/capture.py | grep -E "(import|from|Message|vehicle_msg)" | head -30Repository: OneBusAway/thetransitclock
Length of output: 1317
🏁 Script executed:
# Find the function that contains the reviewed code
cat -n tools/wmata_capture/capture.py | sed -n '235,260p'Repository: OneBusAway/thetransitclock
Length of output: 1071
🌐 Web query:
gtfs_realtime_pb2 VehiclePosition Position latitude longitude HasField proto3
💡 Result:
In GTFS Realtime, gtfs_realtime_pb2.VehiclePosition does not directly contain latitude/longitude fields. Latitude and longitude are fields of the nested message VehiclePosition.position (type Position). Proto fields (from gtfs-realtime.proto): - message VehiclePosition { optional Position position = 2; } - message Position { - required float latitude = 1; - required float longitude = 2; } Therefore: - VehiclePosition HasField('position') can tell you whether the nested Position message is present (in Python protobuf APIs, HasField works for sub-messages). - Once position is present, you can access position.latitude and position.longitude. - There is no HasField('latitude') on VehiclePosition; latitude is not a direct field of VehiclePosition. Grounding example from the official schema and language-binding docs: - GTFS Realtime VehiclePosition has optional Position position=2. - Position defines required latitude and required longitude. - Google’s Python binding docs show using entity.HasField('trip_update') / sub-message presence checks.
Citations:
- 1: https://gtfs.org/documentation/realtime/proto/
- 2: https://github.com/google/transit/blob/master/gtfs-realtime/proto/gtfs-realtime.proto
- 3: https://gtfs.org/de/documentation/realtime/language-bindings/python/
- 4: https://gtfs.org/documentation/realtime/feed-entities/vehicle-positions/
- 5: https://gtfs.org/documentation/realtime/reference/
🌐 Web query:
google.transit.gtfs_realtime_pb2 Position message optional fields
💡 Result:
The Position message in google.transit.gtfs_realtime_pb2 (from GTFS Realtime protobuf definition) has two required fields and four optional fields: Required fields: - latitude (float): Degrees North, in the WGS-84 coordinate system. - longitude (float): Degrees East, in the WGS-84 coordinate system. Optional fields: - bearing (float): Bearing, in degrees, clockwise from True North (0 is North, 90 is East). Can be compass bearing or direction to next stop. - odometer (double): Odometer value, in meters. - speed (float): Momentary speed measured by the vehicle, in meters per second. - Extensions 1000-1999 and 9000-9999: For 3rd-party extensions and private use. This structure is consistent across official sources including the GTFS Realtime .proto file.
Citations:
- 1: https://github.com/MobilityData/gtfs-realtime-bindings/blob/master/ruby/lib/google/transit/gtfs-realtime.pb.rb
- 2: https://github.com/google/transit/blob/master/gtfs-realtime/proto/gtfs-realtime.proto
- 3: https://gtfs.org/documentation/realtime/proto/
- 4: https://gtfs.org/documentation/realtime/reference/
- 5: https://raw.githubusercontent.com/google/transit/master/gtfs-realtime/proto/gtfs-realtime.proto
🌐 Web query:
proto3 required fields HasField() Python protobuf
💡 Result:
In proto3, there are no "required" fields like in proto2. All fields are optional by default, and scalar fields (int, bool, string, etc.) without the optional keyword do not track field presence—HasField returns ValueError for them, and they always appear to have their default value (e.g., 0 for int, false for bool, empty string). To enable field presence tracking (and use HasField) for scalar fields in proto3, mark them as optional (supported since protobuf 3.15+): syntax = "proto3"; message Foo { optional int32 bar = 1; // Now HasField('bar') works bool baz = 2; // No presence tracking; HasField('baz') raises ValueError } In Python generated code: foo = Foo print(foo.HasField('bar')) # False (unset) foo.bar = 0 print(foo.HasField('bar')) # True (explicitly set, even to default value) print(foo.HasField('baz')) # ValueError: Can't test field "Foo.baz" for presence in proto3 # For message fields, HasField always works (implicit presence) # For oneof fields, use WhichOneof HasField(field_name) checks if the field was explicitly set or parsed from wire (returns True) vs. unset (False, returns default). Use ClearField('bar') to unset. Singular message fields always have presence (HasField works). Repeated fields and maps do not. Deprecated fields ([deprecated=true]) are still present in generated code; use ListFields or descriptor to check schema presence, not HasField (which checks value presence).
Citations:
- 1: Optional Field Presence Check for Python in Protobuf3 (_pb2.py) protocolbuffers/protobuf#8933
- 2: https://protobuf.dev/programming-guides/field_presence/
- 3: https://protobuf.dev/programming-guides/field_presence
- 4: https://stackoverflow.com/questions/73793363/how-can-i-check-if-a-protobuf-message-has-a-field-defined
- 5: https://stackoverflow.com/questions/42622015/how-to-define-an-optional-field-in-protobuf-3
🌐 Web query:
python protobuf HasField required fields behavior
💡 Result:
In Python protobuf, the HasField(field_name) method checks if a field has been explicitly set (presence tracking). Behavior differs by proto syntax and field type: Proto2: - Works for all singular fields (scalars, messages). - Returns True if field was set to any value, including default (e.g., set int32 to 0 returns True). - Required fields must be set explicitly; defaults don't count as set for serialization. Proto3 (standard singular scalar fields): - Only works reliably for singular message fields (non-message scalars lack presence tracking). - For scalars (int, string, etc.), HasField may raise ValueError or not indicate presence; use value != default (e.g., int != 0) instead, but can't distinguish explicit default from unset. Proto3 optional fields (syntax="proto3"; optional field = 1; since ~protoc 3.12+): - Enables explicit presence tracking for scalars like proto2. - HasField('field') returns True if set (even to default), False if unset. - Use ClearField('field') to unset. Examples: # Proto3 optional int32 foo = 1; m = Msg m.HasField('foo') # False (unset) m.foo = 0 # Set to default m.HasField('foo') # True m.ClearField('foo') m.HasField('foo') # False ListFields lists only present fields (HasField True). No "required" fields in proto3; use business logic validation. As of 2026, no major changes to this behavior per recent protobuf releases.
Citations:
- 1: https://protobuf.dev/programming-guides/field_presence/
- 2: https://protobuf.dev/getting-started/pythontutorial/
- 3: https://googleapis.dev/python/protobuf/latest/google/protobuf/message.html
- 4: https://protobuf.dev/programming-guides/field_presence
- 5: Optional Field Presence Check for Python in Protobuf3 (_pb2.py) protocolbuffers/protobuf#8933
🌐 Web query:
gtfs-realtime.proto syntax proto2 or proto3 version
💡 Result:
The GTFS Realtime protocol buffer definition (gtfs-realtime.proto) uses proto2 syntax (it explicitly declares syntax = "proto2"). So you should compile it as proto2; there isn’t a proto3 version in the official schema itself. (Some community projects provide proto3-converted copies, but the official gtfs-realtime.proto is proto2.)
Citations:
- 1: https://raw.githubusercontent.com/google/transit/master/gtfs-realtime/proto/gtfs-realtime.proto
- 2: https://gtfs.org/documentation/realtime/proto/
- 3: https://github.com/google/transit/blob/master/gtfs-realtime/proto/gtfs-realtime.proto
🏁 Script executed:
# Check the actual protobuf message structure from gtfs-realtime-bindings
python3 << 'EOF'
try:
from google.transit import gtfs_realtime_pb2
# Create instances to check message structure
vehicle = gtfs_realtime_pb2.VehiclePosition()
position = gtfs_realtime_pb2.Position()
# Check descriptor for Position message
print("Position fields:")
for field in position.DESCRIPTOR.fields:
print(f" {field.name}: label={field.label} (1=optional, 2=required, 3=repeated), type={field.type}")
print("\nVehiclePosition fields:")
for field in vehicle.DESCRIPTOR.fields:
print(f" {field.name}: label={field.label} (1=optional, 2=required, 3=repeated), type={field.type}")
# Test HasField behavior on unset Position
print("\nTesting getattr on unset position:")
print(f" getattr(vehicle, 'position', None) = {getattr(vehicle, 'position', None)}")
print(f" vehicle.position = {vehicle.position}")
# Test HasField on position
try:
print(f" vehicle.HasField('position') = {vehicle.HasField('position')}")
except ValueError as e:
print(f" vehicle.HasField('position') raises: {e}")
# Test on actual position object
print("\nTesting on Position object:")
try:
print(f" position.HasField('latitude') = {position.HasField('latitude')}")
except ValueError as e:
print(f" position.HasField('latitude') raises: {e}")
print(f" getattr(position, 'latitude', None) = {getattr(position, 'latitude', None)}")
print(f" position.latitude = {position.latitude}")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
EOFRepository: OneBusAway/thetransitclock
Length of output: 253
🏁 Script executed:
# Install gtfs-realtime-bindings to inspect the actual message structure
pip install gtfs-realtime-bindings -q && python3 << 'EOF'
try:
from google.transit import gtfs_realtime_pb2
# Create instances to check message structure
vehicle = gtfs_realtime_pb2.VehiclePosition()
position = gtfs_realtime_pb2.Position()
# Check descriptor for Position message
print("Position fields:")
for field in position.DESCRIPTOR.fields:
label_names = {1: "optional", 2: "required", 3: "repeated"}
print(f" {field.name}: label={label_names.get(field.label, field.label)}")
print("\nVehiclePosition fields:")
for field in vehicle.DESCRIPTOR.fields:
label_names = {1: "optional", 2: "required", 3: "repeated"}
print(f" {field.name}: label={label_names.get(field.label, field.label)}")
# Test HasField behavior on unset Position
print("\nTesting getattr on unset position:")
print(f" getattr(vehicle, 'position', None) = {getattr(vehicle, 'position', None)}")
print(f" vehicle.position = {vehicle.position}")
print(f" type(vehicle.position) = {type(vehicle.position)}")
# Test HasField on position
try:
result = vehicle.HasField('position')
print(f" vehicle.HasField('position') = {result}")
except ValueError as e:
print(f" vehicle.HasField('position') raises: {e}")
# Test on actual position object
print("\nTesting on Position object (unset fields):")
try:
result = position.HasField('latitude')
print(f" position.HasField('latitude') = {result}")
except ValueError as e:
print(f" position.HasField('latitude') raises: {e}")
print(f" getattr(position, 'latitude', None) = {getattr(position, 'latitude', None)}")
print(f" position.latitude = {position.latitude}")
print(f" position.latitude == 0.0: {position.latitude == 0.0}")
# Test bearing (optional field)
print("\nTesting bearing (optional field):")
try:
result = position.HasField('bearing')
print(f" position.HasField('bearing') = {result}")
except ValueError as e:
print(f" position.HasField('bearing') raises: {e}")
print(f" position.bearing = {position.bearing}")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
EOFRepository: OneBusAway/thetransitclock
Length of output: 1083
🏁 Script executed:
# Let's verify the exact behavior of the current code logic
python3 << 'EOF'
from google.transit import gtfs_realtime_pb2
# Simulate the current code's logic
vehicle_msg = gtfs_realtime_pb2.VehiclePosition()
print("=== Current code logic (using getattr) ===")
pos = getattr(vehicle_msg, "position", None)
print(f"pos = getattr(vehicle_msg, 'position', None)")
print(f" pos = {repr(pos)}")
print(f" pos is None: {pos is None}")
print(f" type(pos): {type(pos)}")
if pos is None:
print(" Would return early (check fails - pos is not None!)")
else:
print(" Continues past first check")
lat = getattr(pos, "latitude", None)
lon = getattr(pos, "longitude", None)
print(f" lat = getattr(pos, 'latitude', None) = {lat}")
print(f" lon = getattr(pos, 'longitude', None) = {lon}")
print(f" lat is None or lon is None: {lat is None or lon is None}")
if lat is None or lon is None:
print(" Would return early (check fails - lat and lon are 0.0, not None!)")
else:
print(" Would continue with bogus 0,0 coordinates!")
print("\n=== Proposed fix logic (using HasField) ===")
if not vehicle_msg.HasField("position"):
print(" Would return early (HasField correctly detects unset position)")
else:
print(" Has position, continue...")
pos = vehicle_msg.position
if not pos.HasField("latitude") or not pos.HasField("longitude"):
print(" Would return early (HasField correctly detects unset lat/lon)")
else:
print(" Both lat and lon are set, safe to use")
# Now test with an actual set position
print("\n=== Testing with actual position set ===")
vehicle_msg2 = gtfs_realtime_pb2.VehiclePosition()
vehicle_msg2.position.latitude = 38.95778
vehicle_msg2.position.longitude = -77.03645
print(f"vehicle_msg2.HasField('position'): {vehicle_msg2.HasField('position')}")
pos2 = vehicle_msg2.position
print(f"pos2.HasField('latitude'): {pos2.HasField('latitude')}")
print(f"pos2.HasField('longitude'): {pos2.HasField('longitude')}")
print(f"pos2.latitude = {pos2.latitude}, pos2.longitude = {pos2.longitude}")
EOFRepository: OneBusAway/thetransitclock
Length of output: 766
Use protobuf presence checks to distinguish unset fields from defaults.
The current code using getattr() cannot distinguish between an unset position (or timestamp) and one with default values. For an unset position, getattr(vehicle_msg, "position", None) returns an empty Position object (not None), and accessing its fields like latitude returns 0.0 (not None). This causes the code to write bogus 0,0 coordinates. Similarly, an unset timestamp field returns 0, which is indistinguishable from an explicitly set timestamp of 0. Use HasField() instead to check if these fields were actually present in the feed.
Proposed fix
- pos = getattr(vehicle_msg, "position", None)
- if pos is None:
+ if not vehicle_msg.HasField("position"):
return
+ pos = vehicle_msg.position
- lat = getattr(pos, "latitude", None)
- lon = getattr(pos, "longitude", None)
- if lat is None or lon is None:
+ if not pos.HasField("latitude") or not pos.HasField("longitude"):
return
+ lat = pos.latitude
+ lon = pos.longitude
heading = round(getattr(pos, "bearing", 0.0) or 0.0)
- ts = int(getattr(vehicle_msg, "timestamp", 0) or 0)
- if ts <= 0:
+ if not vehicle_msg.HasField("timestamp"):
+ return
+ ts = int(vehicle_msg.timestamp)
+ if ts <= 0:
# No vehicle-reported timestamp — fall back to the feed header's ts.
# Caller doesn't have access here, so skip; header ts is used only
# as a last resort in the main loop.
return🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/wmata_capture/capture.py` around lines 260 - 275, The code currently
uses getattr() so unset protobuf fields are indistinguishable from default
values; instead check presence with protobuf HasField to avoid writing bogus 0,0
coords or treating missing timestamps as real zeros: call
vehicle_msg.HasField("position") before accessing
position/latitude/longitude/bearing (and only then read pos =
vehicle_msg.position), and call vehicle_msg.HasField("timestamp") (or HasField
on the timestamp field name used in the proto) before using
vehicle_msg.timestamp so you can correctly fall back to header TS when absent;
update the logic around vehicle_msg, position, latitude/longitude and timestamp
to use these presence checks (referencing vehicle_msg, position, latitude,
longitude, bearing, timestamp and HasField).
| ap.add_argument( | ||
| "--duration-hours", | ||
| type=float, | ||
| default=0.0, | ||
| help="Stop after this many hours. 0 or omitted = run until SIGINT.", | ||
| ) | ||
| ap.add_argument( | ||
| "--poll-interval", | ||
| type=float, | ||
| default=30.0, | ||
| help="Seconds between GTFS-RT polls (default 30).", | ||
| ) |
There was a problem hiding this comment.
Validate numeric CLI bounds for polling behavior.
--poll-interval currently accepts <= 0 and --duration-hours accepts negative values. That can cause tight retry loops or surprising runtime behavior.
🛠️ Proposed fix
+def _positive_float(value: str) -> float:
+ v = float(value)
+ if v <= 0:
+ raise argparse.ArgumentTypeError("must be > 0")
+ return v
+
+
+def _non_negative_float(value: str) -> float:
+ v = float(value)
+ if v < 0:
+ raise argparse.ArgumentTypeError("must be >= 0")
+ return v
+
+
def parse_args(argv: list[str]) -> argparse.Namespace:
@@
ap.add_argument(
"--duration-hours",
- type=float,
+ type=_non_negative_float,
default=0.0,
help="Stop after this many hours. 0 or omitted = run until SIGINT.",
)
@@
ap.add_argument(
"--poll-interval",
- type=float,
+ type=_positive_float,
default=30.0,
help="Seconds between GTFS-RT polls (default 30).",
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/wmata_capture/capture.py` around lines 468 - 479, The CLI allows
invalid numeric values; update the argument validation so --poll-interval must
be > 0 and --duration-hours must be >= 0: add or use a small validator function
(referenced where ap.add_argument is called for "--poll-interval" and
"--duration-hours") or validate after parsing (e.g., in main/arg parsing block
that defines ap) and exit with an error if poll-interval <= 0 or duration-hours
< 0, giving a clear message indicating which flag is invalid.
| `BatchCsvAvlFeedModule` + `BlockAssigner` handle both types, but some | ||
| transitclock behavior paths depend on block assignment specifically — | ||
| prefer capturing a route/period where `trips.txt` does carry block_ids | ||
| (check via `awk -F, '{print $<block-col>}' trips.txt | sort -u | head`). |
There was a problem hiding this comment.
Replace the non-runnable awk placeholder with an executable command.
$<block-col> is a placeholder and will fail if copied verbatim.
📘 Proposed doc fix
- (check via `awk -F, '{print $<block-col>}' trips.txt | sort -u | head`).
+ (check via `awk -F, 'NR==1{for(i=1;i<=NF;i++) if($i=="block_id") c=i; next} c && $c!="" {print $c}' trips.txt | sort -u | head`).📝 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.
| (check via `awk -F, '{print $<block-col>}' trips.txt | sort -u | head`). | |
| (check via `awk -F, 'NR==1{for(i=1;i<=NF;i++) if($i=="block_id") c=i; next} c && $c!="" {print $c}' trips.txt | sort -u | head`). |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/wmata_capture/README.md` at line 131, The README contains a
non-runnable awk placeholder "$<block-col>" — replace that placeholder with the
actual CSV column index number used for the block column, or modify the awk
invocation to accept a variable (e.g., pass -v col=N and use print $col) so the
command is executable; update the example line that currently shows "awk -F,
'{print $<block-col>}'" to use either a concrete column number or the -v
variable form so readers can copy-and-run it.
Summary by CodeRabbit
New Features
Documentation
Chores