From a588aafe2e3894c3e1657bc7f97fed3ff7e19b81 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sun, 8 Nov 2026 19:46:55 -0600 Subject: [PATCH] test(assistant): pin the live thread to real title, question, answer and tool steps at message.end The panel reloads the conversation at message.end. A tool-using turn must already have stored the real title, the question and answer parts and the tool-call rows then, not the transcript redactor's placeholders (changed:v1, [], {}). The redactor is always attached to a turn, so this is independent of Secured Data. Fails without the flush-before-message.end ordering (RC.24), passes on develop and RC.25. Co-Authored-By: Claude Sonnet 5.5 --- .../assistant/engine/TurnEngineTest.java | 28 ++++++++++++ .../qbits/assistant/testing/TurnRunner.java | 43 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/engine/TurnEngineTest.java b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/engine/TurnEngineTest.java index 7660df2..d2c7ee2 100644 --- a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/engine/TurnEngineTest.java +++ b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/engine/TurnEngineTest.java @@ -162,6 +162,34 @@ class TurnEngineTest extends BaseAssistantTest + /******************************************************************************* + ** The live thread after a tool-using turn: when message.end arrives the panel + ** reloads, and must find the real title, the question, the answer and the tool + ** steps - not the transcript redactor's placeholders (changed:v1, [], {}). + *******************************************************************************/ + @Test + void testLiveThreadIsNotPlaceholdersAtMessageEnd() throws Exception + { + provider.thenToolUse("count_records", input("table", "blog_post", "filter", filter("AND", "status", "EQUALS", List.of("PUBLISHED")))) + .thenText("Four posts are published."); + + TurnRunner.Outcome outcome = runner.run(EDITOR, "how many published posts?"); + assertThat(outcome.end()).containsEntry("status", "COMPLETE"); + + assertThat(runner.conversationAtEnd().getTitle()).isEqualTo("how many published posts?"); + List stored = runner.messagesAtEnd(); + assertThat(stored).extracting(AssistantMessage::getPartsJson).noneMatch(parts -> parts.equals("[]") || parts.contains("changed:v1")); + assertThat(stored.get(0).getPartsJson()).contains("how many published posts?"); + assertThat(stored.get(stored.size() - 1).getPartsJson()).contains("Four posts are published."); + assertThat(runner.toolCallsAtEnd()).isNotEmpty().allSatisfy(call -> + { + assertThat(call.getInputJson()).isNotEqualTo("{}"); + assertThat(call.getResultSummary()).doesNotContain("changed:v1"); + }); + } + + + /******************************************************************************* ** Parallel tool calls: all results in ONE user message, in order. *******************************************************************************/ diff --git a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/testing/TurnRunner.java b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/testing/TurnRunner.java index 00f17ac..fe5d3e9 100644 --- a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/testing/TurnRunner.java +++ b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/testing/TurnRunner.java @@ -27,6 +27,7 @@ import com.kingsrook.qbits.assistant.AssistantRuntime; import com.kingsrook.qbits.assistant.api.AssistantApi; import com.kingsrook.qbits.assistant.engine.RunningTurn; import com.kingsrook.qbits.assistant.engine.TurnEvent; +import com.kingsrook.qbits.assistant.model.AssistantConversation; import com.kingsrook.qbits.assistant.model.AssistantMessage; import com.kingsrook.qbits.assistant.model.AssistantToolCall; import com.kingsrook.qbits.assistant.persistence.AssistantStore; @@ -49,6 +50,8 @@ public class TurnRunner private final AssistantQBitConfig config; private List messagesAtEnd = List.of(); + private AssistantConversation conversationAtEnd; + private List toolCallsAtEnd = List.of(); @@ -175,6 +178,8 @@ public class TurnRunner if(batch.stream().anyMatch(e -> TurnEvent.MESSAGE_END.equals(e.getType()))) { messagesAtEnd = messagesOf(userId, convId); + conversationAtEnd = conversationOf(userId, convId); + toolCallsAtEnd = toolCalls(userId, running.getTurnId()); } if(!batch.isEmpty()) { @@ -213,6 +218,44 @@ public class TurnRunner + /******************************************************************************* + ** The conversation row as stored the moment message.end was seen. + *******************************************************************************/ + public AssistantConversation conversationAtEnd() + { + return (conversationAtEnd); + } + + + + /******************************************************************************* + ** The audited tool calls as stored the moment message.end was seen. + *******************************************************************************/ + public List toolCallsAtEnd() + { + return (toolCallsAtEnd); + } + + + + /******************************************************************************* + ** One conversation of the user, read as the user. + *******************************************************************************/ + private AssistantConversation conversationOf(String userId, Integer conversationId) throws Exception + { + QContext.init(qInstance, AssistantTestFixture.ownerSession(userId, config)); + try + { + return (new AssistantStore().getConversation(userId, conversationId)); + } + finally + { + QContext.clear(); + } + } + + + /******************************************************************************* ** The stored messages of a conversation (read as the user). *******************************************************************************/ -- 2.54.0