@@ -1376,34 +1376,6 @@ def test_signing_state_is_per_tool_use(self):
13761376 assert plugin ._has_successful_signing (event2 ) is False
13771377
13781378
1379- class TestExtractPaymentErrorMessage :
1380- """Tests for _extract_payment_error_message static method."""
1381-
1382- def test_extracts_error_from_body (self ):
1383- """Test extracting error message from body dict."""
1384- body = {"error" : "invalid_exact_evm_insufficient_balance" }
1385- assert AgentCorePaymentsPlugin ._extract_payment_error_message (body ) == "invalid_exact_evm_insufficient_balance"
1386-
1387- def test_returns_unknown_for_none_body (self ):
1388- """Test returns 'unknown error' when body is None."""
1389- assert AgentCorePaymentsPlugin ._extract_payment_error_message (None ) == "unknown error"
1390-
1391- def test_returns_unknown_for_empty_error (self ):
1392- """Test returns 'unknown error' when error is empty string."""
1393- body = {"error" : "" }
1394- assert AgentCorePaymentsPlugin ._extract_payment_error_message (body ) == "unknown error"
1395-
1396- def test_returns_unknown_for_missing_error_key (self ):
1397- """Test returns 'unknown error' when error key is missing."""
1398- body = {"statusCode" : 402 }
1399- assert AgentCorePaymentsPlugin ._extract_payment_error_message (body ) == "unknown error"
1400-
1401- def test_returns_unknown_for_non_string_error (self ):
1402- """Test returns 'unknown error' when error is not a string."""
1403- body = {"error" : 42 }
1404- assert AgentCorePaymentsPlugin ._extract_payment_error_message (body ) == "unknown error"
1405-
1406-
14071379class TestAfterToolCallPostPaymentFailure :
14081380 """Tests for the post-payment failure path in after_tool_call."""
14091381
@@ -1443,12 +1415,13 @@ def test_post_payment_failure_does_not_retry_after_successful_signing(self, mock
14431415
14441416 # Should NOT retry
14451417 assert event .retry is False
1446- # Should NOT store failure state (no interrupt cycle)
1447- assert "payment_failure_tool-123" not in event .invocation_state
1418+ # Should store failure state so agent is notified via interrupt
1419+ assert "payment_failure_tool-123" in event .invocation_state
1420+ failure = event .invocation_state ["payment_failure_tool-123" ]
1421+ assert "Payment rejected after signing" in failure ["exceptionMessage" ]
1422+ assert "invalid_exact_evm_insufficient_balance" in failure ["exceptionMessage" ]
14481423 # Should NOT have called generate_payment_header
14491424 mock_pm_instance .generate_payment_header .assert_not_called ()
1450- # Result should remain unchanged (raw 402 for LLM to reason about)
1451- assert event .result == original_result
14521425
14531426 @patch ("bedrock_agentcore.payments.integrations.strands.plugin.PaymentManager" )
14541427 def test_first_402_without_prior_signing_proceeds_normally (self , mock_payment_manager_class ):
@@ -1558,6 +1531,47 @@ def test_signing_retry_limit_stops_processing(self, mock_payment_manager_class):
15581531 assert event .retry is False
15591532
15601533
1534+ class TestSigningFlagTakesPrecedenceOverRetryLimit :
1535+ """Tests that _has_successful_signing check takes precedence over retry limit."""
1536+
1537+ @patch ("bedrock_agentcore.payments.integrations.strands.plugin.PaymentManager" )
1538+ def test_successful_signing_stops_even_when_retry_limit_not_reached (self , mock_payment_manager_class ):
1539+ """Test that post-payment failure stops processing regardless of retry counter state.
1540+
1541+ If signing succeeded (payment_signed_ flag is True), a subsequent 402 should
1542+ stop immediately — even if payment_retry_count is below MAX_PAYMENT_RETRIES.
1543+ The signing flag takes precedence over the retry counter.
1544+ """
1545+ config = AgentCorePaymentsPluginConfig (
1546+ payment_manager_arn = "arn:aws:bedrock-agentcore:us-west-2:123456789012:payment-manager/test" ,
1547+ user_id = "test-user" ,
1548+ payment_instrument_id = "payment-instrument-123" ,
1549+ payment_session_id = "payment-session-456" ,
1550+ )
1551+
1552+ mock_pm_instance = MagicMock ()
1553+ plugin = AgentCorePaymentsPlugin (config = config )
1554+ plugin .payment_manager = mock_pm_instance
1555+
1556+ # Signing succeeded but retry counter is only at 1 (below MAX_PAYMENT_RETRIES=3)
1557+ event , _ = _create_event_with_agent (
1558+ {
1559+ "result" : [{"text" : f"PAYMENT_REQUIRED: { json .dumps ({'statusCode' : 402 , 'headers' : {}, 'body' : {}})} " }],
1560+ "tool_use" : {"name" : "http_request" , "toolUseId" : "tool-123" , "input" : {"headers" : {}}},
1561+ "invocation_state" : {"payment_signed_tool-123" : True , "payment_retry_count_tool-123" : 1 },
1562+ "retry" : False ,
1563+ }
1564+ )
1565+
1566+ plugin .after_tool_call (event )
1567+
1568+ # Should NOT retry — signing flag takes precedence
1569+ assert event .retry is False
1570+ mock_pm_instance .generate_payment_header .assert_not_called ()
1571+ # Should store failure state for interrupt notification
1572+ assert "payment_failure_tool-123" in event .invocation_state
1573+
1574+
15611575class TestAfterToolCallAutoPaymentDisabled :
15621576 """Tests for auto_payment=False in after_tool_call."""
15631577
0 commit comments