@@ -179,7 +179,7 @@ public void testGetAgentCard_interruptedExceptionThrows() throws Exception {
179179 public void testGetAgentCard_withTenant () throws Exception {
180180 TestHttpClient client = createTestClient ();
181181 A2ACardResolver .builder ().httpClient (client ).baseUrl ("http://example.com" ).tenant ("my-tenant" ).build ().getAgentCard ();
182- assertEquals ("http://example.com/my-tenant" + AGENT_CARD_PATH , client .url );
182+ assertEquals ("http://example.com/.well-known/ my-tenant/agent-card.json" , client .url );
183183 }
184184
185185 @ Test
@@ -233,16 +233,16 @@ public void testBuilder_malformedBaseUrl_throws() {
233233
234234 @ Test
235235 public void testFullWellKnownUrlWithTenant () throws Exception {
236- // Full well-known URL + tenant must strip the suffix before appending tenant,
237- // not produce a malformed path like ...agent-card.json/my-tenant/ .well-known/agent-card.json
236+ // Full well-known URL + tenant must strip the suffix before embedding tenant inside the path ,
237+ // not produce a malformed path like ...agent-card.json/.well-known/my-tenant /agent-card.json
238238 TestHttpClient client = createTestClient ();
239239 A2ACardResolver resolver = A2ACardResolver .builder ()
240240 .httpClient (client )
241241 .baseUrl ("https://example.com/.well-known/agent-card.json" )
242242 .tenant ("my-tenant" )
243243 .build ();
244244 resolver .getAgentCard ();
245- assertEquals ("https://example.com/my-tenant" + AGENT_CARD_PATH , client .url );
245+ assertEquals ("https://example.com/.well-known/ my-tenant/agent-card.json" , client .url );
246246 }
247247
248248 // -------------------------------------------------------------------------
@@ -260,7 +260,7 @@ public void testSpec03PathPreservation_wellKnown() throws Exception {
260260 public void testSpec03PathPreservation_withTenant () throws Exception {
261261 TestHttpClient client = createTestClient ();
262262 A2ACardResolver .builder ().httpClient (client ).baseUrl ("https://example.com/spec03" ).tenant ("my-tenant" ).build ().getAgentCard ();
263- assertEquals ("https://example.com/spec03/my-tenant" + AGENT_CARD_PATH , client .url );
263+ assertEquals ("https://example.com/spec03/.well-known/ my-tenant/agent-card.json" , client .url );
264264 }
265265
266266 @ Test
@@ -303,6 +303,134 @@ public void testFullWellKnownUrlWithSameAgentCardPath() throws Exception {
303303 assertEquals ("https://example.com/spec03/.well-known/agent-card.json" , client .url );
304304 }
305305
306+ // -------------------------------------------------------------------------
307+ // Tenant URL — correct /.well-known/{tenant}/agent-card.json pattern
308+ // -------------------------------------------------------------------------
309+
310+ @ Test
311+ public void testTenantUrl_embeddedInWellKnownPath () throws Exception {
312+ TestHttpClient client = createTestClient ();
313+ A2ACardResolver .builder ().httpClient (client ).baseUrl ("http://example.com" ).tenant ("acme" ).build ().getAgentCard ();
314+ assertEquals ("http://example.com/.well-known/acme/agent-card.json" , client .url );
315+ }
316+
317+ @ Test
318+ public void testTenantUrl_fullTenantCardUrlProvidedAsBase_notDuplicated () throws Exception {
319+ // When the caller already supplies the tenant card URL, it must be used as-is.
320+ TestHttpClient client = createTestClient ();
321+ String tenantCardUrl = "http://example.com/.well-known/acme/agent-card.json" ;
322+ A2ACardResolver .builder ().httpClient (client ).baseUrl (tenantCardUrl ).tenant ("acme" ).build ().getAgentCard ();
323+ assertEquals (tenantCardUrl , client .url );
324+ assertEquals (1 , client .urlsCalled .size ());
325+ }
326+
327+ @ Test
328+ public void testTenantUrl_fullTenantCardUrlProvidedAsBase_withTrailingSlash_notDuplicated () throws Exception {
329+ TestHttpClient client = createTestClient ();
330+ A2ACardResolver .builder ().httpClient (client )
331+ .baseUrl ("http://example.com/.well-known/acme/agent-card.json/" )
332+ .tenant ("acme" )
333+ .build ()
334+ .getAgentCard ();
335+ assertEquals ("http://example.com/.well-known/acme/agent-card.json" , client .url );
336+ assertEquals (1 , client .urlsCalled .size ());
337+ }
338+
339+ @ Test
340+ public void testTenantUrl_fullTenantCardUrlProvidedAsBase_noFallback () throws Exception {
341+ // No fallback when the provided URL already IS the computed card URL.
342+ TestHttpClient client = createTestClient ();
343+ client .status = 404 ;
344+ String tenantCardUrl = "http://example.com/.well-known/acme/agent-card.json" ;
345+ A2ACardResolver resolver = A2ACardResolver .builder ().httpClient (client ).baseUrl (tenantCardUrl ).tenant ("acme" ).build ();
346+ assertThrows (A2AClientHTTPError .class , resolver ::getAgentCard );
347+ assertEquals (1 , client .urlsCalled .size ());
348+ }
349+
350+ @ Test
351+ public void testTenantUrl_withSubBasePath () throws Exception {
352+ TestHttpClient client = createTestClient ();
353+ A2ACardResolver .builder ().httpClient (client ).baseUrl ("https://example.com/spec03" ).tenant ("acme" ).build ().getAgentCard ();
354+ assertEquals ("https://example.com/spec03/.well-known/acme/agent-card.json" , client .url );
355+ }
356+
357+ // -------------------------------------------------------------------------
358+ // Fallback to provided URL on HTTP error
359+ // -------------------------------------------------------------------------
360+
361+ @ Test
362+ public void testGetAgentCard_httpError_fallsBackToProvidedUrl () throws Exception {
363+ // Primary URL (/.well-known/agent-card.json) returns 404; fallback to original base URL succeeds.
364+ TestHttpClient client = createTestClient ();
365+ client .statusSequence .add (404 );
366+ client .statusSequence .add (200 );
367+ A2ACardResolver .builder ().httpClient (client ).baseUrl ("http://example.com" ).build ().getAgentCard ();
368+ assertEquals (2 , client .urlsCalled .size ());
369+ assertEquals ("http://example.com" + AGENT_CARD_PATH , client .urlsCalled .get (0 ));
370+ assertEquals ("http://example.com" , client .urlsCalled .get (1 ));
371+ }
372+
373+ @ Test
374+ public void testGetAgentCard_doubleSlashBaseUrl_fallbackUrlNormalized () throws Exception {
375+ // A baseUrl with a double trailing slash must not produce a double-slash fallback URL.
376+ TestHttpClient client = createTestClient ();
377+ client .statusSequence .add (404 );
378+ client .statusSequence .add (200 );
379+ A2ACardResolver .builder ().httpClient (client ).baseUrl ("http://example.com//" ).build ().getAgentCard ();
380+ assertEquals (2 , client .urlsCalled .size ());
381+ assertEquals ("http://example.com" + AGENT_CARD_PATH , client .urlsCalled .get (0 ));
382+ // cleanBase strips one trailing slash from "http://example.com//", yielding "http://example.com/"
383+ assertEquals ("http://example.com/" , client .urlsCalled .get (1 ));
384+ }
385+
386+ @ Test
387+ public void testGetAgentCard_httpError_bothFail_throwsLastError () throws Exception {
388+ // Both primary (/.well-known/agent-card.json) and fallback return 404; last error is propagated
389+ // and the primary error is attached as a suppressed exception.
390+ TestHttpClient client = createTestClient ();
391+ client .status = 404 ;
392+ A2ACardResolver resolver = A2ACardResolver .builder ().httpClient (client ).baseUrl ("http://example.com" ).build ();
393+ A2AClientHTTPError error = assertThrows (A2AClientHTTPError .class , resolver ::getAgentCard );
394+ assertEquals (404 , error .getCode ());
395+ assertEquals (2 , client .urlsCalled .size ());
396+ assertEquals (1 , error .getSuppressed ().length );
397+ assertTrue (error .getSuppressed ()[0 ] instanceof A2AClientHTTPError );
398+ assertEquals (404 , ((A2AClientHTTPError ) error .getSuppressed ()[0 ]).getCode ());
399+ }
400+
401+ @ Test
402+ public void testGetAgentCard_nonNotFound_httpError_noFallback () throws Exception {
403+ // Non-404 errors (e.g. 503) must not trigger the fallback — only 1 request made.
404+ TestHttpClient client = createTestClient ();
405+ client .status = 503 ;
406+ A2ACardResolver resolver = A2ACardResolver .builder ().httpClient (client ).baseUrl ("http://example.com" ).build ();
407+ A2AClientHTTPError error = assertThrows (A2AClientHTTPError .class , resolver ::getAgentCard );
408+ assertEquals (503 , error .getCode ());
409+ assertEquals (1 , client .urlsCalled .size ());
410+ }
411+
412+ @ Test
413+ public void testGetAgentCard_noFallback_whenUrlAlreadyIsCardUrl () throws Exception {
414+ // When the provided URL already equals the computed card URL, no fallback.
415+ TestHttpClient client = createTestClient ();
416+ client .status = 404 ;
417+ String fullCardUrl = "http://example.com" + AGENT_CARD_PATH ;
418+ A2ACardResolver resolver = A2ACardResolver .builder ().httpClient (client ).baseUrl (fullCardUrl ).build ();
419+ assertThrows (A2AClientHTTPError .class , resolver ::getAgentCard );
420+ assertEquals (1 , client .urlsCalled .size ());
421+ }
422+
423+ @ Test
424+ public void testGetAgentCard_withTenant_httpError_fallsBackToProvidedUrl () throws Exception {
425+ TestHttpClient client = createTestClient ();
426+ client .statusSequence .add (404 );
427+ client .statusSequence .add (200 );
428+ A2ACardResolver .builder ().httpClient (client ).baseUrl ("http://example.com" ).tenant ("acme" ).build ().getAgentCard ();
429+ assertEquals (2 , client .urlsCalled .size ());
430+ assertEquals ("http://example.com/.well-known/acme/agent-card.json" , client .urlsCalled .get (0 ));
431+ assertEquals ("http://example.com" , client .urlsCalled .get (1 ));
432+ }
433+
306434 // -------------------------------------------------------------------------
307435 // Helpers
308436 // -------------------------------------------------------------------------
@@ -319,6 +447,7 @@ private String printAgentCard(AgentCard agentCard) throws InvalidProtocolBufferE
319447
320448 private static class TestHttpClient implements A2AHttpClient {
321449 int status = 200 ;
450+ List <Integer > statusSequence = new ArrayList <>();
322451 String body ;
323452 String url ;
324453 boolean throwIOException = false ;
@@ -352,7 +481,7 @@ public A2AHttpResponse get() throws IOException, InterruptedException {
352481 if (throwInterruptedException ) {
353482 throw new InterruptedException ("Simulated interrupt" );
354483 }
355- int effectiveStatus = status ;
484+ int effectiveStatus = statusSequence . isEmpty () ? status : statusSequence . remove ( 0 ) ;
356485 return new A2AHttpResponse () {
357486 @ Override
358487 public int status () {
0 commit comments