This is an automated email from the ASF dual-hosted git repository.

o-nikolas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new f9dca510f9f Fix global env vars shadowing team secrets in tasks and 
triggers (#75998)
f9dca510f9f is described below

commit f9dca510f9f9ea8ac9652919dcce941e7286a680
Author: Niko Oliveira <[email protected]>
AuthorDate: Thu Nov 12 09:09:41 2026 -0700

    Fix global env vars shadowing team secrets in tasks and triggers (#75998)
    
    In multi-team mode, tasks and triggers looked up Variables and
    Connections without passing their team to the secrets backends. The
    environment variables backend is checked first, so a global
    AIRFLOW_VAR_<KEY> was returned even when AIRFLOW_VAR__<TEAM>___<KEY>
    was set. The docs say the team value takes precedence, and the
    Execution API, which does resolve the team, was never reached.
---
 .../src/airflow/jobs/triggerer_job_runner.py       |  3 +-
 airflow-core/tests/unit/jobs/test_triggerer_job.py | 15 ++++
 .../edge3/example_dags/integration_test.py         |  2 +-
 .../providers/edge3/example_dags/win_test.py       |  2 +-
 task-sdk/src/airflow/sdk/execution_time/context.py | 80 ++++++++++++++++------
 .../src/airflow/sdk/execution_time/task_runner.py  |  6 ++
 .../tests/task_sdk/definitions/test_connection.py  |  2 +-
 .../tests/task_sdk/definitions/test_variables.py   |  2 +-
 .../tests/task_sdk/execution_time/test_context.py  | 58 ++++++++++++++++
 .../task_sdk/execution_time/test_context_cache.py  | 16 ++++-
 .../task_sdk/execution_time/test_task_runner.py    | 43 +++++++++++-
 11 files changed, 201 insertions(+), 28 deletions(-)

diff --git a/airflow-core/src/airflow/jobs/triggerer_job_runner.py 
b/airflow-core/src/airflow/jobs/triggerer_job_runner.py
index 649b8ca8276..afb50da0ef2 100644
--- a/airflow-core/src/airflow/jobs/triggerer_job_runner.py
+++ b/airflow-core/src/airflow/jobs/triggerer_job_runner.py
@@ -108,7 +108,7 @@ from airflow.sdk.execution_time.comms import (
     _new_encoder,
     _RequestFrame,
 )
-from airflow.sdk.execution_time.context import AssetStateStoreAccessors
+from airflow.sdk.execution_time.context import AssetStateStoreAccessors, 
set_current_team_name
 from airflow.sdk.execution_time.request_handlers import (
     handle_clear_asset_state_store_by_name,
     handle_clear_asset_state_store_by_uri,
@@ -1611,6 +1611,7 @@ class TriggerRunner:
             raise RuntimeError(f"Required first message to be a 
messages.StartTriggerer, it was {msg}")
 
         self.team_name = msg.team_name
+        set_current_team_name(msg.team_name)
 
         await self.comms_decoder.start_reader()
 
diff --git a/airflow-core/tests/unit/jobs/test_triggerer_job.py 
b/airflow-core/tests/unit/jobs/test_triggerer_job.py
index e38fbe0343b..2a1f6265f65 100644
--- a/airflow-core/tests/unit/jobs/test_triggerer_job.py
+++ b/airflow-core/tests/unit/jobs/test_triggerer_job.py
@@ -1546,6 +1546,21 @@ def 
test_trigger_logger_fd_closed_when_upload_to_remote_raises(jobless_superviso
 
 
 class TestTriggerRunner:
+    @pytest.mark.asyncio
+    @mock.patch("airflow.jobs.triggerer_job_runner.set_current_team_name", 
autospec=True)
+    @mock.patch("airflow.sdk.execution_time.task_runner.SUPERVISOR_COMMS", 
create=True)
+    @mock.patch("airflow.jobs.triggerer_job_runner.TriggerCommsDecoder", 
autospec=True)
+    @mock.patch("asyncio.open_connection", autospec=True, return_value=(None, 
None))
+    @mock.patch("airflow.jobs.triggerer_job_runner.socket", autospec=True)
+    async def test_init_comms_sets_the_current_team(
+        self, _socket, _open_connection, mock_decoder_cls, _supervisor_comms, 
mock_set_current_team_name
+    ) -> None:
+        mock_decoder_cls.return_value._from_frame.return_value = 
messages.StartTriggerer(team_name="team_a")
+
+        await TriggerRunner().init_comms()
+
+        mock_set_current_team_name.assert_called_once_with("team_a")
+
     def test_blocked_main_thread_warning_threshold_decode(self) -> None:
         with conf_vars({("triggerer", 
"blocked_main_thread_warning_threshold"): "0.5"}):
             trigger_runner = TriggerRunner()
diff --git 
a/providers/edge3/src/airflow/providers/edge3/example_dags/integration_test.py 
b/providers/edge3/src/airflow/providers/edge3/example_dags/integration_test.py
index cea09293c30..2d1bf2a4fa4 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/example_dags/integration_test.py
+++ 
b/providers/edge3/src/airflow/providers/edge3/example_dags/integration_test.py
@@ -128,7 +128,7 @@ with DAG(
     def connection():
         try:
             conn = BaseHook.get_connection("integration_test")
-            print(f"Got connection {conn}")
+            print(f"Got connection {conn.conn_id}")
         except AirflowNotFoundException:
             print("Connection not found... but also OK.")
 
diff --git 
a/providers/edge3/src/airflow/providers/edge3/example_dags/win_test.py 
b/providers/edge3/src/airflow/providers/edge3/example_dags/win_test.py
index 43fd6f40ee5..47a4fff934a 100644
--- a/providers/edge3/src/airflow/providers/edge3/example_dags/win_test.py
+++ b/providers/edge3/src/airflow/providers/edge3/example_dags/win_test.py
@@ -311,7 +311,7 @@ with DAG(
         def connection():
             try:
                 conn = BaseHook.get_connection("integration_test")
-                print(f"Got connection {conn}")
+                print(f"Got connection {conn.conn_id}")
             except AirflowNotFoundException:
                 print("Connection 'integration_test' not found... but also 
OK.")
 
diff --git a/task-sdk/src/airflow/sdk/execution_time/context.py 
b/task-sdk/src/airflow/sdk/execution_time/context.py
index e9a36aba790..60c245bf5a7 100644
--- a/task-sdk/src/airflow/sdk/execution_time/context.py
+++ b/task-sdk/src/airflow/sdk/execution_time/context.py
@@ -22,6 +22,7 @@ import contextlib
 import functools
 import inspect
 import json
+import os
 import warnings
 from collections.abc import Generator, Iterable, Iterator, Mapping, Sequence
 from contextvars import ContextVar
@@ -35,6 +36,7 @@ import structlog
 from pydantic import AwareDatetime, TypeAdapter
 
 from airflow.sdk._shared.observability.metrics import stats
+from airflow.sdk._shared.secrets_backend.base import 
call_secrets_backend_method
 from airflow.sdk._shared.state import AssetScope
 from airflow.sdk.configuration import conf
 from airflow.sdk.definitions._internal.contextmanager import _CURRENT_CONTEXT
@@ -234,6 +236,22 @@ _preset_connections: ContextVar[dict[str, Connection] | 
None] = ContextVar(
     "_preset_connections", default=None
 )
 
+# A plain global rather than a ContextVar, so threads started by task code see 
it too.
+# Also exported to the environment, so child interpreters such as virtualenv 
tasks can
+# restore it in reinit_supervisor_comms.
+_current_team_name: str | None = None
+_CURRENT_TEAM_NAME_ENV_VAR = "_AIRFLOW__CURRENT_TEAM_NAME"
+
+
+def set_current_team_name(team_name: str | None) -> None:
+    """Set the team that Connection and Variable lookups in this process and 
its children belong to."""
+    global _current_team_name
+    _current_team_name = team_name
+    if team_name:
+        os.environ[_CURRENT_TEAM_NAME_ENV_VAR] = team_name
+    else:
+        os.environ.pop(_CURRENT_TEAM_NAME_ENV_VAR, None)
+
 
 def _get_connection(conn_id: str) -> Connection:
     preset = _preset_connections.get()
@@ -247,7 +265,7 @@ def _get_connection(conn_id: str) -> Connection:
 
     # Check cache first (optional; only on dag processor)
     try:
-        uri = SecretCache.get_connection_uri(conn_id)
+        uri = SecretCache.get_connection_uri(conn_id, 
team_name=_current_team_name)
         from airflow.sdk.definitions.connection import Connection
 
         conn = Connection.from_uri(uri, conn_id=conn_id)
@@ -261,9 +279,11 @@ def _get_connection(conn_id: str) -> Connection:
     backends = ensure_secrets_backend_loaded()
     for secrets_backend in backends:
         try:
-            conn = secrets_backend.get_connection(conn_id=conn_id)  # type: 
ignore[assignment]
+            conn = call_secrets_backend_method(
+                secrets_backend.get_connection, team_name=_current_team_name, 
conn_id=conn_id
+            )
             if conn:
-                SecretCache.save_connection_uri(conn_id, conn.get_uri())
+                SecretCache.save_connection_uri(conn_id, conn.get_uri(), 
team_name=_current_team_name)
                 _mask_connection_secrets(conn)
                 return conn
         except AirflowSecretsBackendAccessDenied:
@@ -295,7 +315,7 @@ async def _async_get_connection(conn_id: str) -> Connection:
 
     # Check cache first
     try:
-        uri = SecretCache.get_connection_uri(conn_id)
+        uri = SecretCache.get_connection_uri(conn_id, 
team_name=_current_team_name)
         from airflow.sdk.definitions.connection import Connection
 
         conn = Connection.from_uri(uri, conn_id=conn_id)
@@ -314,10 +334,14 @@ async def _async_get_connection(conn_id: str) -> 
Connection:
             # getattr avoids triggering AsyncMock coroutine creation under 
Python 3.13
             async_method = getattr(secrets_backend, "aget_connection", None)
             if async_method is not None:
-                maybe_awaitable = async_method(conn_id)
+                maybe_awaitable = call_secrets_backend_method(
+                    async_method, team_name=_current_team_name, conn_id=conn_id
+                )
                 conn = await maybe_awaitable if 
inspect.isawaitable(maybe_awaitable) else maybe_awaitable
             else:
-                conn = await 
sync_to_async(secrets_backend.get_connection)(conn_id)  # type: 
ignore[assignment]
+                conn = await sync_to_async(call_secrets_backend_method)(
+                    secrets_backend.get_connection, 
team_name=_current_team_name, conn_id=conn_id
+                )
 
             if conn:
                 # Use aget_uri if the returned connection object supports it 
(the SDK's own
@@ -329,7 +353,7 @@ async def _async_get_connection(conn_id: str) -> Connection:
                     uri = await aget_uri()
                 else:
                     uri = await sync_to_async(conn.get_uri)()
-                SecretCache.save_connection_uri(conn_id, uri)
+                SecretCache.save_connection_uri(conn_id, uri, 
team_name=_current_team_name)
                 await _amask_connection_secrets(conn)
                 return conn
         except AirflowSecretsBackendAccessDenied:
@@ -389,7 +413,7 @@ def _get_variable(key: str, deserialize_json: bool) -> Any:
 
     # Check cache first
     try:
-        var_val = SecretCache.get_variable(key)
+        var_val = SecretCache.get_variable(key, team_name=_current_team_name)
         if var_val is not None:
             return _mask_and_deserialize_variable(var_val, key, 
deserialize_json)
     except SecretCache.NotPresentException:
@@ -400,10 +424,12 @@ def _get_variable(key: str, deserialize_json: bool) -> 
Any:
     # Iterate over backends if not in cache (or expired)
     for secrets_backend in backends:
         try:
-            var_val = secrets_backend.get_variable(key=key)
+            var_val = call_secrets_backend_method(
+                secrets_backend.get_variable, team_name=_current_team_name, 
key=key
+            )
             if var_val is not None:
                 # Save raw value before deserialization to maintain cache 
consistency
-                SecretCache.save_variable(key, var_val)
+                SecretCache.save_variable(key, var_val, 
team_name=_current_team_name)
                 return _mask_and_deserialize_variable(var_val, key, 
deserialize_json)
         except AirflowSecretsBackendAccessDenied:
             # Authoritative deny — must NOT fall through to a less-restrictive 
backend.
@@ -428,7 +454,7 @@ async def _async_get_variable(key: str, deserialize_json: 
bool) -> Any:
 
     # Check cache first
     try:
-        var_val = SecretCache.get_variable(key)
+        var_val = SecretCache.get_variable(key, team_name=_current_team_name)
         if var_val is not None:
             return await _async_mask_and_deserialize_variable(var_val, key, 
deserialize_json)
     except SecretCache.NotPresentException:
@@ -441,12 +467,19 @@ async def _async_get_variable(key: str, deserialize_json: 
bool) -> Any:
         try:
             async_method = getattr(secrets_backend, "aget_variable", None)
             if async_method is not None:
-                var_val = await async_method(key=key)
+                var_val = await call_secrets_backend_method(
+                    async_method, team_name=_current_team_name, key=key
+                )
             else:
-                var_val = await 
asyncio.to_thread(secrets_backend.get_variable, key=key)
+                var_val = await asyncio.to_thread(
+                    call_secrets_backend_method,
+                    secrets_backend.get_variable,
+                    team_name=_current_team_name,
+                    key=key,
+                )
             if var_val is not None:
                 # Save raw value before deserialization to maintain cache 
consistency
-                SecretCache.save_variable(key, var_val)
+                SecretCache.save_variable(key, var_val, 
team_name=_current_team_name)
                 return await _async_mask_and_deserialize_variable(var_val, 
key, deserialize_json)
         except AirflowSecretsBackendAccessDenied:
             # Authoritative deny — must NOT fall through to a less-restrictive 
backend.
@@ -538,7 +571,9 @@ def _set_variable(key: str, value: Any, description: str | 
None = None, serializ
         if isinstance(secrets_backend, ExecutionAPISecretsBackend):
             continue
         try:
-            var_val = secrets_backend.get_variable(key=key)
+            var_val = call_secrets_backend_method(
+                secrets_backend.get_variable, team_name=_current_team_name, 
key=key
+            )
             if var_val is not None:
                 _backend_name = type(secrets_backend).__name__
                 log.warning(
@@ -569,7 +604,7 @@ def _set_variable(key: str, value: Any, description: str | 
None = None, serializ
     SUPERVISOR_COMMS.send(PutVariable(key=key, value=value, 
description=description))
 
     # Invalidate cache after setting the variable
-    SecretCache.invalidate_variable(key)
+    SecretCache.invalidate_variable(key, team_name=_current_team_name)
 
 
 async def _async_set_variable(
@@ -595,7 +630,12 @@ async def _async_set_variable(
         if isinstance(secrets_backend, ExecutionAPISecretsBackend):
             continue
         try:
-            var_val = await asyncio.to_thread(secrets_backend.get_variable, 
key=key)
+            var_val = await asyncio.to_thread(
+                call_secrets_backend_method,
+                secrets_backend.get_variable,
+                team_name=_current_team_name,
+                key=key,
+            )
             if var_val is not None:
                 _backend_name = type(secrets_backend).__name__
                 log.warning(
@@ -626,7 +666,7 @@ async def _async_set_variable(
     await SUPERVISOR_COMMS.asend(PutVariable(key=key, value=value, 
description=description))
 
     # Invalidate cache after setting the variable
-    SecretCache.invalidate_variable(key)
+    SecretCache.invalidate_variable(key, team_name=_current_team_name)
 
 
 def _delete_variable(key: str) -> None:
@@ -643,7 +683,7 @@ def _delete_variable(key: str) -> None:
         assert isinstance(msg, OKResponse)
 
     # Invalidate cache after deleting the variable
-    SecretCache.invalidate_variable(key)
+    SecretCache.invalidate_variable(key, team_name=_current_team_name)
 
 
 async def _async_delete_variable(key: str) -> None:
@@ -660,7 +700,7 @@ async def _async_delete_variable(key: str) -> None:
         assert isinstance(msg, OKResponse)
 
     # Invalidate cache after deleting the variable
-    SecretCache.invalidate_variable(key)
+    SecretCache.invalidate_variable(key, team_name=_current_team_name)
 
 
 class ConnectionAccessor:
diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py 
b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
index c89cdeed176..a173c734670 100644
--- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py
+++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
@@ -143,6 +143,7 @@ from airflow.sdk.execution_time.comms import (
     ValidateInletsAndOutlets,
 )
 from airflow.sdk.execution_time.context import (
+    _CURRENT_TEAM_NAME_ENV_VAR,
     AssetStateStoreAccessors,
     ConnectionAccessor,
     InletEventsAccessors,
@@ -156,6 +157,7 @@ from airflow.sdk.execution_time.context import (
     context_to_airflow_vars,
     get_previous_dagrun_success,
     set_current_context,
+    set_current_team_name,
 )
 from airflow.sdk.execution_time.email_backend import (
     _DEFAULT_EMAIL_BACKEND,
@@ -1305,6 +1307,8 @@ def startup(msg: StartupDetails) -> 
tuple[RuntimeTaskInstance, Context, Logger]:
     except Exception:
         log.exception("error calling listener")
 
+    set_current_team_name(msg.ti_context.dag_run.team_name)
+
     with _airflow_parsing_context_manager(dag_id=msg.ti.dag_id, 
task_id=msg.ti.task_id):
         ti = parse(msg, log)
     log.debug("Dag file parsed", file=msg.dag_rel_path)
@@ -2771,6 +2775,8 @@ def reinit_supervisor_comms() -> None:
     """
     import socket
 
+    set_current_team_name(os.environ.get(_CURRENT_TEAM_NAME_ENV_VAR))
+
     if "SUPERVISOR_COMMS" not in globals():
         global SUPERVISOR_COMMS
 
diff --git a/task-sdk/tests/task_sdk/definitions/test_connection.py 
b/task-sdk/tests/task_sdk/definitions/test_connection.py
index aad4c8f46db..0f40e0c5ea6 100644
--- a/task-sdk/tests/task_sdk/definitions/test_connection.py
+++ b/task-sdk/tests/task_sdk/definitions/test_connection.py
@@ -378,7 +378,7 @@ class TestConnectionsFromSecrets:
         """Tests getting a connection from environment variable."""
         mock_env_get.return_value = Connection(conn_id="something", 
conn_type="some-type")  # return None
         Connection.get("something")
-        mock_env_get.assert_called_once_with(conn_id="something")
+        mock_env_get.assert_called_once_with(conn_id="something", 
team_name=None)
 
     @conf_vars(
         {
diff --git a/task-sdk/tests/task_sdk/definitions/test_variables.py 
b/task-sdk/tests/task_sdk/definitions/test_variables.py
index 77f5245ee78..dd8a8a3dfbf 100644
--- a/task-sdk/tests/task_sdk/definitions/test_variables.py
+++ b/task-sdk/tests/task_sdk/definitions/test_variables.py
@@ -444,7 +444,7 @@ class TestVariableFromSecrets:
         """Tests getting a variable from environment variable."""
         mock_env_get.return_value = "fake_value"
         Variable.get(key="fake_var_key")
-        mock_env_get.assert_called_once_with(key="fake_var_key")
+        mock_env_get.assert_called_once_with(key="fake_var_key", 
team_name=None)
 
     @conf_vars(
         {
diff --git a/task-sdk/tests/task_sdk/execution_time/test_context.py 
b/task-sdk/tests/task_sdk/execution_time/test_context.py
index 9dba8531117..1e711074c65 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_context.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_context.py
@@ -110,8 +110,10 @@ from airflow.sdk.execution_time.context import (
     _async_get_variable_keys,
     _async_set_variable,
     _get_connection,
+    _get_variable,
     _json_size_exceeds,
     _process_connection_result_conn,
+    _set_variable,
     _wrap_external_ref,
     context_to_airflow_vars,
     set_current_context,
@@ -1813,6 +1815,62 @@ class TestJsonSizeExceeds:
         assert _json_size_exceeds(value, 10) is True
 
 
+class TestTeamSecretLookups:
+    """Lookups pass the current team, so a team's env var wins over a global 
one of the same name."""
+
+    @pytest.fixture(autouse=True)
+    def _team_a_and_global_env_vars(self, monkeypatch):
+        from airflow.secrets.environment_variables import 
EnvironmentVariablesBackend
+
+        monkeypatch.setenv("AIRFLOW_VAR_MY_VAR", "global_value")
+        monkeypatch.setenv("AIRFLOW_VAR__TEAM_A___MY_VAR", "team_a_value")
+        monkeypatch.setenv("AIRFLOW_CONN_MY_CONN", "http://global.example.com";)
+        monkeypatch.setenv("AIRFLOW_CONN__TEAM_A___MY_CONN", 
"http://team-a.example.com";)
+        
monkeypatch.setattr("airflow.sdk.execution_time.context._current_team_name", 
"team_a")
+
+        backend = EnvironmentVariablesBackend()
+        backend._set_connection_class(Connection)
+        monkeypatch.setattr(
+            
"airflow.sdk.execution_time.supervisor.ensure_secrets_backend_loaded", lambda: 
[backend]
+        )
+
+    def test_sync_lookups(self):
+        assert _get_variable("my_var", deserialize_json=False) == 
"team_a_value"
+        assert _get_connection("my_conn").host == "team-a.example.com"
+
+    @pytest.mark.asyncio
+    async def test_async_lookups(self):
+        assert await _async_get_variable("my_var", deserialize_json=False) == 
"team_a_value"
+        assert (await _async_get_connection("my_conn")).host == 
"team-a.example.com"
+
+    @pytest.mark.asyncio
+    async def test_async_backend_methods(self, monkeypatch):
+        class AsyncBackend:
+            async def aget_variable(self, key, team_name=None):
+                return f"{team_name}_value"
+
+            async def aget_connection(self, conn_id, team_name=None):
+                return Connection(conn_id=conn_id, conn_type="http", 
host=f"{team_name}.example.com")
+
+        monkeypatch.setattr(
+            
"airflow.sdk.execution_time.supervisor.ensure_secrets_backend_loaded", lambda: 
[AsyncBackend()]
+        )
+
+        assert await _async_get_variable("my_var", deserialize_json=False) == 
"team_a_value"
+        assert (await _async_get_connection("my_conn")).host == 
"team_a.example.com"
+
+    @pytest.mark.asyncio
+    @mock.patch("airflow.sdk.execution_time.context.log")
+    async def test_set_variable_warns_about_team_env_var(self, mock_log, 
monkeypatch, mock_supervisor_comms):
+        monkeypatch.delenv("AIRFLOW_VAR_MY_VAR")
+        mock_supervisor_comms.asend.return_value = None
+
+        _set_variable("my_var", "new_value")
+        await _async_set_variable("my_var", "new_value")
+
+        assert mock_log.warning.call_count == 2
+
+
 class TestTaskStateStoreAccessor:
     TI_ID = UUID("01900000-0000-0000-0000-000000000001")
     SCOPE = TaskScope(dag_id="dag", run_id="run", task_id="task")
diff --git a/task-sdk/tests/task_sdk/execution_time/test_context_cache.py 
b/task-sdk/tests/task_sdk/execution_time/test_context_cache.py
index 58c9c51e153..924466bbaf4 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_context_cache.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_context_cache.py
@@ -79,7 +79,7 @@ class TestConnectionCacheIntegration:
         result = _get_connection(conn_id)
         assert result.conn_id == conn_id
         assert result.conn_type == "mysql"
-        mock_backend.get_connection.assert_called_once_with(conn_id=conn_id)
+        mock_backend.get_connection.assert_called_once_with(conn_id=conn_id, 
team_name=None)
 
         cached_uri = SecretCache.get_connection_uri(conn_id)
         cached_conn = Connection.from_uri(cached_uri, conn_id=conn_id)
@@ -180,10 +180,22 @@ class TestVariableCacheIntegration:
 
         result = _get_variable(key, deserialize_json=False)
         assert result == value
-        mock_backend.get_variable.assert_called_once_with(key=key)
+        mock_backend.get_variable.assert_called_once_with(key=key, 
team_name=None)
         cached_value = SecretCache.get_variable(key)
         assert cached_value == value
 
+    
@patch("airflow.sdk.execution_time.supervisor.ensure_secrets_backend_loaded")
+    def test_get_variable_cache_is_keyed_by_team(self, mock_ensure_backends, 
monkeypatch):
+        
monkeypatch.setattr("airflow.sdk.execution_time.context._current_team_name", 
"team_a")
+        SecretCache.save_variable("my_var", "global_value")
+
+        mock_backend = MagicMock(spec=["get_variable"])
+        mock_backend.get_variable.return_value = "team_a_value"
+        mock_ensure_backends.return_value = [mock_backend]
+
+        assert _get_variable("my_var", deserialize_json=False) == 
"team_a_value"
+        assert SecretCache.get_variable("my_var", team_name="team_a") == 
"team_a_value"
+
     
@patch("airflow.sdk.execution_time.supervisor.ensure_secrets_backend_loaded")
     def test_get_variable_from_api_saves_to_cache(self, mock_ensure_backends, 
mock_supervisor_comms):
         """Test that variable from API server is saved to cache."""
diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py 
b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
index 14c798def40..64beb944057 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
@@ -111,7 +111,7 @@ from airflow.sdk.exceptions import (
     ErrorType,
     TaskDeferred,
 )
-from airflow.sdk.execution_time import task_runner
+from airflow.sdk.execution_time import context as context_module, task_runner
 from airflow.sdk.execution_time.comms import (
     AssetEventResult,
     AssetEventsResult,
@@ -181,6 +181,7 @@ from airflow.sdk.execution_time.context import (
     TriggeringAssetEventsAccessor,
     VariableAccessor,
     _wrap_external_ref,
+    set_current_team_name,
 )
 from airflow.sdk.execution_time.lazy_sequence import LazyXComSequence
 from airflow.sdk.execution_time.task_runner import (
@@ -200,6 +201,7 @@ from airflow.sdk.execution_time.task_runner import (
     finalize,
     get_startup_details,
     parse,
+    reinit_supervisor_comms,
     run,
     startup,
 )
@@ -2283,6 +2285,45 @@ def test_startup_and_run_dag_with_rtif(
     mock_supervisor_comms.assert_has_calls(expected_calls)
 
 
[email protected]("airflow.sdk.execution_time.task_runner.set_current_team_name", 
autospec=True)
+def test_startup_sets_the_current_team(mock_set_current_team_name, 
mocked_parse, make_ti_context):
+    ti_context = make_ti_context()
+    ti_context.dag_run.team_name = "team_a"
+    what = StartupDetails(
+        ti=TaskInstance(
+            id=uuid7(),
+            task_id="my_task",
+            dag_id="basic_dag",
+            run_id="c",
+            try_number=1,
+            dag_version_id=uuid7(),
+            queue="default",
+        ),
+        dag_rel_path="",
+        bundle_info=FAKE_BUNDLE,
+        ti_context=ti_context,
+        start_date=timezone.utcnow(),
+        sentry_integration="",
+    )
+    mocked_parse(what, "basic_dag", CustomOperator(task_id="my_task"))
+
+    startup(what)
+
+    mock_set_current_team_name.assert_called_once_with("team_a")
+
+
[email protected](os.environ)
+def test_reinit_supervisor_comms_restores_the_current_team(monkeypatch, 
mock_supervisor_comms):
+    monkeypatch.setattr(context_module, "_current_team_name", None)
+    set_current_team_name("team_a")
+    # A child interpreter starts with only the environment it inherited.
+    monkeypatch.setattr(context_module, "_current_team_name", None)
+
+    reinit_supervisor_comms()
+
+    assert context_module._current_team_name == "team_a"
+
+
 @patch("os.execvp")
 @patch("os.set_inheritable")
 def test_task_run_with_user_impersonation(

Reply via email to