branch:
non-sse-response.test.ts
5634 bytesRaw
import { describe, it, expect } from "vitest";
import { MessageType } from "../types";
import type { UIMessage as ChatMessage } from "ai";
import { connectChatWS, isUseChatResponseMessage } from "./test-utils";

describe("Non-SSE Response Handling - PR #761", () => {
  it("should send text-start, text-delta, and text-end events for plain text responses", async () => {
    const room = crypto.randomUUID();
    const { ws } = await connectChatWS(`/agents/test-chat-agent/${room}`);

    const messages: unknown[] = [];
    let resolvePromise: (value: boolean) => void;
    const donePromise = new Promise<boolean>((res) => {
      resolvePromise = res;
    });

    const timeout = setTimeout(() => resolvePromise(false), 5000);

    ws.addEventListener("message", (e: MessageEvent) => {
      try {
        const data = JSON.parse(e.data as string);
        messages.push(data);
        if (
          data.type === MessageType.CF_AGENT_USE_CHAT_RESPONSE &&
          data.done === true
        ) {
          clearTimeout(timeout);
          resolvePromise(true);
        }
      } catch {
        messages.push(e.data);
      }
    });

    // Wait for initial connection messages
    await new Promise((r) => setTimeout(r, 50));

    const userMessage: ChatMessage = {
      id: "msg1",
      role: "user",
      parts: [{ type: "text", text: "Hello" }]
    };

    // Send a chat message - TestChatAgent returns plain text response
    ws.send(
      JSON.stringify({
        type: MessageType.CF_AGENT_USE_CHAT_REQUEST,
        id: "req1",
        init: {
          method: "POST",
          body: JSON.stringify({ messages: [userMessage] })
        }
      })
    );

    const done = await donePromise;
    expect(done).toBe(true);

    // Filter to only chat response messages
    const chatResponses = messages.filter(isUseChatResponseMessage);

    // Should have at least 4 messages:
    // 1. text-start
    // 2. text-delta (with the actual content)
    // 3. text-end
    // 4. done: true (final completion signal)
    expect(chatResponses.length).toBeGreaterThanOrEqual(4);

    // Parse the bodies to check event types
    const eventTypes = chatResponses
      .filter((m) => m.body && m.body.length > 0)
      .map((m) => {
        try {
          return JSON.parse(m.body);
        } catch {
          return null;
        }
      })
      .filter(Boolean);

    // Check for text-start event
    const textStartEvent = eventTypes.find((e) => e.type === "text-start");
    expect(textStartEvent).toBeDefined();
    expect(textStartEvent.id).toBe("req1");

    // Check for text-delta event with content
    const textDeltaEvent = eventTypes.find((e) => e.type === "text-delta");
    expect(textDeltaEvent).toBeDefined();
    expect(textDeltaEvent.id).toBe("req1");
    expect(textDeltaEvent.delta).toBe("Hello from chat agent!");

    // Check for text-end event
    const textEndEvent = eventTypes.find((e) => e.type === "text-end");
    expect(textEndEvent).toBeDefined();
    expect(textEndEvent.id).toBe("req1");

    // Verify order: text-start comes before text-delta, text-delta comes before text-end
    const startIndex = eventTypes.findIndex((e) => e.type === "text-start");
    const deltaIndex = eventTypes.findIndex((e) => e.type === "text-delta");
    const endIndex = eventTypes.findIndex((e) => e.type === "text-end");

    expect(startIndex).toBeLessThan(deltaIndex);
    expect(deltaIndex).toBeLessThan(endIndex);

    // Check final done message
    const doneMessage = chatResponses.find((m) => m.done === true);
    expect(doneMessage).toBeDefined();

    ws.close(1000);
  });

  it("should use consistent id across text-start, text-delta, and text-end events", async () => {
    const room = crypto.randomUUID();
    const { ws } = await connectChatWS(`/agents/test-chat-agent/${room}`);

    const messages: unknown[] = [];
    let resolvePromise: (value: boolean) => void;
    const donePromise = new Promise<boolean>((res) => {
      resolvePromise = res;
    });

    const timeout = setTimeout(() => resolvePromise(false), 5000);

    ws.addEventListener("message", (e: MessageEvent) => {
      try {
        const data = JSON.parse(e.data as string);
        messages.push(data);
        if (
          data.type === MessageType.CF_AGENT_USE_CHAT_RESPONSE &&
          data.done === true
        ) {
          clearTimeout(timeout);
          resolvePromise(true);
        }
      } catch {
        messages.push(e.data);
      }
    });

    await new Promise((r) => setTimeout(r, 50));

    const requestId = "test-request-id-123";
    const userMessage: ChatMessage = {
      id: "msg1",
      role: "user",
      parts: [{ type: "text", text: "Test" }]
    };

    ws.send(
      JSON.stringify({
        type: MessageType.CF_AGENT_USE_CHAT_REQUEST,
        id: requestId,
        init: {
          method: "POST",
          body: JSON.stringify({ messages: [userMessage] })
        }
      })
    );

    await donePromise;

    const chatResponses = messages.filter(isUseChatResponseMessage);
    const eventTypes = chatResponses
      .filter((m) => m.body && m.body.length > 0)
      .map((m) => {
        try {
          return JSON.parse(m.body);
        } catch {
          return null;
        }
      })
      .filter(Boolean);

    // All events should have the same request id
    const textEvents = eventTypes.filter((e) =>
      ["text-start", "text-delta", "text-end"].includes(e.type)
    );

    expect(textEvents.length).toBeGreaterThanOrEqual(3);
    for (const event of textEvents) {
      expect(event.id).toBe(requestId);
    }

    ws.close(1000);
  });
});