test(assistant): aggiunti test su draft plan minimo e cancellazione chiamata
This commit is contained in:
parent
c005a09b6f
commit
63e30ab850
|
|
@ -6,6 +6,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -163,6 +166,23 @@ public class AssistantControllerTest {
|
|||
assertEquals(0, response.flow().flow().getConnections().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void draftUsesMinimalLlmPlanWhenAssistantReturnsNoBlocks() {
|
||||
mockAssistantResponsesWithEmptyPlan();
|
||||
|
||||
AssistantFlowResponse response = assistantController.draft(
|
||||
new AssistantGenerationRequest(
|
||||
"create a flow to summarize customer emails",
|
||||
MODEL,
|
||||
1));
|
||||
|
||||
assertNotNull(response);
|
||||
assertTrue(response.valid());
|
||||
assertTrue(response.validationErrors().isEmpty());
|
||||
assertEquals(1, response.flow().flow().getBlocks().size());
|
||||
assertEquals(LLMBlockType.TYPE, response.flow().flow().getBlocks().getFirst().getType().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void draftIgnoresAssistantProvidedLlmDescriptorPlaceholder() {
|
||||
mockAssistantResponsesWithLlmDescriptorPlaceholder();
|
||||
|
|
@ -336,6 +356,46 @@ public class AssistantControllerTest {
|
|||
assertEquals(accepted.callId(), updatedSession.lastCallId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelRunningSessionCallStopsAssistantAndDoesNotUpdateFlow() throws Exception {
|
||||
CountDownLatch assistantStarted = new CountDownLatch(1);
|
||||
AtomicBoolean interrupted = new AtomicBoolean(false);
|
||||
mockBlockingAssistantResponse(assistantStarted, interrupted);
|
||||
LoginEntity user = new LoginEntity("testuser", "testpassword");
|
||||
|
||||
AssistantSessionView session = assistantController.createSession(
|
||||
new AssistantSessionCreateRequest(MODEL),
|
||||
user);
|
||||
|
||||
AssistantCallAcceptedResponse accepted = assistantController.submitMessage(
|
||||
session.id(),
|
||||
new AssistantSessionMessageRequest("Create a slow assistant flow"),
|
||||
user);
|
||||
|
||||
assertTrue(assistantStarted.await(5, TimeUnit.SECONDS));
|
||||
|
||||
AssistantCallView cancelled = assistantController.cancelCall(accepted.callId(), user);
|
||||
|
||||
assertEquals(AssistantCallStatus.CANCELLED, cancelled.status());
|
||||
assertEquals("cancelled", cancelled.phase());
|
||||
assertEquals("Assistant request cancelled", cancelled.progressMessage());
|
||||
assertNotNull(cancelled.updatedAt());
|
||||
|
||||
AssistantCallView reloaded = assistantController.getCall(accepted.callId(), user);
|
||||
assertEquals(AssistantCallStatus.CANCELLED, reloaded.status());
|
||||
assertEquals("cancelled", reloaded.phase());
|
||||
|
||||
AssistantSessionView updatedSession = assistantController.getSession(session.id(), user);
|
||||
assertEquals(accepted.callId(), updatedSession.lastCallId());
|
||||
assertEquals(null, updatedSession.currentFlow());
|
||||
assertTrue(updatedSession.messages().stream()
|
||||
.anyMatch(message -> "Assistant request cancelled.".equals(message.content())));
|
||||
|
||||
waitForInterrupt(interrupted);
|
||||
AssistantCallView idempotent = assistantController.cancelCall(accepted.callId(), user);
|
||||
assertEquals(AssistantCallStatus.CANCELLED, idempotent.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sessionRefineStoresReloadableCurrentFlowWithNewValidationNode() throws Exception {
|
||||
mockFileIndexQueryThenValidationRefineResponses();
|
||||
|
|
@ -387,6 +447,56 @@ public class AssistantControllerTest {
|
|||
assertSharedMemoryFlowIsExecutable(afterRefine.currentFlow());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void draftNormalizesIndexAndCreateSessionBlockAsSharedMemoryProducer() {
|
||||
mockIndexCreateSessionResponsesWithConsumerFlags();
|
||||
|
||||
AssistantFlowResponse response = assistantController.draft(
|
||||
new AssistantGenerationRequest(
|
||||
"download a file, index it in a shared MCP session, then query the indexed data",
|
||||
MODEL,
|
||||
1));
|
||||
|
||||
assertNotNull(response);
|
||||
assertTrue(response.valid());
|
||||
|
||||
Block<?> indexBlock = response.flow().flow().getBlocks().stream()
|
||||
.filter(block -> "Index file and create session".equals(block.getName()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
Block<?> queryBlock = response.flow().flow().getBlocks().stream()
|
||||
.filter(block -> "Query indexed data".equals(block.getName()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
MCPAgentBlockConfiguration indexConfiguration =
|
||||
(MCPAgentBlockConfiguration) indexBlock.getSpecificConfiguration();
|
||||
MCPAgentBlockConfiguration queryConfiguration =
|
||||
(MCPAgentBlockConfiguration) queryBlock.getSpecificConfiguration();
|
||||
|
||||
assertTrue(indexConfiguration.getShareSession());
|
||||
assertFalse(indexConfiguration.getUseSharedSession());
|
||||
assertEquals("sharedMemorySession", indexConfiguration.getSharedSessionName());
|
||||
assertFalse(queryConfiguration.getShareSession());
|
||||
assertTrue(queryConfiguration.getUseSharedSession());
|
||||
assertEquals("sharedMemorySession", queryConfiguration.getSharedSessionRef());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void draftDoesNotFailWhenSharedMemoryFlagsAreIncomplete() {
|
||||
mockSharedMemoryResponsesWithIncompleteFlags();
|
||||
|
||||
AssistantFlowResponse response = assistantController.draft(
|
||||
new AssistantGenerationRequest(
|
||||
"index data then query it using shared memory across workflow steps",
|
||||
MODEL,
|
||||
1));
|
||||
|
||||
assertNotNull(response);
|
||||
assertTrue(response.valid());
|
||||
assertEquals(2, response.flow().flow().getBlocks().size());
|
||||
}
|
||||
|
||||
private void assertSharedMemoryFlowIsExecutable(FlowCreateRequest flow) {
|
||||
Block<?> indexBlock = flow.flow().getBlocks().stream()
|
||||
.filter(block -> "Index file".equals(block.getName()))
|
||||
|
|
@ -510,6 +620,154 @@ public class AssistantControllerTest {
|
|||
.thenAnswer(answer);
|
||||
}
|
||||
|
||||
private void mockIndexCreateSessionResponsesWithConsumerFlags() {
|
||||
Answer<String> answer = invocation -> {
|
||||
String prompt = invocation.getArgument(1, String.class);
|
||||
if (prompt.contains("TASK: PLAN")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Planned download, indexing session, and query.",
|
||||
"plan", java.util.Map.of(
|
||||
"name", "Index and query file",
|
||||
"description", "Download a file, index it, and query indexed data.",
|
||||
"blocks", java.util.List.of(
|
||||
java.util.Map.of(
|
||||
"blockId", "b1",
|
||||
"blockType", "HTTPServerCall",
|
||||
"purpose", "DownloadFile"),
|
||||
java.util.Map.of(
|
||||
"blockId", "b2",
|
||||
"blockType", "MCPAgent",
|
||||
"purpose", "IndexFileAndCreateSession"),
|
||||
java.util.Map.of(
|
||||
"blockId", "b3",
|
||||
"blockType", "MCPAgent",
|
||||
"purpose", "QueryIndexedData")))));
|
||||
}
|
||||
if (prompt.contains("TASK: BLOCK_CONFIG")
|
||||
&& prompt.contains("Current block to configure:\n{\n \"blockId\" : \"b1\"")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Configured file download.",
|
||||
"block", java.util.Map.of(
|
||||
"blockId", "b1",
|
||||
"name", "Download file",
|
||||
"config", java.util.Map.of(
|
||||
"url", "https://example.com/file.txt",
|
||||
"method", "GET",
|
||||
"requiresAuthorization", false,
|
||||
"authorizationType", "NONE"))));
|
||||
}
|
||||
if (prompt.contains("TASK: BLOCK_CONFIG")
|
||||
&& prompt.contains("Current block to configure:\n{\n \"blockId\" : \"b2\"")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Configured indexing but incorrectly marked it as a consumer.",
|
||||
"block", java.util.Map.of(
|
||||
"blockId", "b2",
|
||||
"name", "Index file and create session",
|
||||
"config", java.util.Map.of(
|
||||
"prompt", "Index the downloaded file content: ${{file_content}}",
|
||||
"shareSession", false,
|
||||
"sharedSessionName", "sharedMemorySession",
|
||||
"useSharedSession", true,
|
||||
"sharedSessionRef", "sharedMemorySession"))));
|
||||
}
|
||||
if (prompt.contains("TASK: BLOCK_CONFIG")
|
||||
&& prompt.contains("Current block to configure:\n{\n \"blockId\" : \"b3\"")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Configured query consumer.",
|
||||
"block", java.util.Map.of(
|
||||
"blockId", "b3",
|
||||
"name", "Query indexed data",
|
||||
"config", java.util.Map.of(
|
||||
"prompt", "The shared state is ready: ${{state_ready}}. Query it: ${{query}}",
|
||||
"useSharedSession", true,
|
||||
"sharedSessionRef", "sharedMemorySession"))));
|
||||
}
|
||||
if (prompt.contains("TASK: CONNECTIONS")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Connected download, indexing, and query.",
|
||||
"connections", java.util.List.of(
|
||||
java.util.Map.of(
|
||||
"fromBlockId", "b1",
|
||||
"fromOutput", "response",
|
||||
"toBlockId", "b2",
|
||||
"toInput", "file_content"),
|
||||
java.util.Map.of(
|
||||
"fromBlockId", "b2",
|
||||
"fromOutput", "response",
|
||||
"toBlockId", "b3",
|
||||
"toInput", "state_ready"))));
|
||||
}
|
||||
throw new IllegalStateException("Unexpected assistant prompt:\n" + prompt);
|
||||
};
|
||||
|
||||
Mockito.when(internalOllamaLLMProvider.generate(Mockito.eq(MODEL), Mockito.anyString()))
|
||||
.thenAnswer(answer);
|
||||
Mockito.when(internalOllamaLLMProvider.generateJson(Mockito.eq(MODEL), Mockito.anyString()))
|
||||
.thenAnswer(answer);
|
||||
}
|
||||
|
||||
private void mockSharedMemoryResponsesWithIncompleteFlags() {
|
||||
Answer<String> answer = invocation -> {
|
||||
String prompt = invocation.getArgument(1, String.class);
|
||||
if (prompt.contains("TASK: PLAN")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Planned shared-memory steps with incomplete flags.",
|
||||
"plan", java.util.Map.of(
|
||||
"name", "Shared memory index and query",
|
||||
"description", "Index data, keep shared memory, and query it later.",
|
||||
"blocks", java.util.List.of(
|
||||
java.util.Map.of(
|
||||
"blockId", "b1",
|
||||
"blockType", "MCPAgent",
|
||||
"purpose", "PrepareSharedContext"),
|
||||
java.util.Map.of(
|
||||
"blockId", "b2",
|
||||
"blockType", "MCPAgent",
|
||||
"purpose", "UseSharedContext")))));
|
||||
}
|
||||
if (prompt.contains("TASK: BLOCK_CONFIG")
|
||||
&& prompt.contains("Current block to configure:\n{\n \"blockId\" : \"b1\"")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Configured first shared-context step with consumer-only flags.",
|
||||
"block", java.util.Map.of(
|
||||
"blockId", "b1",
|
||||
"name", "Prepare shared context",
|
||||
"config", java.util.Map.of(
|
||||
"prompt", "Prepare context from input: ${{input}}",
|
||||
"useSharedSession", true,
|
||||
"sharedSessionRef", "sharedMemorySession"))));
|
||||
}
|
||||
if (prompt.contains("TASK: BLOCK_CONFIG")
|
||||
&& prompt.contains("Current block to configure:\n{\n \"blockId\" : \"b2\"")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Configured second shared-context step with consumer-only flags.",
|
||||
"block", java.util.Map.of(
|
||||
"blockId", "b2",
|
||||
"name", "Use shared context",
|
||||
"config", java.util.Map.of(
|
||||
"prompt", "Use prepared context: ${{state_ready}}",
|
||||
"useSharedSession", true,
|
||||
"sharedSessionRef", "sharedMemorySession"))));
|
||||
}
|
||||
if (prompt.contains("TASK: CONNECTIONS")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Connected the two advisory shared-memory steps.",
|
||||
"connections", java.util.List.of(
|
||||
java.util.Map.of(
|
||||
"fromBlockId", "b1",
|
||||
"fromOutput", "response",
|
||||
"toBlockId", "b2",
|
||||
"toInput", "state_ready"))));
|
||||
}
|
||||
throw new IllegalStateException("Unexpected assistant prompt:\n" + prompt);
|
||||
};
|
||||
|
||||
Mockito.when(internalOllamaLLMProvider.generate(Mockito.eq(MODEL), Mockito.anyString()))
|
||||
.thenAnswer(answer);
|
||||
Mockito.when(internalOllamaLLMProvider.generateJson(Mockito.eq(MODEL), Mockito.anyString()))
|
||||
.thenAnswer(answer);
|
||||
}
|
||||
|
||||
private void mockTruncatedBlockConfigurationResponse() {
|
||||
Answer<String> answer = invocation -> {
|
||||
String prompt = invocation.getArgument(1, String.class);
|
||||
|
|
@ -630,6 +888,40 @@ public class AssistantControllerTest {
|
|||
.thenAnswer(answer);
|
||||
}
|
||||
|
||||
private void mockAssistantResponsesWithEmptyPlan() {
|
||||
Answer<String> answer = invocation -> {
|
||||
String prompt = invocation.getArgument(1, String.class);
|
||||
if (prompt.contains("TASK: PLAN")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "The model returned an empty plan.",
|
||||
"plan", java.util.Map.of(
|
||||
"name", "Email summarization",
|
||||
"description", "Summarize customer emails.",
|
||||
"blocks", java.util.List.of())));
|
||||
}
|
||||
if (prompt.contains("TASK: BLOCK_CONFIG")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Configured the fallback summarizer block.",
|
||||
"block", java.util.Map.of(
|
||||
"blockId", "b1",
|
||||
"name", "Email summarizer",
|
||||
"config", java.util.Map.of(
|
||||
"prompt", "Summarize the customer email: ${{email}}"))));
|
||||
}
|
||||
if (prompt.contains("TASK: CONNECTIONS")) {
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "No extra connections needed.",
|
||||
"connections", java.util.List.of()));
|
||||
}
|
||||
throw new IllegalStateException("Unexpected assistant prompt:\n" + prompt);
|
||||
};
|
||||
|
||||
Mockito.when(internalOllamaLLMProvider.generate(Mockito.eq(MODEL), Mockito.anyString()))
|
||||
.thenAnswer(answer);
|
||||
Mockito.when(internalOllamaLLMProvider.generateJson(Mockito.eq(MODEL), Mockito.anyString()))
|
||||
.thenAnswer(answer);
|
||||
}
|
||||
|
||||
private void mockAssistantResponsesWithLlmDescriptorPlaceholder() {
|
||||
Answer<String> answer = invocation -> {
|
||||
String prompt = invocation.getArgument(1, String.class);
|
||||
|
|
@ -934,11 +1226,52 @@ public class AssistantControllerTest {
|
|||
.thenAnswer(answer);
|
||||
}
|
||||
|
||||
private void mockBlockingAssistantResponse(CountDownLatch assistantStarted, AtomicBoolean interrupted) {
|
||||
Answer<String> answer = invocation -> {
|
||||
assistantStarted.countDown();
|
||||
try {
|
||||
Thread.sleep(60_000);
|
||||
} catch (InterruptedException e) {
|
||||
interrupted.set(true);
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("assistant call interrupted", e);
|
||||
}
|
||||
return TestAssistantResponses.wrap(java.util.Map.of(
|
||||
"rationale", "Should not be used after cancellation.",
|
||||
"plan", java.util.Map.of(
|
||||
"name", "Cancelled flow",
|
||||
"description", "This response should not update the session.",
|
||||
"blocks", java.util.List.of(
|
||||
java.util.Map.of(
|
||||
"blockId", "b1",
|
||||
"blockType", "LLMBlock",
|
||||
"purpose", "Cancelled block")))));
|
||||
};
|
||||
|
||||
Mockito.when(internalOllamaLLMProvider.generate(Mockito.eq(MODEL), Mockito.anyString()))
|
||||
.thenAnswer(answer);
|
||||
Mockito.when(internalOllamaLLMProvider.generateJson(Mockito.eq(MODEL), Mockito.anyString()))
|
||||
.thenAnswer(answer);
|
||||
}
|
||||
|
||||
private void waitForInterrupt(AtomicBoolean interrupted) throws Exception {
|
||||
long deadline = System.currentTimeMillis() + 5_000;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
if (interrupted.get()) {
|
||||
return;
|
||||
}
|
||||
Thread.sleep(25);
|
||||
}
|
||||
throw new AssertionError("Assistant task was not interrupted");
|
||||
}
|
||||
|
||||
private AssistantCallView waitForCallCompletion(String callId, LoginEntity user) throws Exception {
|
||||
long deadline = System.currentTimeMillis() + 15_000;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
AssistantCallView call = assistantController.getCall(callId, user);
|
||||
if (call.status() == AssistantCallStatus.COMPLETED || call.status() == AssistantCallStatus.FAILED) {
|
||||
if (call.status() == AssistantCallStatus.COMPLETED
|
||||
|| call.status() == AssistantCallStatus.FAILED
|
||||
|| call.status() == AssistantCallStatus.CANCELLED) {
|
||||
return call;
|
||||
}
|
||||
Thread.sleep(25);
|
||||
|
|
|
|||
Loading…
Reference in New Issue