4343from airflow .models .dagbundle import DagBundleModel
4444from airflow .models .renderedtifields import RenderedTaskInstanceFields as RTIF
4545from airflow .models .task_store import TaskStoreModel
46+ from airflow .models .taskinstance import uuid7
4647from airflow .models .taskinstancehistory import TaskInstanceHistory
4748from airflow .models .taskmap import TaskMap
4849from airflow .models .team import Team
4950from airflow .models .trigger import Trigger
50- from airflow .providers .standard .operators .empty import EmptyOperator
51- from airflow .sdk import BaseOperator , TaskGroup
51+ from airflow .sdk import BaseOperator
5252from airflow .state .metastore import MetastoreStoreBackend
5353from airflow .utils .platform import getuser
5454from airflow .utils .state import DagRunState , State , TaskInstanceState
@@ -979,10 +979,6 @@ def test_rendered_map_index_order_stable_regardless_of_uuid_order(self, test_cli
979979 This verifies that even when UUIDs are assigned out of map_index order
980980 (as happens during retries), the response is still sorted 0, 1, 2, ...
981981 """
982- from sqlalchemy import update as sa_update
983-
984- from airflow .models .taskinstance import uuid7
985-
986982 self .create_dag_runs_with_mapped_tasks (
987983 dag_maker ,
988984 session ,
@@ -994,7 +990,7 @@ def test_rendered_map_index_order_stable_regardless_of_uuid_order(self, test_cli
994990 # result must still follow integer map_index order, not UUID order.
995991 for map_index in [1 , 3 ]:
996992 session .execute (
997- sa_update (TaskInstance )
993+ update (TaskInstance )
998994 .where (
999995 TaskInstance .dag_id == "retry_dag" ,
1000996 TaskInstance .task_id == "task_2" ,
@@ -2117,46 +2113,46 @@ def test_cursor_pagination_invalid_token(self, test_client, session):
21172113 )
21182114 assert response .status_code == 400
21192115
2120- def test_task_group_filter_uses_run_version_not_latest (self , test_client , dag_maker , session ):
2116+ def test_cursor_pagination_order_by_run_after_roundtrips (self , test_client , session ):
21212117 """
2122- Task group lookup should use the DAG version from the run, not the latest version.
2118+ Sorting by ``run_after`` (a column-form ``to_replace`` backed by an association proxy)
2119+ must not raise a 500 when ``has_next=true``. Regression for
2120+ https://github.com/apache/airflow/issues/67970.
21232121
2124- When a task group is renamed between versions, clicking on a historical run's
2125- task group in the grid should still resolve correctly against the version
2126- that run was created with — not the latest version where the group may have
2127- a different name, i.e serialized_dag might not have that taskgroup anymore.
2122+ Verify the full cursor round-trip: the first page must include a ``next_cursor``,
2123+ and following that cursor must return the remaining TIs without overlap.
21282124 """
2129- dag_id = "test_tg_version"
2130-
2131- # Version 1: task group named "process_data"
2132- with dag_maker (dag_id , session = session ):
2133- with TaskGroup (group_id = "process_data" ):
2134- EmptyOperator (task_id = "step_1" )
2135- dag_maker .create_dagrun (run_id = "run_v1" )
2136- session .commit ()
2137-
2138- # Version 2: task group renamed to "process_data_v2"
2139- with dag_maker (dag_id , session = session ):
2140- with TaskGroup (group_id = "process_data_v2" ):
2141- EmptyOperator (task_id = "step_1" )
2142- session .commit ()
2143-
2144- # The run was created with v1 which had "process_data".
2145- # Querying with the old group name must succeed.
2146- response = test_client .get (
2147- f"/dags/{ dag_id } /dagRuns/run_v1/taskInstances" ,
2148- params = {"task_group_id" : "process_data" },
2125+ dag_id = "example_python_operator"
2126+ total_tis = 5
2127+ self .create_task_instances (
2128+ session ,
2129+ task_instances = [
2130+ {"start_date" : DEFAULT_DATETIME_1 + dt .timedelta (minutes = (i + 1 ))} for i in range (total_tis )
2131+ ],
2132+ dag_id = dag_id ,
21492133 )
2150- assert response .status_code == 200 , response .json ()
2151- assert response .json ()["total_entries" ] == 1
2152- assert response .json ()["task_instances" ][0 ]["task_id" ] == "process_data.step_1"
2134+ # First page — limit < total so next_cursor must be present
2135+ response1 = test_client .get (
2136+ "/dags/~/dagRuns/~/taskInstances" ,
2137+ params = {"limit" : 3 , "order_by" : ["-run_after" ], "cursor" : "" },
2138+ )
2139+ assert response1 .status_code == 200 , response1 .json ()
2140+ body1 = response1 .json ()
2141+ assert len (body1 ["task_instances" ]) == 3
2142+ next_cursor = body1 ["next_cursor" ]
2143+ assert next_cursor is not None , "next_cursor must be present when more rows exist"
21532144
2154- # The new group name should NOT be found in the old run's version.
2155- response = test_client .get (
2156- f "/dags/{ dag_id } /dagRuns/run_v1 /taskInstances" ,
2157- params = {"task_group_id " : "process_data_v2" },
2145+ # Second page — follow the cursor; must not 500 and must return remaining TIs
2146+ response2 = test_client .get (
2147+ "/dags/~ /dagRuns/~ /taskInstances" ,
2148+ params = {"limit " : 10 , "order_by" : [ "-run_after" ], "cursor" : next_cursor },
21582149 )
2159- assert response .status_code == 404
2150+ assert response2 .status_code == 200 , response2 .json ()
2151+ body2 = response2 .json ()
2152+ ids1 = {ti ["id" ] for ti in body1 ["task_instances" ]}
2153+ ids2 = {ti ["id" ] for ti in body2 ["task_instances" ]}
2154+ assert ids1 .isdisjoint (ids2 ), "Pages must not overlap"
2155+ assert len (ids1 ) + len (ids2 ) == total_tis
21602156
21612157
21622158class TestGetTaskDependencies (TestTaskInstanceEndpoint ):
0 commit comments