From 920c974b88ec07627b3efe3cc93e53d55d43d1e2 Mon Sep 17 00:00:00 2001 From: Christie Williams Date: Mon, 5 Oct 2026 15:54:26 -0400 Subject: [PATCH] test(client): assert a failed init_client can be retried _resolve_client assigns _client only on success, so a call made before LD_SDK_KEY is available already leaves nothing behind and the next call retries. Nothing asserted it. These tests match the ones added to TypeScript in launchdarkly/js-ai-sdk#105, where the first rejection was cached for the life of the process: - fail without a key, then init_client({"sdkKey": ...}) succeeds - fail, shutdown(), then init succeeds - inspect_config, which swallows the init error, reports disabled while the key is missing and enabled once it is set Co-Authored-By: Claude Opus 5.5 --- packages/client/tests/test_lifecycle.py | 68 +++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/client/tests/test_lifecycle.py b/packages/client/tests/test_lifecycle.py index 589bb8cc..7ffc33ae 100644 --- a/packages/client/tests/test_lifecycle.py +++ b/packages/client/tests/test_lifecycle.py @@ -261,6 +261,24 @@ async def test_a_failed_telemetry_setup_leaves_no_client_behind( ): assert await init_client() is healthy + async def test_retries_after_a_failed_init_instead_of_replaying_it(self) -> None: + # A failure must not be cached: an app whose first call ran before its + # key was available would otherwise fail for the life of the process. + stub = _make_stub_client() + mock_ld = MagicMock() + mock_ld.Config = MagicMock(return_value=MagicMock()) + mock_ld.LDClient = MagicMock(return_value=stub) + env = {k: v for k, v in os.environ.items() if k != "LD_SDK_KEY"} + with patch.dict(os.environ, env, clear=True): + with patch.object(lifecycle_module, "_setup_telemetry", return_value=None): + with patch("importlib.import_module", return_value=mock_ld): + with pytest.raises(RuntimeError, match="SDK key"): + await init_client() + + assert await init_client({"sdkKey": "late-key"}) is stub + mock_ld.Config.assert_called_once_with("late-key") + assert get_client() is stub + async def test_returns_initialized_client(self) -> None: stub = _make_stub_client() mock_ld = MagicMock() @@ -347,6 +365,22 @@ async def test_allows_reinitialization(self) -> None: await init_client(client=stub2) assert get_client() is stub2 + async def test_allows_initialization_after_a_failed_init(self) -> None: + stub = _make_stub_client() + mock_ld = MagicMock() + mock_ld.Config = MagicMock(return_value=MagicMock()) + mock_ld.LDClient = MagicMock(return_value=stub) + env = {k: v for k, v in os.environ.items() if k != "LD_SDK_KEY"} + with patch.dict(os.environ, env, clear=True): + with patch.object(lifecycle_module, "_setup_telemetry", return_value=None): + with patch("importlib.import_module", return_value=mock_ld): + with pytest.raises(RuntimeError, match="SDK key"): + await init_client() + await shutdown() + + await init_client({"sdkKey": "test-key"}) + assert get_client() is stub + async def test_releases_the_global_tracer_provider( self, restore_otel_globals: Any ) -> None: @@ -781,6 +815,40 @@ async def test_returns_enabled_true_and_null_config_on_schema_failure(self) -> N assert result["enabled"] is True assert result["config"] is None + async def test_recovers_once_sdk_key_is_available_after_a_failed_lazy_init( + self, + ) -> None: + # inspect_config swallows the init error, so a cached failure would make + # an app that called it too early serve disabled configs forever. + stub = _make_stub_client() + stub.variation = AsyncMock( + return_value={ + "_ldMeta": {"enabled": True, "variationKey": "v1", "version": 1}, + "model": {"name": "claude-3-5"}, + "provider": {"name": "Anthropic"}, + "instructions": "You are helpful.", + } + ) + mock_ld = MagicMock() + mock_ld.Config = MagicMock(return_value=MagicMock()) + mock_ld.LDClient = MagicMock(return_value=stub) + ctx = {"kind": "user", "key": "user-1"} + env = {k: v for k, v in os.environ.items() if k != "LD_SDK_KEY"} + with patch.dict(os.environ, env, clear=True): + with patch.object(lifecycle_module, "_setup_telemetry", return_value=None): + with patch("importlib.import_module", return_value=mock_ld): + with patch( + "launchdarkly_ai_server.utils.to_ld_context", + side_effect=lambda _c, ctx: ctx, + ): + assert (await inspect_config("my-flag", ctx))[ + "enabled" + ] is False + + os.environ["LD_SDK_KEY"] = "test-key" + assert (await inspect_config("my-flag", ctx))["enabled"] is True + mock_ld.LDClient.assert_called_once() + async def test_never_raises_when_variation_throws(self) -> None: stub = _make_stub_client() stub.variation = AsyncMock(side_effect=RuntimeError("network error"))