|
46 | 46 | import com.google.genai.types.Part; |
47 | 47 | import io.a2a.client.Client; |
48 | 48 | import io.a2a.client.ClientEvent; |
| 49 | +import io.a2a.client.MessageEvent; |
49 | 50 | import io.a2a.client.TaskEvent; |
50 | 51 | import io.a2a.client.TaskUpdateEvent; |
51 | 52 | import io.a2a.spec.AgentCapabilities; |
@@ -889,6 +890,47 @@ private ClientEvent createFinalEvent(String text) { |
889 | 890 | return createTestEvent(new TextPart(text), TaskState.COMPLETED, false, false); |
890 | 891 | } |
891 | 892 |
|
| 893 | + private ClientEvent createTerminalStatusUpdateEvent(String message, TaskState state) { |
| 894 | + Task task = |
| 895 | + new Task.Builder() |
| 896 | + .id("task-id-1") |
| 897 | + .contextId("context-1") |
| 898 | + .status(new TaskStatus(state)) |
| 899 | + .build(); |
| 900 | + TaskStatusUpdateEvent statusUpdate = |
| 901 | + new TaskStatusUpdateEvent.Builder() |
| 902 | + .taskId("task-id-1") |
| 903 | + .contextId("context-1") |
| 904 | + .status( |
| 905 | + new TaskStatus( |
| 906 | + state, |
| 907 | + new Message.Builder() |
| 908 | + .role(Message.Role.AGENT) |
| 909 | + .parts(ImmutableList.of(new TextPart(message))) |
| 910 | + .build(), |
| 911 | + null)) |
| 912 | + .build(); |
| 913 | + return new TaskUpdateEvent(task, statusUpdate); |
| 914 | + } |
| 915 | + |
| 916 | + private ClientEvent createFailedEvent(String errorMessage) { |
| 917 | + return createTerminalStatusUpdateEvent(errorMessage, TaskState.FAILED); |
| 918 | + } |
| 919 | + |
| 920 | + private ClientEvent createCanceledEvent(String message) { |
| 921 | + return createTerminalStatusUpdateEvent(message, TaskState.CANCELED); |
| 922 | + } |
| 923 | + |
| 924 | + private ClientEvent createMessageEvent(String text) { |
| 925 | + Message message = |
| 926 | + new Message.Builder() |
| 927 | + .messageId("msg-id-1") |
| 928 | + .role(Message.Role.AGENT) |
| 929 | + .parts(new TextPart(text)) |
| 930 | + .build(); |
| 931 | + return new MessageEvent(message); |
| 932 | + } |
| 933 | + |
892 | 934 | private ClientEvent createTestEvent( |
893 | 935 | io.a2a.spec.Part<?> part, TaskState state, boolean append, boolean lastChunk) { |
894 | 936 | Artifact artifact = |
@@ -916,6 +958,100 @@ private ClientEvent createTestEvent( |
916 | 958 | return new TaskUpdateEvent(task, updateEvent); |
917 | 959 | } |
918 | 960 |
|
| 961 | + @Test |
| 962 | + public void runAsync_terminatesOnFailureTaskState() { |
| 963 | + RemoteA2AAgent agent = createAgent(); |
| 964 | + mockStreamResponse( |
| 965 | + consumer -> { |
| 966 | + consumer.accept(createPartialEvent("Processing data...", true, false), agentCard); |
| 967 | + consumer.accept(createFailedEvent("Internal Server Error"), agentCard); |
| 968 | + }); |
| 969 | + |
| 970 | + List<Event> events = agent.runAsync(invocationContext).toList().blockingGet(); |
| 971 | + |
| 972 | + // The stream must terminate cleanly and include the final event with error info. |
| 973 | + assertThat(events).hasSize(2); |
| 974 | + assertText(events.get(0), "Processing data..."); |
| 975 | + assertText(events.get(1), "Processing data..."); // aggregated/merged |
| 976 | + assertThat(events.get(1).errorMessage().orElse(null)).isEqualTo("Internal Server Error"); |
| 977 | + } |
| 978 | + |
| 979 | + @Test |
| 980 | + public void runAsync_terminatesOnCanceledTaskState() { |
| 981 | + RemoteA2AAgent agent = createAgent(); |
| 982 | + mockStreamResponse( |
| 983 | + consumer -> { |
| 984 | + consumer.accept(createPartialEvent("Processing data...", true, false), agentCard); |
| 985 | + consumer.accept(createCanceledEvent("Execution Canceled"), agentCard); |
| 986 | + }); |
| 987 | + |
| 988 | + List<Event> events = agent.runAsync(invocationContext).toList().blockingGet(); |
| 989 | + |
| 990 | + // The stream must terminate cleanly and include the final event with cancellation info. |
| 991 | + assertThat(events).hasSize(3); |
| 992 | + assertText(events.get(0), "Processing data..."); |
| 993 | + assertText(events.get(1), "Processing data..."); // aggregated |
| 994 | + assertText(events.get(2), "Execution Canceled"); // terminal canceled event |
| 995 | + } |
| 996 | + |
| 997 | + @Test |
| 998 | + public void runAsync_terminatesOnInputRequiredTaskState() { |
| 999 | + RemoteA2AAgent agent = createAgent(); |
| 1000 | + mockStreamResponse( |
| 1001 | + consumer -> { |
| 1002 | + consumer.accept(createPartialEvent("Processing data...", true, false), agentCard); |
| 1003 | + consumer.accept( |
| 1004 | + createTerminalStatusUpdateEvent("User Action Needed", TaskState.INPUT_REQUIRED), |
| 1005 | + agentCard); |
| 1006 | + }); |
| 1007 | + |
| 1008 | + List<Event> events = agent.runAsync(invocationContext).toList().blockingGet(); |
| 1009 | + |
| 1010 | + assertThat(events).hasSize(3); |
| 1011 | + assertText(events.get(0), "Processing data..."); |
| 1012 | + assertText(events.get(1), "Processing data..."); // aggregated |
| 1013 | + assertText(events.get(2), "User Action Needed"); |
| 1014 | + } |
| 1015 | + |
| 1016 | + @Test |
| 1017 | + public void runAsync_terminatesOnRejectedTaskState() { |
| 1018 | + RemoteA2AAgent agent = createAgent(); |
| 1019 | + mockStreamResponse( |
| 1020 | + consumer -> { |
| 1021 | + consumer.accept(createPartialEvent("Analyzing request...", true, false), agentCard); |
| 1022 | + consumer.accept( |
| 1023 | + createTerminalStatusUpdateEvent("Execution Rejected by Policy", TaskState.REJECTED), |
| 1024 | + agentCard); |
| 1025 | + }); |
| 1026 | + |
| 1027 | + List<Event> events = agent.runAsync(invocationContext).toList().blockingGet(); |
| 1028 | + |
| 1029 | + assertThat(events).hasSize(3); |
| 1030 | + assertText(events.get(0), "Analyzing request..."); |
| 1031 | + assertText(events.get(1), "Analyzing request..."); // aggregated |
| 1032 | + assertText(events.get(2), "Execution Rejected by Policy"); |
| 1033 | + } |
| 1034 | + |
| 1035 | + @Test |
| 1036 | + public void runAsync_doesNotTerminateOnMessageEvent() { |
| 1037 | + RemoteA2AAgent agent = createAgent(); |
| 1038 | + mockStreamResponse( |
| 1039 | + consumer -> { |
| 1040 | + consumer.accept(createPartialEvent("Processing data...", true, false), agentCard); |
| 1041 | + consumer.accept(createMessageEvent("Standard chat update message"), agentCard); |
| 1042 | + consumer.accept(createFinalEvent("Done"), agentCard); |
| 1043 | + }); |
| 1044 | + |
| 1045 | + List<Event> events = agent.runAsync(invocationContext).toList().blockingGet(); |
| 1046 | + |
| 1047 | + // The stream must not terminate early on MessageEvent, and run until createFinalEvent. |
| 1048 | + assertThat(events).hasSize(4); |
| 1049 | + assertText(events.get(0), "Processing data..."); |
| 1050 | + assertText(events.get(1), "Processing data..."); // aggregated (flushed) |
| 1051 | + assertText(events.get(2), "Standard chat update message"); // message event |
| 1052 | + assertText(events.get(3), "Done"); // terminal completed event |
| 1053 | + } |
| 1054 | + |
919 | 1055 | private RemoteA2AAgent.Builder getAgentBuilder() { |
920 | 1056 | return RemoteA2AAgent.builder().name("remote-agent").a2aClient(mockClient).agentCard(agentCard); |
921 | 1057 | } |
|
0 commit comments