From 4832f724a1bf4421be185bcc5e7173d24640cc2b Mon Sep 17 00:00:00 2001 From: Ayush Saraswat Date: Tue, 26 May 2026 14:34:42 -0400 Subject: [PATCH 1/3] fix: live booking canary tolerates Google's empty-options responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The booking canary picked the first itinerary with a booking_token + selected legs and failed if Google returned zero booking options for that one — even when the RPC itself succeeded. That made the canary fail ~weekly on schedule for two months while the actual code worked end-to-end. - _bookable_candidates returns up to 5 candidates (was: first match only). - Test iterates candidates and passes if any returns booking options; fails only when all came back empty. - get_booking_results now logs a WARNING when the RPC succeeds with 0 options so artifact triage can distinguish upstream variance from parser regressions. Co-Authored-By: Claude Opus 4.7 (1M context) --- swoop/rpc.py | 9 ++++++++- tests/test_live_contract.py | 34 ++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/swoop/rpc.py b/swoop/rpc.py index c807c39..1a08ec7 100644 --- a/swoop/rpc.py +++ b/swoop/rpc.py @@ -621,11 +621,18 @@ def get_booking_results( transport=transport, ) - return _parse_booking_rpc_response( + options = _parse_booking_rpc_response( res.text, registry_version=registry_version, required_keys=required_keys, ) + if not options: + logger.warning( + "get_booking_results %s->%s on %s returned 0 options " + "(upstream RPC succeeded; token may be stale or itinerary unbookable)", + origin, destination, date, + ) + return options def get_trip_booking_results( diff --git a/tests/test_live_contract.py b/tests/test_live_contract.py index b2eca00..233bbf4 100644 --- a/tests/test_live_contract.py +++ b/tests/test_live_contract.py @@ -235,15 +235,18 @@ def _record_booking_artifacts(case_id: str, rpc_captures: list[dict[str, str]], ) -def _find_bookable_itinerary(search_result: Any) -> Itinerary: +def _bookable_candidates(search_result: Any, *, limit: int = 5) -> list[Itinerary]: + candidates: list[Itinerary] = [] for option in search_result.results: for leg in option.legs: itinerary = leg.itinerary if itinerary is None: continue if itinerary.booking_token and rpc._build_selected_legs(itinerary): - return itinerary - raise AssertionError("Expected at least one itinerary with booking token and selected legs") + candidates.append(itinerary) + if len(candidates) >= limit: + return candidates + return candidates class TestShoppingContract: @@ -317,16 +320,27 @@ def test_booking_results_parseable_and_artifacted(self, monkeypatch: pytest.Monk ) assert search_result.results, "Expected at least one itinerary before live booking lookup" - itinerary = _find_bookable_itinerary(search_result) + candidates = _bookable_candidates(search_result) + assert candidates, "Expected at least one itinerary with booking token and selected legs" + rpc_captures = _capture_rpc_texts(monkeypatch) - options = rpc.get_booking_results( - itinerary, - registry_version=date.today().isoformat(), - transport=TransportConfig(timeout=30, retries=1), - ) + options: list[Any] = [] + itinerary: Itinerary | None = None + for candidate in candidates: + options = rpc.get_booking_results( + candidate, + registry_version=date.today().isoformat(), + transport=TransportConfig(timeout=30, retries=1), + ) + if options: + itinerary = candidate + break assert rpc_captures, "Expected a live booking RPC capture" - assert options, "Expected at least one booking option from live booking lookup" + assert options and itinerary is not None, ( + f"Expected at least one booking option after trying {len(candidates)} " + "itineraries (upstream returned empty for every candidate)" + ) for option in options[:5]: assert option.price > 0 From 530e7bf050167575d5c485d30c63671d4b7ee891 Mon Sep 17 00:00:00 2001 From: Ayush Saraswat Date: Tue, 26 May 2026 14:34:47 -0400 Subject: [PATCH 2/3] ci: bump checkout/setup-python/upload-artifact to Node 24 versions Node 20 is deprecated on GitHub-hosted runners as of June 2026. Move the canary and mutation workflows to checkout@v5, setup-python@v6, and upload-artifact@v5 so we stop emitting the deprecation warning. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/live-canary.yml | 6 +++--- .github/workflows/mutation.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/live-canary.yml b/.github/workflows/live-canary.yml index 2ec2640..3c540f8 100644 --- a/.github/workflows/live-canary.yml +++ b/.github/workflows/live-canary.yml @@ -10,8 +10,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies @@ -24,7 +24,7 @@ jobs: run: python -m pytest tests/test_live_contract.py -v -m live - name: Upload live canary artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: live-canary-artifacts path: ${{ runner.temp }}/swoop-live-artifacts diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index ba52ddf..d6510a9 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -7,8 +7,8 @@ jobs: mutmut: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies @@ -21,7 +21,7 @@ jobs: run: mutmut results > mutmut-results.txt || true - name: Upload mutation results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: mutmut-results path: mutmut-results.txt From 0a64993f55e353f1b1efe808dd2dfcf187280463 Mon Sep 17 00:00:00 2001 From: Ayush Saraswat Date: Tue, 26 May 2026 14:34:51 -0400 Subject: [PATCH 3/3] ci: release titles use bare vX.Y.Z; bump action versions The release-create step inherited GITHUB_REF_NAME as the title, so v0.5.0 ended up published as 'swoop-v0.5.0' while v0.4.x were titled 'v0.4.0' / 'v0.4.1'. Pass --title v${VERSION} explicitly so future releases stay consistent. Tag still ships as swoop-vX.Y.Z (publish job and PyPI parser depend on that prefix). Also bump checkout/setup-python to Node 24-compatible versions to silence the deprecation warning on every CI run. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfb5bbb..6c256f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ jobs: matrix: python-version: ["3.10", "3.13"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -26,8 +26,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install build tools @@ -55,8 +55,8 @@ jobs: id-token: write contents: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install build tools @@ -71,10 +71,12 @@ jobs: id: changelog run: | VERSION="${GITHUB_REF_NAME#swoop-v}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" # Extract the section between this version's header and the next version header awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md > release-notes.md cat release-notes.md - name: Create GitHub Release - run: gh release create "${{ github.ref_name }}" --notes-file release-notes.md + run: gh release create "${{ github.ref_name }}" --title "v${VERSION}" --notes-file release-notes.md env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.changelog.outputs.version }}