From 108f457872de8a0c3c64d65ee1c36c04ce3bee5d Mon Sep 17 00:00:00 2001 From: ak2k <19240940+ak2k@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:03:29 -0400 Subject: [PATCH 1/2] flight-cli: test that envelope docs name a config error as an exit 2 Unit: work-3w2z9 --- tests/test_envelope_usage_error_docs.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_envelope_usage_error_docs.py b/tests/test_envelope_usage_error_docs.py index cbf2985b..1e4e5ce1 100644 --- a/tests/test_envelope_usage_error_docs.py +++ b/tests/test_envelope_usage_error_docs.py @@ -1,4 +1,4 @@ -"""`--format envelope` is written at exit 0 and 1; a usage error (exit 2) writes no document.""" +"""`--format envelope` is written at exit 0 and 1; exit 2 (usage or configuration) writes none.""" from __future__ import annotations @@ -10,7 +10,7 @@ from flight_cli import cli _ROOT = Path(__file__).resolve().parent.parent -_SAYS = "a usage error (exit 2) writes no document" +_SAYS = "exit 2 (a usage or configuration error) writes no document" _BULLET_DOCS = [ _ROOT / "README.md", _ROOT / ".claude" / "skills" / "flight-search" / "SKILL.md", @@ -21,7 +21,7 @@ def _flat(text: str) -> str: return " ".join(text.replace("│", " ").split()) -def test_the_format_help_says_a_usage_error_writes_no_document() -> None: +def test_the_format_help_says_exit_2_writes_no_document() -> None: for command in ("search", "calendar"): result = CliRunner().invoke(cli.app, [command, "--help"], env={"COLUMNS": "200"}) flat = _flat(result.stdout) @@ -31,7 +31,7 @@ def test_the_format_help_says_a_usage_error_writes_no_document() -> None: @pytest.mark.parametrize("doc", _BULLET_DOCS, ids=lambda p: p.name) -def test_the_envelope_bullet_says_a_usage_error_writes_no_document(doc: Path) -> None: +def test_the_envelope_bullet_says_exit_2_writes_no_document(doc: Path) -> None: (bullet,) = ( ln for ln in doc.read_text().splitlines() if ln.startswith("- `--format envelope`") ) @@ -45,3 +45,11 @@ def test_the_envelope_note_title_names_the_exit_codes_that_write_one() -> None: assert "at exit 0 and 1" in title, title assert "on every path" not in title, title + + +def test_the_envelope_note_and_the_run_docstring_name_both_causes_of_exit_2() -> None: + note = _flat((_ROOT / "docs" / "memories" / "envelope.md").read_text()) + run_doc = _flat((_ROOT / "src" / "flight_cli" / "_envelope.py").read_text()) + + assert "Exit 2 is a usage or configuration error and writes none." in note, note + assert "that is a usage or configuration error, and the command never ran." in run_doc From 49586bb7d4da286c0f4eae3f62afb23f21ff9fb5 Mon Sep 17 00:00:00 2001 From: ak2k <19240940+ak2k@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:04:05 -0400 Subject: [PATCH 2/2] flight-cli: envelope docs say exit 2 is a usage or configuration error A malformed config.toml also exits 2 with no envelope, so the --format help, README, skill, envelope note and run's docstring name both causes. Unit: work-3w2z9 --- .claude/skills/flight-search/SKILL.md | 2 +- README.md | 2 +- docs/memories/envelope.md | 2 +- src/flight_cli/_envelope.py | 2 +- src/flight_cli/cli.py | 3 ++- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.claude/skills/flight-search/SKILL.md b/.claude/skills/flight-search/SKILL.md index 35fe63af..42812cb1 100644 --- a/.claude/skills/flight-search/SKILL.md +++ b/.claude/skills/flight-search/SKILL.md @@ -24,7 +24,7 @@ intent into the right invocation **on the first try**. Global flags (every search-printing command): - `-v` / `-vv` — verbose logging (cache hits, retries) to stderr -- `--format envelope` — `search` and `calendar`: one JSON object with the same keys whichever path answered (`version`, `command`, `backend`, `currency`, `complete`, `notes`, `results`, `awards`, `insight`, `price_history`, `facets`, `price_graph`, `verify`, `cross_check`, `split_ticket`). It is written at exit 0 and 1; a usage error (exit 2) writes no document. Use it whenever the output is read by a program: `complete: false` means the answer is narrower than asked (on a calendar, also departure dates Matrix priced no fare on, each named in `notes`), and `notes` says why. A calendar's result rows carry their `departure` and `return` dates beside `row`. On a Google search, `facets` lists what Google's filters offer (fare, trip-length and layover ranges, airlines, alliances, connecting airports): read it to choose a carrier, alliance, `--max-price`, `MAXDUR` or `MAXCONNECT` before a second search. Add `--gf-transport browser` or `auto` to a calendar to read Google's price graph into `price_graph` (opens a headless Chrome); without either the key is empty and its note says so +- `--format envelope` — `search` and `calendar`: one JSON object with the same keys whichever path answered (`version`, `command`, `backend`, `currency`, `complete`, `notes`, `results`, `awards`, `insight`, `price_history`, `facets`, `price_graph`, `verify`, `cross_check`, `split_ticket`). It is written at exit 0 and 1; exit 2 (a usage or configuration error) writes no document. Use it whenever the output is read by a program: `complete: false` means the answer is narrower than asked (on a calendar, also departure dates Matrix priced no fare on, each named in `notes`), and `notes` says why. A calendar's result rows carry their `departure` and `return` dates beside `row`. On a Google search, `facets` lists what Google's filters offer (fare, trip-length and layover ranges, airlines, alliances, connecting airports): read it to choose a carrier, alliance, `--max-price`, `MAXDUR` or `MAXCONNECT` before a second search. Add `--gf-transport browser` or `auto` to a calendar to read Google's price graph into `price_graph` (opens a headless Chrome); without either the key is empty and its note says so - `--format csv` (`search`) — a header row, then one row per itinerary the table lists, for a spreadsheet or a script that reads no JSON: idx (the table's row number), total_price (a plain decimal with no currency code; for a party, the party's total, empty where Matrix states none), currency, num_stops (summed over the slices), total_duration_min (the slices' durations summed), carriers (each code once, in flight order, joined by `;`), flight_numbers (every flight in slice order, joined by `;`), origin and destination (the first slice's), dep_date (the first slice's local departure date, YYYY-MM-DD). Cash fares only: no award provider is asked, and `--awards-only`, several `--cabin` values, `--slice`, `--split`, `--sellers`, `--fare-rules`, `--verify` and `--enrich` are usage errors (exit 2). An answer with no rows writes the header alone; a failed search writes nothing. `calendar` and `detail` refuse it - `--format json` — the answering path's own shape (Google Flights rows, Matrix's raw response, `{cabin: …}`, or the award document; a calendar under `--gf-transport browser` or `auto` adds `google_price_graph` to Matrix's body); `--json` is a deprecated alias for it - Several cabins (`--cabin economy,business`) — one table, a row per itinerary and a price column per cabin, sorted by `--sort` (default the first cabin). Every cabin is priced on the sort cabin's itineraries, so another cabin's own cheapest can be on no row; a line under the table then names it, e.g. `J's own cheapest: USD2508.00 (UA2831+UA1509 / UA373+UA2282), on no row above; --sort business lists J's cheapest first.` For a party it is priced as the table's cells are and says so: `J's own cheapest, total for 2 travelers: …`, or `, per traveler: …` where Matrix states no total. On Google, `--format json` and `--format envelope` list each cabin's `-n` cheapest rows, then every other row whose fare the table or the line under it prints in that cabin, in price order; on Matrix, each cabin's whole answer diff --git a/README.md b/README.md index d35a1bd9..160ffbff 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Every result-printing command supports: - `--fare-rules` (`search`) — after the table, print itinerary `--pick N`'s fare basis, booking codes and fare rules (penalties, changes, refunds) from Matrix - `--verify` (`search`, Google Flights) — after the table, ask Matrix for itinerary `--pick N` as exactly that itinerary (its flights by number, each on its own day and minute, between its airports) and print Matrix's price beside Google's with the fare basis, booking codes and fare rules; or say why Matrix does not price it: those flights only on another itinerary, no fare, or that none of the trips Matrix returned for that route and day names its carrier. For a party, Matrix's price is the party's total, as Google's is, or one passenger's marked per traveler where Matrix states no total. With `--format json` the document is `{"search": [...], "verify": {...}}`, and `--format envelope` carries the same object under `verify`; `verify.matrix.price` and `verify.delta` (Google's price minus Matrix's) are the party's, and `verify.matrix.per_traveler` is one passenger's - `--no-separate-tickets` (`search`, Google Flights) — hide the itineraries Google sells as separate tickets, which every search that shows Google rows otherwise adds from Google's Cheapest tab, marked `†` (`‡` for a self transfer, where bags are rechecked between flights) on the Google, merged and multi-cabin tables or `separate_tickets: true`, a round trip as its outbound alone at Google's round-trip total, never priced against Matrix (cross-check reason `separate_tickets`) and skipped, with a reason, by awards, `--sellers`, `--verify` and pinned links; on a multi-city `--slice` search, no one-way tickets are asked for -- `--format envelope` (`search`, `calendar`) — for agents and scripts: one JSON object with the same keys whichever path answered (`version`, `command`, `backend`, `currency`, `complete`, `notes`, `results`, `awards`, `insight`, `price_history`, `facets`, `price_graph`, `verify`, `cross_check`, `split_ticket`). It is written at exit 0 and 1; a usage error (exit 2) writes no document. `complete` is false when the answer is narrower than asked, such as a cabin, a calendar sub-query, a departure date Matrix priced no fare on, an award provider or Google's Cheapest tab lost, and `notes` carries what stderr said. A calendar's result rows carry their `departure` and `return` dates beside `row`. A Google search's `facets` are what Google's filters offer for it (fare, trip-length and layover ranges, airlines, alliances, connecting airports), to choose a carrier, alliance, `--max-price`, `MAXDUR` or `MAXCONNECT` without a second search. A calendar's `price_graph` is Google's estimate per date pair, read under `--gf-transport browser` or `auto`. Schema: [`docs/envelope.schema.json`](docs/envelope.schema.json) +- `--format envelope` (`search`, `calendar`) — for agents and scripts: one JSON object with the same keys whichever path answered (`version`, `command`, `backend`, `currency`, `complete`, `notes`, `results`, `awards`, `insight`, `price_history`, `facets`, `price_graph`, `verify`, `cross_check`, `split_ticket`). It is written at exit 0 and 1; exit 2 (a usage or configuration error) writes no document. `complete` is false when the answer is narrower than asked, such as a cabin, a calendar sub-query, a departure date Matrix priced no fare on, an award provider or Google's Cheapest tab lost, and `notes` carries what stderr said. A calendar's result rows carry their `departure` and `return` dates beside `row`. A Google search's `facets` are what Google's filters offer for it (fare, trip-length and layover ranges, airlines, alliances, connecting airports), to choose a carrier, alliance, `--max-price`, `MAXDUR` or `MAXCONNECT` without a second search. A calendar's `price_graph` is Google's estimate per date pair, read under `--gf-transport browser` or `auto`. Schema: [`docs/envelope.schema.json`](docs/envelope.schema.json) - `--format csv` (`search`) — for a spreadsheet or a script that reads no JSON: a header row, then one row per itinerary the table lists. Columns: idx (the table's row number), total_price (a plain decimal with no currency code; for a party, the party's total, empty where Matrix states none), currency, num_stops (summed over the slices), total_duration_min (the slices' durations summed), carriers (each code once, in flight order, joined by `;`), flight_numbers (every flight in slice order, joined by `;`), origin and destination (the first slice's), dep_date (the first slice's local departure date, YYYY-MM-DD). The search takes the path `--format json` takes, and no link line or note goes to stdout. Cash fares only: no award provider is asked, and `--awards-only`, several `--cabin` values, `--slice`, `--split`, `--sellers`, `--fare-rules`, `--verify` and `--enrich` are usage errors (exit 2). An answer with no rows writes the header alone; a failed search writes nothing. `calendar` and `detail` refuse it - `--format json` — the answering path's own document: Google Flights rows, Matrix's raw response (a calendar's with `google_price_graph` beside it under `--gf-transport browser` or `auto`), `{cabin: …}` for several cabins, or the award document when awards run. `--json` is a deprecated alias for it - `--no-cache` — bypass the on-disk response cache (`~/.cache/flight-cli/`) diff --git a/docs/memories/envelope.md b/docs/memories/envelope.md index eb0c6b83..c724a942 100644 --- a/docs/memories/envelope.md +++ b/docs/memories/envelope.md @@ -64,7 +64,7 @@ with no such trip length. A Google cell's are its own `departure` and `return`. Exit codes are those of `--format json` in the same state, and an envelope is -written at exit 0 and 1. Exit 2 is a usage error and writes none. +written at exit 0 and 1. Exit 2 is a usage or configuration error and writes none. ## What makes `complete` false diff --git a/src/flight_cli/_envelope.py b/src/flight_cli/_envelope.py index 53f2d1f3..1446a7d0 100644 --- a/src/flight_cli/_envelope.py +++ b/src/flight_cli/_envelope.py @@ -406,7 +406,7 @@ def run( ) -> None: """Run `call` as an envelope run, then write its one envelope to stdout. - No envelope at exit 2: that is a usage error, and the command never ran. + No envelope at exit 2: that is a usage or configuration error, and the command never ran. With `csv_out` the run writes the recorded table as CSV instead, at exit 0 alone: a failed run writes nothing, so a header never stands for an answer. `consoles` print one stderr line per message for the run, so each message diff --git a/src/flight_cli/cli.py b/src/flight_cli/cli.py index 4d88a173..a7c22950 100644 --- a/src/flight_cli/cli.py +++ b/src/flight_cli/cli.py @@ -11501,7 +11501,8 @@ def _resolve_gf_transport(mode: str) -> GfTransportMode: _ENVELOPE_FORMAT_CHOICES = "/".join(_ENVELOPE_FORMATS) _ENVELOPE_FORMAT_HELP = ( "envelope is one versioned " - "JSON document, written at exit 0 and 1; a usage error (exit 2) writes no document. " + "JSON document, written at exit 0 and 1; exit 2 (a usage or configuration error) " + "writes no document. " "Its keys, whichever path answered: version, command, backend, currency, complete, " "notes, results, awards, insight, price_history, facets, price_graph, verify, " "cross_check, split_ticket. complete is false when the answer is narrower than "