mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-04 10:11:46 +03:00
fix(webui): preserve causal message order (NAN-29) (#5503)
This commit is contained in:
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useNanobotStream } from "@/hooks/useNanobotStream";
|
||||
import { normalizeActivityTimeline } from "@/lib/activity-timeline";
|
||||
import type { StreamError } from "@/lib/nanobot-client";
|
||||
import type {
|
||||
ConnectionStatus,
|
||||
@@ -1813,6 +1814,84 @@ describe("useNanobotStream", () => {
|
||||
expect(result.current.messages[2].reasoning).toBe("Second reasoning.");
|
||||
});
|
||||
|
||||
it("preserves closed reasoning slices when the tool trace is unavailable", async () => {
|
||||
const fake = fakeClient();
|
||||
const { result } = renderHook(() => useNanobotStream("chat-r8", EMPTY_MESSAGES), {
|
||||
wrapper: wrap(fake.client),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fake.emit("chat-r8", {
|
||||
event: "reasoning_delta",
|
||||
chat_id: "chat-r8",
|
||||
text: "First reasoning.",
|
||||
turn_id: "turn-r8",
|
||||
turn_phase: "reasoning",
|
||||
turn_seq: 1,
|
||||
});
|
||||
fake.emit("chat-r8", {
|
||||
event: "reasoning_end",
|
||||
chat_id: "chat-r8",
|
||||
turn_id: "turn-r8",
|
||||
turn_phase: "reasoning",
|
||||
turn_seq: 2,
|
||||
});
|
||||
fake.emit("chat-r8", {
|
||||
event: "reasoning_delta",
|
||||
chat_id: "chat-r8",
|
||||
text: "Second reasoning.",
|
||||
turn_id: "turn-r8",
|
||||
turn_phase: "reasoning",
|
||||
turn_seq: 3,
|
||||
});
|
||||
fake.emit("chat-r8", {
|
||||
event: "reasoning_end",
|
||||
chat_id: "chat-r8",
|
||||
turn_id: "turn-r8",
|
||||
turn_phase: "reasoning",
|
||||
turn_seq: 4,
|
||||
});
|
||||
fake.emit("chat-r8", {
|
||||
event: "message",
|
||||
chat_id: "chat-r8",
|
||||
text: "Final answer.",
|
||||
turn_id: "turn-r8",
|
||||
turn_phase: "answer",
|
||||
turn_seq: 5,
|
||||
});
|
||||
fake.emit("chat-r8", {
|
||||
event: "turn_end",
|
||||
chat_id: "chat-r8",
|
||||
turn_id: "turn-r8",
|
||||
turn_phase: "complete",
|
||||
turn_seq: 6,
|
||||
});
|
||||
});
|
||||
|
||||
await flushStreamFrame();
|
||||
|
||||
expect(result.current.messages.map((message) => ({
|
||||
reasoning: message.reasoning,
|
||||
content: message.content,
|
||||
}))).toEqual([
|
||||
{ reasoning: "First reasoning.", content: "" },
|
||||
{ reasoning: "Second reasoning.", content: "Final answer." },
|
||||
]);
|
||||
|
||||
const units = normalizeActivityTimeline(result.current.messages);
|
||||
expect(units).toHaveLength(2);
|
||||
expect(units[0].type === "activity" ? units[0].messages.map((message) => (
|
||||
message.reasoning || message.traces?.[0]
|
||||
)) : []).toEqual([
|
||||
"First reasoning.",
|
||||
"Second reasoning.",
|
||||
]);
|
||||
expect(units[1]).toMatchObject({
|
||||
type: "message",
|
||||
message: { content: "Final answer." },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps tool-call reasoning before the matching live tool trace", () => {
|
||||
const fake = fakeClient();
|
||||
const { result } = renderHook(() => useNanobotStream("chat-tool-reasoning", EMPTY_MESSAGES), {
|
||||
@@ -1903,7 +1982,7 @@ describe("useNanobotStream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prunes reasoning-only placeholders when a turn ends without an answer", () => {
|
||||
it("preserves reasoning-only output when a turn ends without an answer", () => {
|
||||
const fake = fakeClient();
|
||||
const { result } = renderHook(() => useNanobotStream("chat-empty-thinking", EMPTY_MESSAGES), {
|
||||
wrapper: wrap(fake.client),
|
||||
@@ -1925,11 +2004,18 @@ describe("useNanobotStream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.messages).toHaveLength(0);
|
||||
expect(result.current.messages).toHaveLength(1);
|
||||
expect(result.current.messages[0]).toMatchObject({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
reasoning: "thinking without final text",
|
||||
reasoningStreaming: false,
|
||||
isStreaming: false,
|
||||
});
|
||||
expect(result.current.isStreaming).toBe(false);
|
||||
});
|
||||
|
||||
it("drops stale reasoning-only placeholders before sending the next user turn", () => {
|
||||
it("keeps earlier reasoning before sending the next user turn", () => {
|
||||
const fake = fakeClient();
|
||||
const initialMessages = [
|
||||
{
|
||||
@@ -1950,12 +2036,16 @@ describe("useNanobotStream", () => {
|
||||
result.current.send("fine");
|
||||
});
|
||||
|
||||
expect(result.current.messages).toHaveLength(1);
|
||||
expect(result.current.messages[0].role).toBe("user");
|
||||
expect(result.current.messages[0].content).toBe("fine");
|
||||
expect(result.current.messages[0].turnId).toEqual(expect.any(String));
|
||||
expect(result.current.messages[0].turnPhase).toBe("user");
|
||||
expect(result.current.messages[0].deliveryStatus).toBe("sending");
|
||||
expect(result.current.messages).toHaveLength(2);
|
||||
expect(result.current.messages[0]).toMatchObject({
|
||||
role: "assistant",
|
||||
reasoning: "leftover thinking",
|
||||
});
|
||||
expect(result.current.messages[1].role).toBe("user");
|
||||
expect(result.current.messages[1].content).toBe("fine");
|
||||
expect(result.current.messages[1].turnId).toEqual(expect.any(String));
|
||||
expect(result.current.messages[1].turnPhase).toBe("user");
|
||||
expect(result.current.messages[1].deliveryStatus).toBe("sending");
|
||||
});
|
||||
|
||||
it("returns the submitted turn identity used by the optimistic row and wire frame", () => {
|
||||
|
||||
Reference in New Issue
Block a user