diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e9b26..9cb630e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,22 @@ A running server reports its version at `/health`. ## [Unreleased] +## [0.4.7] - 2026-11-12 + +### Fixed + +- SimBrief generation no longer clicks Route Finder. When no route was agreed, the form is + filled and Generate Flight is clicked directly, keeping SimBrief's own route suggestion. + Route Finder had hung past its 30-second wait on KEWR–EIDW, so Generate was never reached + and the leg failed with `TimeoutError`. A local live check of KEWR–EIDW (B752, no route) + generated in 35 seconds with a NAT-track route ending in the OLAP4L arrival. +- Removed the pre-Generate stop for an empty or direct-only automatic route; the generated + OFP's route is reported in the briefing. Explicit routes are still filled and must match. +- Plan verification ignores the explicit `DCT` tokens SimBrief's OFP writes between fixes + (`DCT MERIT DCT HFD`) when comparing with the form's route (`MERIT HFD`). Without this the + SimBrief-chosen KEWR–EIDW route failed finalization as `plan_mismatch`. Any other fix or + airway difference still fails. + ### Documentation - Refresh the README for v0.4.6: SimBrief sign-in is on demand (no daily keep-alive), approval @@ -279,6 +295,7 @@ First public release, and the first version live on the self-hosted Cloudflare d of blocking it permanently ([#3](https://github.com/IntelIP/InfiniteFlightAgent/pull/3)). [Unreleased]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.6...HEAD +[0.4.7]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.6...v0.4.7 [0.4.6]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.5...v0.4.6 [0.4.5]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.4...v0.4.5 [0.4.4]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.3...v0.4.4 diff --git a/docs/backlog.md b/docs/backlog.md index cfaf562..aff2083 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -18,7 +18,7 @@ This proves the exercised dispatch integration, not simulator import or aircraft | Surface | Current capability | Remaining work | | --- | --- | --- | | Foundation — SETUP-01, BUILD-01/02 | Locked Python application, Deep Agent, MCP, transactional storage/checkpoints, CI and product-validation manifest are installed; deployed generation is observed | Whole-operation progress, cancellation/deadlines and intended-client acceptance remain separate tasks | -| SimBrief — DISC-03/06, BUILD-04 | Browser generation, Route Finder, exact-input approval/readback, retained route/navlog correlation and two retained legs are observed | More dispatch inputs and broader live-provider coverage; controlled recovery scenarios do not prove every provider option | +| SimBrief — DISC-03/06, BUILD-04 | Browser generation (SimBrief-chosen route when none is agreed), exact-input approval/readback, retained route/navlog correlation and two retained legs are observed | More dispatch inputs and broader live-provider coverage; controlled recovery scenarios do not prove every provider option | | Flight research — BUILD-03 | Dated FlightAware research, connection checks, reported gates and explicitly unverified spawn suggestions are implemented | Verified simulator stand mapping, private configuration evidence and import behavior | | Thrust — BUILD-05, VERIFY-01 | Labeled A20N/A21N/B38M estimates and condition-specific observed N1 calibration are implemented | Zero aircraft have verified support under the accepted definition; engine/build applicability and simulator evidence remain open | | Trip management and evaluation | Saved legs/briefings, approved revisions, recoverable validation/provider guidance and a matched 28-case whole-conversation MCP/recovery suite are shipped | Durable progress/cancellation and more dispatch/configuration inputs remain planned | diff --git a/docs/implementation/agent-usage.md b/docs/implementation/agent-usage.md index 85897a6..31820c7 100644 --- a/docs/implementation/agent-usage.md +++ b/docs/implementation/agent-usage.md @@ -143,7 +143,7 @@ in force: never regenerate merely because waiting timed out. | Idle between browser operations | 30 seconds, checked at one-second intervals | | Browserbase HTTP request | 8 seconds; no automatic SDK retries | | Playwright navigation / ordinary action | 20 / 10 seconds | -| Route Finder wait | 30 seconds | +| Airport/date lookup wait before Generate | 30 seconds | | Generate completion polling | At most 60 iterations, with the session deadline still enforced | | Owner-thread cleanup wait | 12 seconds, then remote release | diff --git a/pyproject.toml b/pyproject.toml index b9b3abe..6098bc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "infinite-flight-dispatcher" -version = "0.4.6" +version = "0.4.7" description = "AI dispatcher for Infinite Flight: real flights, SimBrief plans, and takeoff N1 estimates over MCP" requires-python = ">=3.11" license = "GPL-3.0-only" diff --git a/src/ifdispatcher/core.py b/src/ifdispatcher/core.py index 7da6ad1..72f0975 100644 --- a/src/ifdispatcher/core.py +++ b/src/ifdispatcher/core.py @@ -200,9 +200,7 @@ def save(self, trip: str, leg: str, revision: int, raw: bytes, static_id: str | meta = json.loads(meta_row[0]) if meta_row else {} readback = ((meta.get("generation") or {}).get("values") or {}).get("route") route = (expected.get("route") or "").strip() or readback - if route and " ".join(route.upper().split()) != " ".join( - (normalized["flight_route"] or "").upper().split() - ): + if route and _route_tokens(route) != _route_tokens(normalized["flight_route"]): raise ValueError( "identity_mismatch: route differs from approved input or form readback" ) @@ -422,6 +420,13 @@ def open_store() -> Store: FETCHER = "https://www.simbrief.com/api/xml.fetcher.php" +def _route_tokens(route: str | None) -> list[str]: + """Route fixes/airways for comparison. SimBrief's OFP writes direct legs as explicit + `DCT` tokens (`DCT MERIT DCT HFD`) that its form shows without (`MERIT HFD`); a `DCT` + between two fixes means the same route either way.""" + return [token for token in (route or "").upper().split() if token != "DCT"] + + def _fetch_params(alias: str | None, static_id: str | None, userid: str | None) -> dict: if not alias and not userid: raise ValueError("missing_configuration: set SIMBRIEF_ALIAS or SIMBRIEF_USERID") diff --git a/src/ifdispatcher/simbrief_browser.py b/src/ifdispatcher/simbrief_browser.py index 4cdf385..65f5219 100644 --- a/src/ifdispatcher/simbrief_browser.py +++ b/src/ifdispatcher/simbrief_browser.py @@ -653,7 +653,7 @@ def form_values(fields: dict[str, Any]) -> dict[str, str]: def form_mismatches(fields: dict, want: dict, got: dict) -> dict: - """Unspecified routes use Route Finder; explicitly agreed fields must match.""" + """Unspecified routes are left to SimBrief; explicitly agreed fields must match.""" expected = dict(want) if not (fields.get("route") or "").strip(): expected.pop("route", None) @@ -728,15 +728,13 @@ def on_dialog(dialog): page.select_option("#origrwy", fields["runway"]) if not open_prompts: # Airport/date/runway lookups can replace the route asynchronously. - # Finish those first, then apply the approved route or run Route Finder. + # Finish those first, then apply the approved route. Without an agreed route + # the form goes straight to Generate Flight: SimBrief chooses the route itself. + # Route Finder is never clicked; it hung past its wait on oceanic legs. page.wait_for_function("() => jQuery.active === 0", timeout=30_000) open_prompts = page.evaluate(VISIBLE_MODALS_JS) - if not open_prompts: - if want["route"]: - page.fill("#route", want["route"]) - else: - page.get_by_text("Find Route", exact=True).click() - page.wait_for_function("() => jQuery.active === 0", timeout=30_000) + if not open_prompts and want["route"]: + page.fill("#route", want["route"]) open_prompts = page.evaluate(VISIBLE_MODALS_JS) got = {k: page.input_value(f"#{k}") for k in want} got["tlr"] = page.evaluate("() => document.getElementById('tlr').checked") @@ -756,13 +754,6 @@ def on_dialog(dialog): return {**result, "status": "needs_review", "prompts": open_prompts + dialogs} if mismatched: return {**result, "status": "fill_mismatch", "mismatched": mismatched} - if not want["route"] and got["route"].strip().upper() in ("", "DCT", "DIRECT"): - return { - **result, - "status": "needs_review", - "reason": "Route Finder returned no waypoint routing. Supply a route or " - "explicitly approve DCT before generating.", - } if not click: return {**result, "status": "filled"} if before_click: diff --git a/tests/test_routing_evidence.py b/tests/test_routing_evidence.py index 4336d01..aa9273d 100644 --- a/tests/test_routing_evidence.py +++ b/tests/test_routing_evidence.py @@ -125,3 +125,33 @@ def test_missing_route_and_navlog_remain_unavailable(tmp_path, monkeypatch): assert result["synopsis"]["navlog_fix_count"] is None assert result["synopsis"]["navlog_idents"] is None assert "Route:** unavailable" in result["briefing_markdown"] + + +READBACK = "MERIT HFD PUT BOS TUSKY FORTE NICSO NATS BEXET OLAPO OLAP4L" + + +@pytest.mark.parametrize( + "returned,matches", + [ + ( + "DCT MERIT DCT HFD DCT PUT DCT BOS DCT TUSKY DCT FORTE DCT NICSO NATS BEXET " + "DCT OLAPO OLAP4L", + True, + ), + ("DCT MERIT DCT HFD DCT PUT DCT BOS DCT TUSKY DCT FORTE DCT NICSO NATS BEXET", False), + ("DCT", False), + ], +) +def test_automatic_route_ignores_only_explicit_dct_tokens(tmp_path, returned, matches): + """Observed live (KEWR-EIDW, v0.4.7 check): the form shows SimBrief's route without + DCT tokens, and the OFP writes the same route with explicit DCT between fixes.""" + store = Store(tmp_path) + prepared(store) + meta = store.read_meta("routing", "1", 1) + meta["generation"] = {"status": "generated", "values": {"route": READBACK}} + store.save_meta("routing", "1", 1, meta) + if matches: + assert store.save("routing", "1", 1, ofp(returned), None)["data"]["flight_route"] + else: + with pytest.raises(ValueError, match="identity_mismatch: route"): + store.save("routing", "1", 1, ofp(returned), None) diff --git a/tests/test_simbrief_browser.py b/tests/test_simbrief_browser.py index f10ccb6..c689e77 100644 --- a/tests/test_simbrief_browser.py +++ b/tests/test_simbrief_browser.py @@ -90,16 +90,22 @@ def test_keepalive_route_respects_the_kill_switch(monkeypatch): @pytest.mark.parametrize( "route,found,prompts,timeout,status,finder_clicks", [ - (None, "DOTSS2 CLEEE BLH J169 KOFFA SNDBR3", [], False, "generated", 1), - (" ", "JCOBY4 SWANN PHLBO4", [], False, "generated", 1), + # No agreed route: SimBrief's own route suggestion stays and Generate is clicked + # directly. Route Finder is never clicked (it hung past its wait on KEWR-EIDW). + ( + None, + "MERIT HFD PUT BOS TUSKY FORTE NICSO NATS BEXET OLAPO OLAP4L", + [], + False, + "generated", + 0, + ), + (" ", "JCOBY4 SWANN PHLBO4", [], False, "generated", 0), + (None, "", [], False, "generated", 0), ("DCT", "unused", [], False, "generated", 0), ("JCOBY4 SWANN PHLBO4", "unused", [], False, "generated", 0), - (None, "", [], False, "needs_review", 1), - (None, "DCT", [], False, "needs_review", 1), - (None, "", ["No route found"], False, "needs_review", 1), - (None, "EARLY SUGGESTION", ["Native: Route failed"], False, "needs_review", 1), (None, "", ["No Alternates Found"], False, "needs_review", 0), - (None, "", [], True, "timeout", 1), + (None, "", [], True, "timeout", 0), ], ) def test_generation_waits_for_routing_before_claiming_generate( @@ -149,21 +155,18 @@ def dispatch_event(self, *args): pass def wait_for_function(self, script, **kwargs): - if self.pending_route: + if "jQuery.active" in script: if timeout: - raise TimeoutError("Route Finder did not finish") - self.values["route"] = found - self.pending_route = False + raise TimeoutError("Airport lookups did not finish") + if not self.values["route"]: + # SimBrief's own suggestion after the airport/date lookups finish. + self.values["route"] = found def evaluate(self, script, arg=None): if script == simbrief_browser.SET_DATE_JS: self.values["date"] = "09 Nov 2026 - 15:00" elif script == simbrief_browser.VISIBLE_MODALS_JS: - return ( - [p for p in prompts if not p.startswith("Native:")] - if self.finder_clicks or "Alternates" in " ".join(prompts) - else [] - ) + return list(prompts) elif script == simbrief_browser.SET_CHECKBOX_JS: self.values["tlr"] = arg[1] else: @@ -245,8 +248,6 @@ def confirm_time(page): assert result["values"]["route"] == ( route.strip() if route and route.strip() else found ) - elif not prompts: - assert "explicitly approve DCT" in result["reason"] assert page.finder_clicks == finder_clicks assert page.generate_clicks == len(claims) == int(status == "generated") assert page.dialog_callback is None diff --git a/uv.lock b/uv.lock index 89f0be8..c21cc93 100644 --- a/uv.lock +++ b/uv.lock @@ -720,7 +720,7 @@ wheels = [ [[package]] name = "infinite-flight-dispatcher" -version = "0.4.6" +version = "0.4.7" source = { editable = "." } dependencies = [ { name = "boto3" },