From 02f43285f0191d2ef4ea72c3cb13c0d545cb20e8 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Sat, 7 Feb 2026 01:10:57 +0000 Subject: [PATCH 1/7] Support flights as planned and snap mock_uss flight start times to now --- .basedpyright/baseline.json | 210 ------------------ monitoring/mock_uss/flight_planning/routes.py | 28 +-- monitoring/mock_uss/flights/planning.py | 38 +++- .../scd_injection/routes_injection.py | 45 ++-- .../clients/flight_planning/client_v1.py | 3 + .../clients/flight_planning/planning.py | 7 + monitoring/uss_qualifier/fileio.py | 8 +- .../flight_planning/activate_flight_intent.md | 4 + .../flight_planning/injection_evaluation.py | 197 ++++++++++++++++ .../modify_activated_flight_intent.md | 4 + .../modify_planned_flight_intent.md | 4 + .../flight_planning/plan_flight_intent.md | 4 + .../scenarios/flight_planning/test_steps.py | 27 +++ 13 files changed, 334 insertions(+), 245 deletions(-) create mode 100644 monitoring/uss_qualifier/scenarios/flight_planning/injection_evaluation.py diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index 3ec2963299..82d5bff63e 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -1269,14 +1269,6 @@ "lineCount": 1 } }, - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 30, - "lineCount": 1 - } - }, { "code": "reportReturnType", "range": { @@ -1879,30 +1871,6 @@ } ], "./monitoring/mock_uss/scd_injection/routes_injection.py": [ - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 30, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 30, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 58, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -1919,14 +1887,6 @@ "lineCount": 1 } }, - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 30, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -1943,14 +1903,6 @@ "lineCount": 1 } }, - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 30, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -5006,30 +4958,6 @@ "endColumn": 69, "lineCount": 1 } - }, - { - "code": "reportPrivateImportUsage", - "range": { - "startColumn": 50, - "endColumn": 55, - "lineCount": 1 - } - }, - { - "code": "reportPrivateImportUsage", - "range": { - "startColumn": 43, - "endColumn": 48, - "lineCount": 1 - } - }, - { - "code": "reportPrivateImportUsage", - "range": { - "startColumn": 44, - "endColumn": 49, - "lineCount": 1 - } } ], "./monitoring/uss_qualifier/main.py": [ @@ -20396,144 +20324,6 @@ } } ], - "./monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py": [ - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 16, - "endColumn": 20, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 19, - "endColumn": 23, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 84, - "endColumn": 88, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 127, - "endColumn": 131, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 22, - "endColumn": 26, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 31, - "endColumn": 35, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 35, - "endColumn": 39, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 19, - "endColumn": 23, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 60, - "endColumn": 64, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 103, - "endColumn": 107, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 42, - "endColumn": 47, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 12, - "endColumn": 16, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 34, - "endColumn": 38, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 65, - "endColumn": 69, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 108, - "endColumn": 112, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 34, - "endColumn": 39, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 11, - "endColumn": 15, - "lineCount": 1 - } - } - ], "./monitoring/uss_qualifier/scenarios/interuss/flight_authorization/general_flight_authorization.py": [ { "code": "reportOptionalMemberAccess", diff --git a/monitoring/mock_uss/flight_planning/routes.py b/monitoring/mock_uss/flight_planning/routes.py index 9ec43e9ce9..af94304862 100644 --- a/monitoring/mock_uss/flight_planning/routes.py +++ b/monitoring/mock_uss/flight_planning/routes.py @@ -1,5 +1,4 @@ import os -import uuid from datetime import timedelta import arrow @@ -12,8 +11,7 @@ from monitoring.mock_uss.app import require_config_value, webapp from monitoring.mock_uss.auth import requires_scope from monitoring.mock_uss.config import KEY_BASE_URL -from monitoring.mock_uss.f3548v21.flight_planning import op_intent_from_flightinfo -from monitoring.mock_uss.flights.database import FlightRecord, db +from monitoring.mock_uss.flights.database import db from monitoring.mock_uss.scd_injection.routes_injection import ( clear_area, delete_flight, @@ -54,7 +52,9 @@ def injection_status() -> tuple[dict, int]: @webapp.route("/flight_planning/v1/flight_plans/", methods=["PUT"]) @requires_scope(Scope.Plan) @idempotent_request() -def flight_planning_v1_upsert_flight_plan(flight_plan_id: str) -> tuple[str, int]: +def flight_planning_v1_upsert_flight_plan( + flight_plan_id: str, +) -> tuple[str | flask.Response, int]: def log(msg: str) -> None: logger.debug(f"[upsert_plan/{os.getpid()}:{flight_plan_id}] {msg}") @@ -73,19 +73,13 @@ def log(msg: str) -> None: existing_flight = lock_flight(flight_plan_id, log) try: info = FlightInfo.from_flight_plan(req_body.flight_plan) - op_intent = op_intent_from_flightinfo(info, str(uuid.uuid4())) - new_flight = FlightRecord( - flight_info=info, - op_intent=op_intent, - mod_op_sharing_behavior=( - req_body.behavior - if "behavior" in req_body and req_body.behavior - else None - ), + inject_resp = inject_flight( + flight_plan_id, + info, + req_body.behavior if "behavior" in req_body and req_body.behavior else None, + existing_flight, ) - inject_resp = inject_flight(flight_plan_id, new_flight, existing_flight) - finally: release_flight_lock(flight_plan_id, log) @@ -93,8 +87,10 @@ def log(msg: str) -> None: planning_result=api.PlanningActivityResult(inject_resp.activity_result), flight_plan_status=api.FlightPlanStatus(inject_resp.flight_plan_status), ) + if "as_planned" in inject_resp and inject_resp.as_planned: + resp.as_planned = inject_resp.as_planned.to_flight_plan() for k, v in inject_resp.items(): - if k not in {"planning_result", "flight_plan_status", "has_conflict"}: + if k not in {"planning_result", "flight_plan_status", "as_planned"}: resp[k] = v return flask.jsonify(resp), 200 diff --git a/monitoring/mock_uss/flights/planning.py b/monitoring/mock_uss/flights/planning.py index ba1ed0f5ce..9423d8f006 100644 --- a/monitoring/mock_uss/flights/planning.py +++ b/monitoring/mock_uss/flights/planning.py @@ -1,8 +1,44 @@ +import json from collections.abc import Callable -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta + +import arrow +from implicitdict import ImplicitDict from monitoring.mock_uss.flights.database import DEADLOCK_TIMEOUT, FlightRecord, db +from monitoring.monitorlib.clients.flight_planning.flight_info import FlightInfo from monitoring.monitorlib.delay import sleep +from monitoring.monitorlib.temporal import Time + + +def adjust_flight_info(info: FlightInfo) -> FlightInfo: + result: FlightInfo = ImplicitDict.parse(json.loads(json.dumps(info)), FlightInfo) + + now = arrow.utcnow() + + for v4d in result.basic_information.area: + # Fill in empty start times with now + if "time_start" not in v4d or not v4d.time_start: + v4d.time_start = Time(now) + + # Truncate volume start times to current + elif v4d.time_start.datetime < now: + v4d.time_start = Time(now + timedelta(seconds=5)) + + # Validate volume times + for i, v4d in enumerate(result.basic_information.area): + if ( + "time_start" in v4d + and v4d.time_start + and "time_end" in v4d + and v4d.time_end + and v4d.time_start >= v4d.time_end + ): + raise ValueError( + f"Volume {i} start time {v4d.time_start} (originally {info.basic_information.area[i].time_start}) is at or after end time {v4d.time_end}" + ) + + return result def lock_flight(flight_id: str, log: Callable[[str], None]) -> FlightRecord: diff --git a/monitoring/mock_uss/scd_injection/routes_injection.py b/monitoring/mock_uss/scd_injection/routes_injection.py index 69c38bfbc4..683e7b11f5 100644 --- a/monitoring/mock_uss/scd_injection/routes_injection.py +++ b/monitoring/mock_uss/scd_injection/routes_injection.py @@ -1,4 +1,5 @@ import os +import uuid from datetime import UTC, datetime, timedelta import arrow @@ -32,6 +33,7 @@ ) from monitoring.mock_uss.flights.database import FlightRecord, db from monitoring.mock_uss.flights.planning import ( + adjust_flight_info, delete_flight_record, lock_flight, release_flight_lock, @@ -55,6 +57,7 @@ PlanningActivityResult, ) from monitoring.monitorlib.clients.mock_uss.mock_uss_scd_injection_api import ( + MockUssFlightBehavior, MockUSSInjectFlightRequest, ) from monitoring.monitorlib.errors import stacktrace_string @@ -73,7 +76,7 @@ @webapp.route("/scdsc/v1/status", methods=["GET"]) @requires_scope(SCOPE_SCD_QUALIFIER_INJECT) -def scdsc_injection_status() -> tuple[str, int]: +def scdsc_injection_status() -> tuple[str | flask.Response, int]: """Implements USS status in SCD automated testing injection API.""" json, code = injection_status() return flask.jsonify(json), code @@ -88,7 +91,7 @@ def injection_status() -> tuple[dict, int]: @webapp.route("/scdsc/v1/capabilities", methods=["GET"]) @requires_scope(SCOPE_SCD_QUALIFIER_INJECT) -def scdsc_scd_capabilities() -> tuple[str, int]: +def scdsc_scd_capabilities() -> tuple[str | flask.Response, int]: """Implements USS capabilities in SCD automated testing injection API.""" json, code = scd_capabilities() return flask.jsonify(json), code @@ -110,7 +113,7 @@ def scd_capabilities() -> tuple[dict, int]: @webapp.route("/scdsc/v1/flights/", methods=["PUT"]) @requires_scope(SCOPE_SCD_QUALIFIER_INJECT) @idempotent_request() -def scdsc_inject_flight(flight_id: str) -> tuple[str, int]: +def scdsc_inject_flight(flight_id: str) -> tuple[str | flask.Response, int]: """Implements flight injection in SCD automated testing injection API.""" def log(msg): @@ -129,15 +132,14 @@ def log(msg): # Construct potential new flight flight_info = FlightInfo.from_scd_inject_flight_request(req_body) - op_intent = op_intent_from_flightinfo(flight_info, flight_id) - new_flight = FlightRecord( - flight_info=flight_info, - op_intent=op_intent, - mod_op_sharing_behavior=req_body.behavior if "behavior" in req_body else None, - ) try: - resp = inject_flight(flight_id, new_flight, existing_flight) + resp = inject_flight( + flight_id, + flight_info, + req_body.behavior if "behavior" in req_body else None, + existing_flight, + ) finally: release_flight_lock(flight_id, log) return flask.jsonify(resp.to_inject_flight_response()), 200 @@ -145,7 +147,8 @@ def log(msg): def inject_flight( flight_id: str, - new_flight: FlightRecord, + flight_info: FlightInfo, + mod_op_sharing_behavior: MockUssFlightBehavior | None, existing_flight: FlightRecord | None, ) -> PlanningActivityResponse: pid = os.getpid() @@ -159,7 +162,7 @@ def log(msg: str): ) def unsuccessful( - result: PlanningActivityResult, msg: str, has_conflict=None + result: PlanningActivityResult, msg: str ) -> PlanningActivityResponse: return PlanningActivityResponse( flight_id=flight_id, @@ -167,9 +170,20 @@ def unsuccessful( activity_result=result, flight_plan_status=old_status, notes=msg, - has_conflict=has_conflict, ) + try: + flight_info = adjust_flight_info(flight_info) + except ValueError as e: + return unsuccessful(PlanningActivityResult.Rejected, str(e)) + + op_intent = op_intent_from_flightinfo(flight_info, str(uuid.uuid4())) + new_flight = FlightRecord( + flight_info=flight_info, + op_intent=op_intent, + mod_op_sharing_behavior=mod_op_sharing_behavior, + ) + # Validate request try: if locality.is_uspace_applicable(): @@ -234,6 +248,7 @@ def unsuccessful( queries=[], # TODO: Add queries used activity_result=PlanningActivityResult.Completed, flight_plan_status=FlightPlanStatus.from_flightinfo(record.flight_info), + as_planned=flight_info, notes=notes, ) except (ValueError, ConnectionError) as e: @@ -256,7 +271,7 @@ def unsuccessful( @webapp.route("/scdsc/v1/flights/", methods=["DELETE"]) @requires_scope(SCOPE_SCD_QUALIFIER_INJECT) -def scdsc_delete_flight(flight_id: str) -> tuple[str, int]: +def scdsc_delete_flight(flight_id: str) -> tuple[str | flask.Response, int]: """Implements flight deletion in SCD automated testing injection API.""" del_resp, status_code = delete_flight(flight_id) @@ -359,7 +374,7 @@ def unsuccessful(msg: str) -> PlanningActivityResponse: @webapp.route("/scdsc/v1/clear_area_requests", methods=["POST"]) @requires_scope(SCOPE_SCD_QUALIFIER_INJECT) @idempotent_request() -def scdsc_clear_area() -> tuple[str, int]: +def scdsc_clear_area() -> tuple[str | flask.Response, int]: try: json = flask.request.json if json is None: diff --git a/monitoring/monitorlib/clients/flight_planning/client_v1.py b/monitoring/monitorlib/clients/flight_planning/client_v1.py index 455560014e..eedb47997c 100644 --- a/monitoring/monitorlib/clients/flight_planning/client_v1.py +++ b/monitoring/monitorlib/clients/flight_planning/client_v1.py @@ -100,6 +100,9 @@ def _inject( ), ) + if "as_planned" in resp and resp.as_planned: + response.as_planned = FlightInfo.from_flight_plan(resp.as_planned) + # If we know that the flight was successfully not created # (the server explicitly refused to), we remove it from set of flights. # That the only case when we do this, if we recieve no response after a diff --git a/monitoring/monitorlib/clients/flight_planning/planning.py b/monitoring/monitorlib/clients/flight_planning/planning.py index 84eeb210c6..5dd76e89bb 100644 --- a/monitoring/monitorlib/clients/flight_planning/planning.py +++ b/monitoring/monitorlib/clients/flight_planning/planning.py @@ -89,6 +89,13 @@ class PlanningActivityResponse(ImplicitDict): flight_plan_status: FlightPlanStatus """Status of the flight plan following the flight planning activity.""" + as_planned: Optional[FlightInfo] + """The flight information, as it was actually planned (after any adjustments or adaptations). + + If the flight was planned or modified successfully but this field is not populated, the flight information was + accepted exactly as provided. + """ + notes: Optional[str] """Any human-readable notes regarding the activity.""" diff --git a/monitoring/uss_qualifier/fileio.py b/monitoring/uss_qualifier/fileio.py index 1896bf81f0..4dd01b6d4b 100644 --- a/monitoring/uss_qualifier/fileio.py +++ b/monitoring/uss_qualifier/fileio.py @@ -332,7 +332,7 @@ def _replace_refs( cache: dict[str, dict] | None = None, ) -> None: for path in ref_parent_paths: - parent = [m.value for m in bc_jsonpath_ng.parse(path).find(content)] + parent = [m.value for m in bc_jsonpath_ng.parser.parse(path).find(content)] if len(parent) != 1: raise RuntimeError( f'Unexpectedly found {len(parent)} matches for $ref parent JSON Path "{path}"' @@ -344,7 +344,7 @@ def _replace_refs( ref_path, context_file_name, cache ) else: - ref_json_path = bc_jsonpath_ng.parse( + ref_json_path = bc_jsonpath_ng.parser.parse( ref_path.replace("#", "$").replace("/", ".") ) ref_content = [m.value for m in ref_json_path.find(content)] @@ -362,7 +362,9 @@ def _replace_refs( if allof_parent_path + ".allOf" in allof_paths: allof_parent_content = [ m.value - for m in bc_jsonpath_ng.parse(allof_parent_path).find(content) + for m in bc_jsonpath_ng.parser.parse(allof_parent_path).find( + content + ) ] if len(allof_parent_content) != 1: raise RuntimeError( diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/activate_flight_intent.md b/monitoring/uss_qualifier/scenarios/flight_planning/activate_flight_intent.md index 94772e5aca..35cd1a9e06 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/activate_flight_intent.md +++ b/monitoring/uss_qualifier/scenarios/flight_planning/activate_flight_intent.md @@ -11,3 +11,7 @@ All flight intent data provided is correct and valid and free of conflict in spa All flight intent data provided was complete and correct. It should have been processed successfully, allowing the USS to reject or accept the flight. If the USS indicates that the injection attempt failed, this check will fail per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../requirements/interuss/automated_testing/flight_planning.md)**. + +## 🛑 Injection fidelity check + +The requested flight should have been activated essentially as requested. The system may adapt requested parameters as necessary, but may not change the test-critical attributes of the flight when fulfilling the planning request per **interuss.automated_testing.flight_planning.ExpectedBehavior**. diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/injection_evaluation.py b/monitoring/uss_qualifier/scenarios/flight_planning/injection_evaluation.py new file mode 100644 index 0000000000..cf9e95b68d --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/flight_planning/injection_evaluation.py @@ -0,0 +1,197 @@ +from collections.abc import Callable, Iterable +from numbers import Number +from types import NoneType +from typing import Any + +import arrow +import bc_jsonpath_ng +from implicitdict import StringBasedDateTime + +from monitoring.monitorlib.clients.flight_planning.flight_info import FlightInfo +from monitoring.monitorlib.dicts import JSONAddress, JSONPath +from monitoring.uss_qualifier.scenarios.scenario import PendingCheck + +CompatibilityEvaluator = Callable[[Any, Any, JSONAddress, PendingCheck], None] +"""Evaluates whether an as-planned value is compatible with an as-requested value. + Arguments: + * as_requested: The value, as requested. + * as_planned: The value, as planned. + * address: Location of the value within the content structure being evalated. + * check: Pending check for whether the planned value is compatible with the requested value. +""" + + +def values_exactly_equal( + as_requested: str | Number | StringBasedDateTime, + as_planned: str | Number | StringBasedDateTime, + address: JSONAddress, + check: PendingCheck, +): + """Implements CompatibilityEvaluator pattern, requiring requested and planned values to match exactly.""" + + for dtype in (StringBasedDateTime, Number, str, NoneType): + if isinstance(as_requested, dtype): + if not isinstance(as_planned, dtype): + check.record_failed( + summary=f"Mismatched data type in {address}", + details=f"The data type in {address} was a {dtype.__name__} in the request, but {type(as_planned).__name__} ('{as_planned}') as planned", + ) + return + if dtype == StringBasedDateTime: + assert isinstance(as_planned, StringBasedDateTime) + assert isinstance(as_requested, StringBasedDateTime) + equal = as_planned.datetime == as_requested.datetime + else: + equal = as_planned == as_requested + if not equal: + check.record_failed( + summary=f"Incompatible flight info at {address}", + details=f"The value at {address} as requested was '{as_requested}', but as planned was '{as_planned}' when exact equality was expected", + ) + return + + raise NotImplementedError( + f"Means to compare data type {type(as_requested).__name__} (in field {address}) for equality has not yet been implemented" + ) + + +def times_not_later_than_specified_or_now( + as_requested: StringBasedDateTime, + as_planned: StringBasedDateTime, + address: JSONAddress, + check: PendingCheck, +): + """Implements CompatibilityEvaluator pattern for StringBasedDateTimes, requiring planned value to not be later than + requested or now, which ever is later. This allows a client to plan a time to be the current wall time when the + requested time was earlier than the wall time.""" + + if not isinstance(as_requested, StringBasedDateTime): + check.record_failed( + summary=f"Incorrect requested data type in {address}", + details=f"Expected a StringBasedDateTime in {address}, but found a {type(as_requested).__name__} ('{as_requested}') in the request instead", + ) + return + if not isinstance(as_planned, StringBasedDateTime): + check.record_failed( + summary=f"Incorrect data type in {address}", + details=f"Expected a StringBasedDateTime in {address}, but found a {type(as_planned).__name__} ('{as_planned}') as planned instead", + ) + return + now = arrow.utcnow().datetime + latest = now if as_requested.datetime < now else as_requested.datetime + if as_planned.datetime > latest: + check.record_failed( + f"Planned time {as_planned} is too late", + details=f"Requested time no later than {as_requested} (or now, at {now}) for {address}, but planned time was {as_planned} which is later than the latest time allowed of {latest}", + ) + + +def _resolve_addresses( + paths: Iterable[JSONPath] | JSONPath, content: dict[str, Any] +) -> Iterable[JSONAddress]: + if isinstance(paths, str): + paths = [paths] + for path in paths: + for match in bc_jsonpath_ng.parser.parse(path).find(content): + full_path = "$." + str(match.full_path) + full_path = full_path.replace(".[", "[") + yield JSONAddress(full_path) + + +def _require_compatible_values( + as_requested: dict | list | Any, + as_planned: dict | list | Any, + check: PendingCheck, + default_compatibility: CompatibilityEvaluator | None, + compatibility: dict[JSONAddress, CompatibilityEvaluator | None] | None, + address: JSONAddress = JSONAddress("$"), +): + # Use an explicit evaluator for this value if there is one + if compatibility and address in compatibility: + evaluator = compatibility[address] + if evaluator is not None: + evaluator(as_requested, as_planned, address, check) + + elif isinstance(as_requested, dict): + if not isinstance(as_planned, dict): + check.record_failed( + summary=f"Mismatched data in {address}", + details=f"The data type in {address} was an object/dictionary in the request, but '{as_planned}' ({type(as_planned)}) indicated as planned", + ) + return + for k, v in as_requested.items(): + if k in as_planned: + _require_compatible_values( + v, + as_planned[k], + check, + default_compatibility, + compatibility, + address + "." + k, + ) + else: + check.record_failed( + summary=f"As-planned missing {address}.{k}", + details=f"The request specified {address}.{k} as '{v}' but there was no such '{k}' key in {address} as planned", + ) + return + + elif isinstance(as_requested, list) and not isinstance(as_requested, str): + if not isinstance(as_planned, list): + check.record_failed( + summary=f"Mismatched data in {address}", + details=f"The data type in {address} was a list in the request, but '{as_planned}' ({type(as_planned)}) indicated as planned", + ) + return + if len(as_requested) != len(as_planned): + check.record_failed( + summary=f"Mismatched list at {address}", + details=f"As requested, {address} has {len(as_requested)} elements, but as planned, {address} has {len(as_planned)} elements (equality expected)", + ) + return + for i, v in enumerate(as_requested): + _require_compatible_values( + v, + as_planned[i], + check, + default_compatibility, + compatibility, + address + f"[{i}]", + ) + + else: + if default_compatibility: + default_compatibility(as_requested, as_planned, address, check) + else: + raise NotImplementedError( + f"Means to compare data type {type(as_requested)} in as_planned field {address} has not yet been implemented" + ) + + +def require_compatible_values( + as_requested: FlightInfo, + as_planned: FlightInfo, + check: PendingCheck, + compatibility: dict[JSONPath, CompatibilityEvaluator | None] | None = None, + default_compatibility: CompatibilityEvaluator | None = None, +) -> None: + """Requires values in as_planned FlightInfo to be compatible with those in as_requested. + + Arguments: + * as_requested: The flight information that was requested. + * as_planned: The flight information that was actually planned. + * check: Pending check that should fail if any planned values are incompatible with the request. + * default_compatibility: If specified, determine if a particular value pair is compatible using this method when + no other method is specified via `compatibility` + * compatibility: If specified, mapping between JSONPaths (e.g., $.basic_information.area[0].time_start) + describing element(s) of as_requested and the method to determine compatibility between each matching value + pair. + """ + compatibility_by_address = dict() + if compatibility: + for json_path, evaluator in compatibility.items(): + for address in _resolve_addresses(json_path, as_requested): + compatibility_by_address[address] = evaluator + _require_compatible_values( + as_requested, as_planned, check, default_compatibility, compatibility_by_address + ) diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/modify_activated_flight_intent.md b/monitoring/uss_qualifier/scenarios/flight_planning/modify_activated_flight_intent.md index 04bbdf2561..45b88c9ea5 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/modify_activated_flight_intent.md +++ b/monitoring/uss_qualifier/scenarios/flight_planning/modify_activated_flight_intent.md @@ -40,3 +40,7 @@ a low severity finding per **[astm.f3548.v21.SCD0030](../../requirements/astm/f3 All flight intent data provided was complete and correct. It should have been processed successfully, allowing the USS to reject or accept the flight. If the USS indicates that the injection attempt failed, this check will fail per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../requirements/interuss/automated_testing/flight_planning.md)**. + +## 🛑 Injection fidelity check + +The requested flight should have been modified essentially as requested. The system may adapt requested parameters as necessary, but may not change the test-critical attributes of the flight when fulfilling the planning request per **interuss.automated_testing.flight_planning.ExpectedBehavior**. diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/modify_planned_flight_intent.md b/monitoring/uss_qualifier/scenarios/flight_planning/modify_planned_flight_intent.md index a0033a6f01..f7c0ba947a 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/modify_planned_flight_intent.md +++ b/monitoring/uss_qualifier/scenarios/flight_planning/modify_planned_flight_intent.md @@ -15,3 +15,7 @@ flight, this check will fail. All flight intent data provided was complete and correct. It should have been processed successfully, allowing the USS to reject or accept the flight. If the USS indicates that the injection attempt failed, this check will fail per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../requirements/interuss/automated_testing/flight_planning.md)**. + +## 🛑 Injection fidelity check + +The requested flight should have been modified essentially as requested. The system may adapt requested parameters as necessary, but may not change the test-critical attributes of the flight when fulfilling the planning request per **interuss.automated_testing.flight_planning.ExpectedBehavior**. diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/plan_flight_intent.md b/monitoring/uss_qualifier/scenarios/flight_planning/plan_flight_intent.md index a57719e6cd..cdfd18350e 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/plan_flight_intent.md +++ b/monitoring/uss_qualifier/scenarios/flight_planning/plan_flight_intent.md @@ -11,3 +11,7 @@ All flight intent data provided is correct and valid and free of conflict in spa All flight intent data provided was complete and correct. It should have been processed successfully, allowing the USS to reject or accept the flight. If the USS indicates that the injection attempt failed, this check will fail per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../requirements/interuss/automated_testing/flight_planning.md)**. + +## 🛑 Injection fidelity check + +The requested flight should have been planned essentially as requested. The system may adapt requested parameters as necessary, but may not change the test-critical attributes of the flight when fulfilling the planning request per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../requirements/interuss/automated_testing/flight_planning.md)**. diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py index e212d591a2..066db6a682 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py @@ -19,8 +19,14 @@ PlanningActivityResponse, PlanningActivityResult, ) +from monitoring.monitorlib.dicts import JSONPath from monitoring.monitorlib.fetch import Query, QueryError from monitoring.monitorlib.geotemporal import end_time_of +from monitoring.uss_qualifier.scenarios.flight_planning.injection_evaluation import ( + require_compatible_values, + times_not_later_than_specified_or_now, + values_exactly_equal, +) from monitoring.uss_qualifier.scenarios.scenario import ( ScenarioDidNotStopError, TestScenario, @@ -278,6 +284,7 @@ def submit_flight( details=f"{str(e)}\n\nStack trace:\n{e.stacktrace}", query_timestamps=[q.request.timestamp for q in e.queries], ) + raise ScenarioDidNotStopError(check) if ( skip_if_not_supported @@ -310,6 +317,26 @@ def submit_flight( query_timestamps=[query.request.timestamp], ) + if resp.flight_plan_status in { + FlightPlanStatus.Planned, + FlightPlanStatus.OkToFly, + }: + if "as_planned" in resp and resp.as_planned: + with scenario.check( + "Injection fidelity", flight_planner.participant_id + ) as fidelity_check: + require_compatible_values( + flight_info, + resp.as_planned, + fidelity_check, + default_compatibility=values_exactly_equal, + compatibility={ + JSONPath( + "$.basic_information.area[*].time_start" + ): times_not_later_than_specified_or_now + }, + ) + return resp, flight_id From adfb9e045e64f3996631e02fc0d82ab5dfce5ede Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Mon, 9 Feb 2026 21:15:42 +0000 Subject: [PATCH 2/7] Use flights as planned --- monitoring/mock_uss/flights/planning.py | 4 +- .../get_op_data_validation.py | 14 ++++-- .../flight_intent_validation.py | 8 ++- .../conflict_equal_priority_not_permitted.py | 36 ++++++++++--- .../conflict_higher_priority.py | 35 ++++++++++--- .../astm/utm/off_nominal_planning/down_uss.py | 4 +- .../receive_notifications_for_awareness.py | 15 ++++-- .../scenarios/astm/utm/test_steps.py | 20 ++++++-- .../scenarios/flight_planning/test_steps.py | 50 +++++++++++++------ .../uspace/flight_auth/validation.py | 5 +- 10 files changed, 141 insertions(+), 50 deletions(-) diff --git a/monitoring/mock_uss/flights/planning.py b/monitoring/mock_uss/flights/planning.py index 9423d8f006..9cf7eefaa2 100644 --- a/monitoring/mock_uss/flights/planning.py +++ b/monitoring/mock_uss/flights/planning.py @@ -1,6 +1,6 @@ import json from collections.abc import Callable -from datetime import UTC, datetime, timedelta +from datetime import UTC, datetime import arrow from implicitdict import ImplicitDict @@ -23,7 +23,7 @@ def adjust_flight_info(info: FlightInfo) -> FlightInfo: # Truncate volume start times to current elif v4d.time_start.datetime < now: - v4d.time_start = Time(now + timedelta(seconds=5)) + v4d.time_start = Time(now) # Validate volume times for i, v4d in enumerate(result.basic_information.area): diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/data_exchange_validation/get_op_data_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/data_exchange_validation/get_op_data_validation.py index 393f23ecc5..e33a66128b 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/data_exchange_validation/get_op_data_validation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/data_exchange_validation/get_op_data_validation.py @@ -156,11 +156,13 @@ def _plan_successfully_test_case(self): flight_2.basic_information.area.bounding_volume.to_f3548v21(), ) as validator: flight_2_planning_time = Time(arrow.utcnow().datetime) - _, self.flight_2_id = plan_flight( + _, self.flight_2_id, as_planned = plan_flight( self, self.mock_uss_client, flight_2, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight_2 = as_planned flight_2_oi_ref = validator.expect_shared(flight_2) self.op_intent_ids.add(flight_2_oi_ref.id) @@ -176,11 +178,13 @@ def _plan_successfully_test_case(self): flight_1.basic_information.area.bounding_volume.to_f3548v21(), ) as validator: flight_1_planning_time = Time(arrow.utcnow().datetime) - plan_res, self.flight_1_id = plan_flight( + plan_res, self.flight_1_id, as_planned = plan_flight( self, self.tested_uss_client, flight_1, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight_1 = as_planned flight_1_oi_ref = validator.expect_shared(flight_1) self.op_intent_ids.add(flight_1_oi_ref.id) self.end_test_step() @@ -243,12 +247,14 @@ def _plan_unsuccessfully_test_case(self): flight_info.basic_information.area.bounding_volume.to_f3548v21(), ) as validator: flight_2_planning_time = Time(arrow.utcnow().datetime) - _, self.flight_2_id = plan_flight( + _, self.flight_2_id, as_planned = plan_flight( self, self.mock_uss_client, flight_info, additional_fields, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight_info = as_planned flight_2_oi_ref = validator.expect_shared_with_invalid_data( flight_info, validation_failure_type=OpIntentValidationFailureType.DataFormat, @@ -266,7 +272,7 @@ def _plan_unsuccessfully_test_case(self): flight_1.basic_information.area.bounding_volume.to_f3548v21(), ) as validator: flight_1_planning_time = Time(arrow.utcnow().datetime) - _, self.flight_1_id = submit_flight( + _, self.flight_1_id, _ = submit_flight( self, "Plan should fail", { diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py index 00f440a614..f17615dc28 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py @@ -208,11 +208,14 @@ def _validate_ended_cancellation(self): self.dss, valid_flight, ) as planned_validator: - _, flight_id = plan_flight( + _, flight_id, as_planned = plan_flight( self, self.tested_uss, valid_flight, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + assert as_planned is not None + valid_flight = as_planned oi_ref = planned_validator.expect_shared(valid_flight) self.end_test_step() @@ -231,11 +234,12 @@ def _validate_precision_intersection(self): self.begin_test_step(self.PLAN_VALID_FLIGHT_STEP) valid_flight = self.resolve_flight(self.valid_flight) - plan_flight( + _, _, as_planned = plan_flight( self, self.tested_uss, valid_flight, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed self.end_test_step() self.begin_test_step("Attempt to plan Tiny Overlap Conflict Flight") diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py index cb8be153b6..e6f1dc90ad 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py @@ -234,11 +234,13 @@ def _attempt_plan_flight_conflict(self): self.dss, flight2_planned, ) as validator: - _, self.flight2_id = plan_flight( + _, self.flight2_id, as_planned = plan_flight( self, self.control_uss, flight2_planned, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight2_planned = as_planned flight_2_oi_ref = validator.expect_shared(flight2_planned) self.end_test_step() @@ -252,12 +254,14 @@ def _attempt_plan_flight_conflict(self): flight2_activated, flight_2_oi_ref, ) as validator: - activate_flight( + _, _, as_planned = activate_flight( self, self.control_uss, flight2_activated, self.flight2_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight2_activated = as_planned validator.expect_shared(flight2_activated) self.end_test_step() @@ -309,12 +313,15 @@ def _attempt_modify_planned_flight_conflict( self.dss, flight1c_planned, ) as validator: - _, self.flight1_id = plan_flight( + _, self.flight1_id, as_planned = plan_flight( self, self.tested_uss, flight1c_planned, nearby_potential_conflict=True, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + assert as_planned is not None + flight1c_planned = as_planned flight_1_oi_ref = validator.expect_shared(flight1c_planned) self.end_test_step() @@ -354,12 +361,15 @@ def _attempt_modify_activated_flight_conflict( flight1c_activated, flight_1_oi_ref, ) as validator: - activate_flight( + _, _, as_planned = activate_flight( self, self.tested_uss, flight1c_activated, self.flight1_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + assert as_planned is not None + flight1c_activated = as_planned flight_1_oi_ref = validator.expect_shared(flight1c_activated) self.end_test_step() @@ -413,12 +423,15 @@ def _modify_activated_flight_preexisting_conflict( flight1_activated, flight_1_oi_ref, ) as validator: - _, self.flight1_id = activate_flight( + _, self.flight1_id, as_planned = activate_flight( self, self.tested_uss, flight1_activated, self.flight1_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + assert as_planned is not None + flight1_activated = as_planned flight_1_oi_ref = validator.expect_shared(flight1_activated) self.end_test_step() @@ -431,11 +444,14 @@ def _modify_activated_flight_preexisting_conflict( self.dss, flight2m_planned, ) as validator: - _, self.flight2_id = plan_flight( + _, self.flight2_id, as_planned = plan_flight( self, self.control_uss, flight2m_planned, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + assert as_planned is not None + flight2m_planned = as_planned flight_2_oi_ref = validator.expect_shared(flight2m_planned) self.end_test_step() @@ -449,7 +465,7 @@ def _modify_activated_flight_preexisting_conflict( [flight2m_planned, flight2_nonconforming], flight_2_oi_ref, ) as validator: - resp_flight_2, _ = submit_flight( + resp_flight_2, _, as_planned = submit_flight( scenario=self, success_check="Successful transition to non-conforming state", expected_results={ @@ -461,6 +477,8 @@ def _modify_activated_flight_preexisting_conflict( flight_info=flight2_nonconforming, flight_id=self.flight2_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight2_nonconforming = as_planned if resp_flight_2.activity_result == PlanningActivityResult.NotSupported: msg = f"{self.control_uss.participant_id} does not support the transition to a Nonconforming state; execution of the scenario was stopped without failure" @@ -482,7 +500,7 @@ def _modify_activated_flight_preexisting_conflict( [flight1_activated, flight1m_activated], flight_1_oi_ref, ) as validator: - resp_flight_1, _ = submit_flight( + resp_flight_1, _, as_planned = submit_flight( scenario=self, success_check="Successful flight intent handling", expected_results={ @@ -495,6 +513,8 @@ def _modify_activated_flight_preexisting_conflict( flight_info=flight1m_activated, flight_id=self.flight1_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight1m_activated = as_planned if resp_flight_1.activity_result == PlanningActivityResult.Completed: validator.expect_shared(flight1m_activated) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py index 2f4a74e659..028479710e 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py @@ -216,11 +216,13 @@ def _attempt_plan_flight_conflict(self): self.dss, flight2_planned, ) as validator: - _, self.flight2_id = plan_flight( + _, self.flight2_id, as_planned = plan_flight( self, self.control_uss, flight2_planned, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight2_planned = as_planned validator.expect_shared(flight2_planned) self.end_test_step() @@ -258,11 +260,14 @@ def _attempt_modify_planned_flight_conflict( self.dss, flight1_planned, ) as validator: - _, self.flight1_id = plan_flight( + _, self.flight1_id, as_planned = plan_flight( self, self.tested_uss, flight1_planned, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + assert as_planned is not None + flight1_planned = as_planned flight_1_oi_ref = validator.expect_shared(flight1_planned) self.end_test_step() @@ -279,11 +284,14 @@ def _attempt_modify_planned_flight_conflict( flight2_planned, ) as validator: earliest_creation_time = arrow.utcnow().datetime - _, self.flight2_id = plan_flight( + _, self.flight2_id, as_planned = plan_flight( self, self.control_uss, flight2_planned, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + assert as_planned is not None + flight2_planned = as_planned latest_creation_time = arrow.utcnow().datetime validator.expect_shared(flight2_planned) self.end_test_step() @@ -367,12 +375,15 @@ def _modify_activated_flight_conflict_preexisting( flight1_activated, flight_1_oi_ref, ) as validator: - activate_flight( + _, _, as_planned = activate_flight( self, self.tested_uss, flight1_activated, self.flight1_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + assert as_planned is not None + flight1_activated = as_planned flight_1_oi_ref = validator.expect_shared(flight1_activated) self.end_test_step() @@ -385,11 +396,13 @@ def _modify_activated_flight_conflict_preexisting( self.dss, flight2_planned, ) as validator: - _, self.flight2_id = plan_flight( + _, self.flight2_id, as_planned = plan_flight( self, self.control_uss, flight2_planned, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight2_planned = as_planned flight_2_oi_ref = validator.expect_shared(flight2_planned) self.end_test_step() @@ -407,12 +420,14 @@ def _modify_activated_flight_conflict_preexisting( flight_2_oi_ref, ) as validator: earliest_activation_time = arrow.utcnow().datetime - activate_flight( + _, _, as_planned = activate_flight( self, self.control_uss, flight2_activated, self.flight2_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight2_activated = as_planned latest_activation_time = arrow.utcnow().datetime flight_2_oi_ref = validator.expect_shared(flight2_activated) self.end_test_step() @@ -439,13 +454,15 @@ def _modify_activated_flight_conflict_preexisting( [flight1_activated, flight1m_activated], flight_1_oi_ref, ) as validator: - resp = modify_activated_flight( + resp, as_planned = modify_activated_flight( self, self.tested_uss, flight1m_activated, self.flight1_id, preexisting_conflict=True, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight1m_activated = as_planned if resp.activity_result == PlanningActivityResult.Completed: flight_1_oi_ref = validator.expect_shared(flight1m_activated) @@ -474,12 +491,14 @@ def _attempt_modify_activated_flight_conflict( flight2m_activated, flight_2_oi_ref, ) as validator: - resp = modify_activated_flight( + resp, as_planned = modify_activated_flight( self, self.control_uss, flight2m_activated, self.flight2_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight2m_activated = as_planned if resp.activity_result == PlanningActivityResult.Completed: validator.expect_shared(flight2m_activated) self.end_test_step() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py index be1f804e3b..82c0d7de61 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py @@ -235,7 +235,7 @@ def _plan_flight_conflict_planned(self): self.dss, flight1_planned, ) as validator: - resp, flight_id = submit_flight( + resp, flight_id, as_planned = submit_flight( scenario=self, success_check="Successful planning", expected_results={ @@ -247,6 +247,8 @@ def _plan_flight_conflict_planned(self): flight_planner=self.tested_uss, flight_info=flight1_planned, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight1_planned = as_planned if resp.activity_result == PlanningActivityResult.Completed: validator.expect_shared(flight1_planned) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py b/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py index 3b056b66ba..3793589710 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py @@ -156,11 +156,13 @@ def _receive_notification_successfully_when_activated_test_case(self): self.dss, resolved_extents, ) as validator: - _, self.flight_1_id = plan_flight( + _, self.flight_1_id, as_planned = plan_flight( self, self.tested_uss_client, flight_1_planned, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight_1_planned = as_planned self.flight_1_oi_ref = validator.expect_shared(flight_1_planned) with OpIntentValidator( @@ -170,12 +172,14 @@ def _receive_notification_successfully_when_activated_test_case(self): resolved_extents, self.flight_1_oi_ref, ) as validator: - _, self.flight_1_id = activate_flight( + _, self.flight_1_id, as_planned = activate_flight( self, self.tested_uss_client, flight_1_activated, self.flight_1_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight_1_activated = as_planned self.flight_1_oi_ref = validator.expect_shared(flight_1_activated) self.end_test_step() @@ -188,11 +192,13 @@ def _receive_notification_successfully_when_activated_test_case(self): resolved_extents, ) as validator: flight_2_planning_time = arrow.utcnow().datetime - _, self.flight_2_id = plan_flight( + _, self.flight_2_id, as_planned = plan_flight( self, self.mock_uss_client, flight_2_planned, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight_2_planned = as_planned self.flight_2_oi_ref = validator.expect_shared(flight_2_planned) self.end_test_step() @@ -224,12 +230,13 @@ def _receive_notification_successfully_when_activated_modified_test_case(self): self.flight_2_oi_ref, ) as validator: flight_2_modif_time = arrow.utcnow().datetime - modify_planned_flight( + _, as_planned = modify_planned_flight( self, self.mock_uss_client, flight_2_planned_modified, self.flight_2_id, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed self.flight_2_oi_ref = validator.expect_shared(flight_2_planned_modified) self.end_test_step() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py index dcdd5bf87f..001b93a0e4 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py @@ -31,6 +31,8 @@ validate_op_intent_details, ) from monitoring.uss_qualifier.scenarios.scenario import ( + ScenarioDidNotStopError, + ScenarioLogicError, TestRunCannotContinueError, TestScenarioType, ) @@ -201,7 +203,7 @@ def expect_not_shared(self) -> None: def expect_shared( self, - flight_info: FlightInfo, + flight_info: FlightInfo | None, skip_if_not_found: bool = False, ) -> OperationalIntentReference | None: """Validate that operational intent information was correctly shared for a flight intent. @@ -215,6 +217,10 @@ def expect_shared( """ self._begin_step_fragment() oi_ref = self._operational_intent_shared_check(flight_info, skip_if_not_found) + if flight_info is None: + raise ScenarioLogicError( + "Scenario should have stopped with missing flight_info during self._operational_intent_shared_check" + ) if oi_ref is None: return None @@ -234,7 +240,7 @@ def expect_shared( def expect_shared_with_invalid_data( self, - flight_info: FlightInfo, + flight_info: FlightInfo | None, validation_failure_type: OpIntentValidationFailureType, invalid_fields: list | None = None, skip_if_not_found: bool = False, @@ -244,6 +250,7 @@ def expect_shared_with_invalid_data( This function implements the test step described in validate_sharing_operational_intent_but_with_invalid_interuss_data. + :param flight_info: the flight intent that was supposed to have been shared. :param skip_if_not_found: set to True to skip the execution of the checks if the operational intent was not found while it should have been modified. :param validation_failure_type: specific type of validation failure expected :param invalid_fields: Optional list of invalid fields to expect when validation_failure_type is OI_DATA_FORMAT @@ -296,12 +303,19 @@ def expect_shared_with_invalid_data( def _operational_intent_shared_check( self, - flight_intent: FlightInfo, + flight_intent: FlightInfo | None, skip_if_not_found: bool, ) -> OperationalIntentReference | None: with self._scenario.check( "Operational intent shared correctly", [self._flight_planner.participant_id] ) as check: + if flight_intent is None: + check.record_failed( + summary="Flight not eligible to be shared as planned", + details=f"USS {self._flight_planner.participant_id} was supposed to have planned a flight that would be shared with the DSS, but instead indicated that the flight planning activity did not result in a flight plan that would be shared with the DSS", + query_timestamps=[], # TODO: Identify the flight planning query that resulted in no flight info as planned + ) + raise ScenarioDidNotStopError(check) if self._orig_oi_ref is None: # We expect a new op intent to have been created. Exception made if skip_if_not_found=True: step is # skipped. diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py index 066db6a682..6199f89044 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py @@ -58,7 +58,7 @@ def plan_flight( flight_info: FlightInfo, additional_fields: dict | None = None, nearby_potential_conflict: bool = False, -) -> tuple[PlanningActivityResponse, str | None]: +) -> tuple[PlanningActivityResponse, str | None, FlightInfo | None]: """Plan a flight intent that should result in success. This function implements the test step fragment described in @@ -70,9 +70,10 @@ def plan_flight( Returns: * The injection response. * The ID of the injected flight if it is returned, None otherwise. + * The flight info, as actually planned by the client. """ - resp, flight_id = submit_flight( + resp, flight_id, as_planned = submit_flight( scenario=scenario, success_check="Successful planning", expected_results={(PlanningActivityResult.Completed, FlightPlanStatus.Planned)}, @@ -90,7 +91,7 @@ def plan_flight( "Validate tested USS intersection algorithm", flight_planner.participant_id ).record_passed() - return resp, flight_id + return resp, flight_id, as_planned def modify_planned_flight( @@ -99,13 +100,15 @@ def modify_planned_flight( flight_info: FlightInfo, flight_id: str, additional_fields: dict | None = None, -) -> PlanningActivityResponse: +) -> tuple[PlanningActivityResponse, FlightInfo | None]: """Modify a planned flight intent that should result in success. This function implements the test step described in modify_planned_flight_intent.md. - Returns: The injection response. + Returns: + * The injection response. + * The flight info, as actually planned by the client. """ expect_flight_intent_state( flight_info, @@ -114,7 +117,7 @@ def modify_planned_flight( scenario, ) - return submit_flight( + resp, _, as_planned = submit_flight( scenario=scenario, success_check="Successful modification", expected_results={ @@ -129,7 +132,8 @@ def modify_planned_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, - )[0] + ) + return resp, as_planned def modify_activated_flight( @@ -139,13 +143,15 @@ def modify_activated_flight( flight_id: str, preexisting_conflict: bool = False, additional_fields: dict | None = None, -) -> PlanningActivityResponse: +) -> tuple[PlanningActivityResponse, FlightInfo | None]: """Modify an activated flight intent that should result in success. This function implements the test step described in modify_activated_flight_intent.md. - Returns: The injection response. + Returns: + * The injection response. + * The flight info, as actually planned by the client. """ expect_flight_intent_state( flight_info, @@ -155,7 +161,7 @@ def modify_activated_flight( ) if preexisting_conflict: - resp, _ = submit_flight( + resp, _, as_planned = submit_flight( scenario=scenario, success_check="Successful modification", expected_results={ @@ -170,6 +176,7 @@ def modify_activated_flight( flight_id=flight_id, additional_fields=additional_fields, ) + assert as_planned is not None with scenario.check( "Rejected modification", [flight_planner.participant_id] @@ -186,7 +193,7 @@ def modify_activated_flight( ) else: - resp, _ = submit_flight( + resp, _, as_planned = submit_flight( scenario=scenario, success_check="Successful modification", expected_results={ @@ -202,8 +209,9 @@ def modify_activated_flight( flight_id=flight_id, additional_fields=additional_fields, ) + assert as_planned is not None - return resp + return resp, as_planned def activate_flight( @@ -212,7 +220,7 @@ def activate_flight( flight_info: FlightInfo, flight_id: str | None = None, additional_fields: dict | None = None, -) -> tuple[PlanningActivityResponse, str | None]: +) -> tuple[PlanningActivityResponse, str | None, FlightInfo | None]: """Activate a flight intent that should result in success. This function implements the test step fragment described in @@ -221,6 +229,7 @@ def activate_flight( Returns: * The injection response. * The ID of the injected flight if it is returned, None otherwise. + * The flight info, as actually planned by the client. """ return submit_flight( scenario=scenario, @@ -245,7 +254,7 @@ def submit_flight( additional_fields: dict | None = None, skip_if_not_supported: bool = False, may_end_in_past: bool = False, -) -> tuple[PlanningActivityResponse, str | None]: +) -> tuple[PlanningActivityResponse, str | None, FlightInfo | None]: """Submit a flight intent with an expected result. A check fail is considered by default of high severity and as such will raise an ScenarioCannotContinueError. The severity of each failed check may be overridden if needed. @@ -258,6 +267,7 @@ def submit_flight( Returns: * The injection response. * The ID of the injected flight if it is returned, None otherwise. + * The flight info, as actually planned by the client. """ if expected_results.intersection(failed_checks.keys()): raise ValueError( @@ -291,7 +301,7 @@ def submit_flight( and resp.activity_result == PlanningActivityResult.NotSupported ): check.skip() - return resp, None + return resp, None, None msg = f"{flight_planner.participant_id} indicated flight planning activity {resp.activity_result} leaving flight plan {resp.flight_plan_status} rather than the expected {' or '.join([f'(Activity {expected_result[0]}, flight plan {expected_result[1]})' for expected_result in expected_results])}" if "notes" in resp and resp.notes: @@ -320,11 +330,14 @@ def submit_flight( if resp.flight_plan_status in { FlightPlanStatus.Planned, FlightPlanStatus.OkToFly, + FlightPlanStatus.OffNominal, }: if "as_planned" in resp and resp.as_planned: with scenario.check( "Injection fidelity", flight_planner.participant_id ) as fidelity_check: + # TODO(#1326): Relax/remove this global fidelity check when each individual scenario validates that + # the flight, as injected, will satisfy the needs of the scenario. require_compatible_values( flight_info, resp.as_planned, @@ -336,8 +349,13 @@ def submit_flight( ): times_not_later_than_specified_or_now }, ) + as_planned = resp.as_planned + else: + as_planned = flight_info + else: + as_planned = None - return resp, flight_id + return resp, flight_id, as_planned def request_flight( diff --git a/monitoring/uss_qualifier/scenarios/uspace/flight_auth/validation.py b/monitoring/uss_qualifier/scenarios/uspace/flight_auth/validation.py index fad58927b9..2751e997ca 100644 --- a/monitoring/uss_qualifier/scenarios/uspace/flight_auth/validation.py +++ b/monitoring/uss_qualifier/scenarios/uspace/flight_auth/validation.py @@ -109,7 +109,7 @@ def _attempt_invalid_flights(self) -> bool: for flight_intent_template in self.invalid_flight_intents: flight_intent = self.resolve_flight(flight_intent_template) - resp, _ = submit_flight( + submit_flight( scenario=self, success_check="Incorrectly planned", expected_results={ @@ -127,11 +127,12 @@ def _attempt_invalid_flights(self) -> bool: def _plan_valid_flight(self) -> bool: valid_flight_intent = self.resolve_flight(self.valid_flight_intent) - resp, _ = plan_flight( + resp, _, as_planned = plan_flight( self, self.ussp, valid_flight_intent, ) + # TODO(#1326): Validate that flight as planned still allows this scenario to proceed if resp is None: return False From d9e25d2728ffafc79e5aafff1aefa7c326315d70 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 11 Feb 2026 04:40:12 +0000 Subject: [PATCH 3/7] Add missing updates --- .../scenarios/astm/utm/off_nominal_planning/down_uss.md | 4 ++++ .../receive_notifications_for_awareness.py | 1 + 2 files changed, 5 insertions(+) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.md b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.md index 1a9eabd468..6f46537f7d 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.md @@ -90,6 +90,10 @@ All flight intent data provided is correct and the USS should have either succes properly the planning if it decided to be more conservative with such conflicts. If the USS rejects the planning, this check will produce a low severity finding per **[astm.f3548.v21.SCD0005](../../../../requirements/astm/f3548/v21.md)**. +#### 🛑 Injection fidelity check + +The requested flight should have been planned essentially as requested. The system may adapt requested parameters as necessary, but may not change the test-critical attributes of the flight when fulfilling the planning request per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../../../requirements/interuss/automated_testing/flight_planning.md)**. + #### 🛑 Failure check All flight intent data provided was complete and correct. It should have been processed successfully, allowing the USS to reject or accept Flight 1. If the USS indicates that the injection attempt failed, this check will fail per diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py b/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py index 3793589710..9ce0ac0ca6 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py @@ -237,6 +237,7 @@ def _receive_notification_successfully_when_activated_modified_test_case(self): self.flight_2_id, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed + flight_2_planned_modified = as_planned self.flight_2_oi_ref = validator.expect_shared(flight_2_planned_modified) self.end_test_step() From 62cceb6a43424960e99639a0633d05a44eb5b8d1 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 11 Feb 2026 05:20:18 +0000 Subject: [PATCH 4/7] Retrofit scd injection API for as_planned support --- .../mock_uss/scd_injection/routes_injection.py | 7 ++++++- .../monitorlib/clients/flight_planning/client_scd.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/monitoring/mock_uss/scd_injection/routes_injection.py b/monitoring/mock_uss/scd_injection/routes_injection.py index 683e7b11f5..764573fa67 100644 --- a/monitoring/mock_uss/scd_injection/routes_injection.py +++ b/monitoring/mock_uss/scd_injection/routes_injection.py @@ -142,7 +142,12 @@ def log(msg): ) finally: release_flight_lock(flight_id, log) - return flask.jsonify(resp.to_inject_flight_response()), 200 + api_response = resp.to_inject_flight_response() + if "as_planned" in resp and resp.as_planned: + # Append as_planned as an additional field formatted according to flight_planning API to ease continuing support + # of legacy scd injection API + api_response["as_planned"] = resp.as_planned.to_flight_plan() + return flask.jsonify(api_response), 200 def inject_flight( diff --git a/monitoring/monitorlib/clients/flight_planning/client_scd.py b/monitoring/monitorlib/clients/flight_planning/client_scd.py index 73042451e0..1491dee955 100644 --- a/monitoring/monitorlib/clients/flight_planning/client_scd.py +++ b/monitoring/monitorlib/clients/flight_planning/client_scd.py @@ -3,6 +3,7 @@ import arrow from implicitdict import ImplicitDict, StringBasedDateTime +from uas_standards.interuss.automated_testing.flight_planning.v1.api import FlightPlan from uas_standards.interuss.automated_testing.scd.v1 import api as scd_api from uas_standards.interuss.automated_testing.scd.v1 import ( constants as scd_api_constants, @@ -157,6 +158,17 @@ def _inject( if response.flight_plan_status in created_status: self.created_flight_ids.add(flight_id) + if query.response.json and "as_planned" in query.response.json: + # Make best effort to interpret additional `as_planned` field according to flight_planning API as an ad-hoc + # retrofit to the legacy scd injection API + try: + response.as_planned = FlightInfo.from_flight_plan( + ImplicitDict.parse(query.response.json["as_planned"], FlightPlan) + ) + except ValueError: + # Best effort failed so it's ok to ignore additional `as_planned` field + pass + self._plan_statuses[flight_id] = response.flight_plan_status return response From 019723bf6fc41756e2b1bca21d121fb1bc4060a8 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 11 Feb 2026 05:35:02 +0000 Subject: [PATCH 5/7] Add missing check --- .../conflict_equal_priority_not_permitted.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md index ef94349eb4..c0a070e17c 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md @@ -83,6 +83,7 @@ Otherwise, the FlightIntentsResource must provide the following flight intents: + Because the scenario involves activation of intents, the start times of all activated intents must be during the time the test scenario is executed (not before). Additionally, their end times must leave sufficient time for the execution of the test scenario. @@ -97,7 +98,6 @@ CMSA role in order to transition to the `Nonconforming` state in order to create ### dss DSSInstanceResource that provides access to a DSS instance where flight creation/sharing can be verified. - ## Prerequisites check test case ### [Verify area is clear test step](../../clear_area_validation.md) @@ -234,6 +234,10 @@ per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../../../ If the USS rejects the transition, this check will fail. If the USS indicates that the operation is not supported, the USS does not support the CMSA role, and as such the scenario execution will stop without failing. +#### 🛑 Injection fidelity check + +The requested flight should have been updated essentially as requested. The system may adapt requested parameters as necessary, but may not change the test-critical attributes of the flight when fulfilling the planning request per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../../../../requirements/interuss/automated_testing/flight_planning.md)**. + #### 🛑 Failure check All flight intent data provided was complete and correct. It should have been processed successfully, allowing the USS to reject or accept the flight. If the USS indicates that the injection attempt failed, this check will fail per From 777e3522269efbad67ab9eb222bfc70e8d5b8369 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 18 Feb 2026 17:57:55 +0000 Subject: [PATCH 6/7] Address comments --- .../clients/flight_planning/client_scd.py | 2 + .../flight_planning/injection_evaluation.py | 6 +-- .../scenarios/flight_planning/test_steps.py | 44 +++++++++---------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/monitoring/monitorlib/clients/flight_planning/client_scd.py b/monitoring/monitorlib/clients/flight_planning/client_scd.py index 1491dee955..5ab6c6652a 100644 --- a/monitoring/monitorlib/clients/flight_planning/client_scd.py +++ b/monitoring/monitorlib/clients/flight_planning/client_scd.py @@ -3,6 +3,7 @@ import arrow from implicitdict import ImplicitDict, StringBasedDateTime +from loguru import logger from uas_standards.interuss.automated_testing.flight_planning.v1.api import FlightPlan from uas_standards.interuss.automated_testing.scd.v1 import api as scd_api from uas_standards.interuss.automated_testing.scd.v1 import ( @@ -167,6 +168,7 @@ def _inject( ) except ValueError: # Best effort failed so it's ok to ignore additional `as_planned` field + logger.warning("SCD API response contained unparseable `as_planned` supplemental field") pass self._plan_statuses[flight_id] = response.flight_plan_status diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/injection_evaluation.py b/monitoring/uss_qualifier/scenarios/flight_planning/injection_evaluation.py index cf9e95b68d..28f125e376 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/injection_evaluation.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/injection_evaluation.py @@ -22,8 +22,8 @@ def values_exactly_equal( - as_requested: str | Number | StringBasedDateTime, - as_planned: str | Number | StringBasedDateTime, + as_requested: str | Number | StringBasedDateTime | None, + as_planned: str | Number | StringBasedDateTime | None, address: JSONAddress, check: PendingCheck, ): @@ -89,7 +89,7 @@ def times_not_later_than_specified_or_now( def _resolve_addresses( paths: Iterable[JSONPath] | JSONPath, content: dict[str, Any] ) -> Iterable[JSONAddress]: - if isinstance(paths, str): + if isinstance(paths, JSONPath): paths = [paths] for path in paths: for match in bc_jsonpath_ng.parser.parse(path).find(content): diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py index 6199f89044..2cea81d9e0 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py @@ -327,33 +327,33 @@ def submit_flight( query_timestamps=[query.request.timestamp], ) - if resp.flight_plan_status in { + if resp.flight_plan_status not in { FlightPlanStatus.Planned, FlightPlanStatus.OkToFly, FlightPlanStatus.OffNominal, }: - if "as_planned" in resp and resp.as_planned: - with scenario.check( - "Injection fidelity", flight_planner.participant_id - ) as fidelity_check: - # TODO(#1326): Relax/remove this global fidelity check when each individual scenario validates that - # the flight, as injected, will satisfy the needs of the scenario. - require_compatible_values( - flight_info, - resp.as_planned, - fidelity_check, - default_compatibility=values_exactly_equal, - compatibility={ - JSONPath( - "$.basic_information.area[*].time_start" - ): times_not_later_than_specified_or_now - }, - ) - as_planned = resp.as_planned - else: - as_planned = flight_info + return resp, flight_id, None + + if "as_planned" in resp and resp.as_planned: + with scenario.check( + "Injection fidelity", flight_planner.participant_id + ) as fidelity_check: + # TODO(#1326): Relax/remove this global fidelity check when each individual scenario validates that + # the flight, as injected, will satisfy the needs of the scenario. + require_compatible_values( + flight_info, + resp.as_planned, + fidelity_check, + default_compatibility=values_exactly_equal, + compatibility={ + JSONPath( + "$.basic_information.area[*].time_start" + ): times_not_later_than_specified_or_now + }, + ) + as_planned = resp.as_planned else: - as_planned = None + as_planned = flight_info return resp, flight_id, as_planned From 55eb84124bf6026dc46e0d38efe6a36d64574881 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 18 Feb 2026 18:06:15 +0000 Subject: [PATCH 7/7] `make format` --- monitoring/monitorlib/clients/flight_planning/client_scd.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monitoring/monitorlib/clients/flight_planning/client_scd.py b/monitoring/monitorlib/clients/flight_planning/client_scd.py index 5ab6c6652a..85837d1849 100644 --- a/monitoring/monitorlib/clients/flight_planning/client_scd.py +++ b/monitoring/monitorlib/clients/flight_planning/client_scd.py @@ -168,7 +168,9 @@ def _inject( ) except ValueError: # Best effort failed so it's ok to ignore additional `as_planned` field - logger.warning("SCD API response contained unparseable `as_planned` supplemental field") + logger.warning( + "SCD API response contained unparseable `as_planned` supplemental field" + ) pass self._plan_statuses[flight_id] = response.flight_plan_status