summaryrefslogtreecommitdiffhomepage
path: root/packages/api
diff options
context:
space:
mode:
Diffstat (limited to 'packages/api')
-rw-r--r--packages/api/tests/agent-manager.test.ts113
1 files changed, 109 insertions, 4 deletions
diff --git a/packages/api/tests/agent-manager.test.ts b/packages/api/tests/agent-manager.test.ts
index 28fa044..ea8dc46 100644
--- a/packages/api/tests/agent-manager.test.ts
+++ b/packages/api/tests/agent-manager.test.ts
@@ -1,6 +1,11 @@
import type { AgentEvent, ToolDefinition } from "@dispatch/core";
import { describe, expect, it, vi } from "vitest";
+// Spy on appendEventToChunks so we can assert persistence calls
+const appendEventToChunksSpy = vi.fn((_chunks: unknown[], _event: unknown) => {
+ // no-op; we inspect calls in tests
+});
+
// Mock @dispatch/core's Agent to avoid real LLM calls
vi.mock("@dispatch/core", () => ({
Agent: class MockAgent {
@@ -9,13 +14,26 @@ vi.mock("@dispatch/core", () => ({
async *run(_message: string) {
yield { type: "status", status: "running" } as const;
await new Promise<void>((r) => setTimeout(r, 10));
+ // v6-style reasoning turn: delta(s) then end with providerMetadata
+ yield { type: "reasoning-delta", delta: "thinking about it" } as const;
+ yield {
+ type: "reasoning-end",
+ metadata: { anthropic: { signature: "mock-sig" } },
+ } as const;
yield { type: "text-delta", delta: "Hello " } as const;
yield { type: "text-delta", delta: "world" } as const;
yield {
type: "done",
message: {
role: "assistant",
- chunks: [{ type: "text", text: "Hello world" }],
+ chunks: [
+ {
+ type: "thinking",
+ text: "thinking about it",
+ metadata: { anthropic: { signature: "mock-sig" } },
+ },
+ { type: "text", text: "Hello world" },
+ ],
},
} as const;
yield { type: "status", status: "idle" } as const;
@@ -185,9 +203,7 @@ vi.mock("@dispatch/core", () => ({
getMessagesForTab() {
return [];
},
- appendEventToChunks(_chunks: unknown[], _event: unknown) {
- // no-op stub; chunk accumulation isn't exercised in these unit tests
- },
+ appendEventToChunks: appendEventToChunksSpy,
applySystemEvent(_messages: unknown[], _event: unknown) {
return { messageId: "mock-system-msg" };
},
@@ -311,4 +327,93 @@ describe("AgentManager", () => {
expect(listener1).toHaveBeenCalled();
expect(listener2).toHaveBeenCalled();
});
+
+ // ─── v6 reasoning-end tests ───────────────────────────────────────
+
+ it("reasoning-end event is broadcast to WS listeners", async () => {
+ const manager = new AgentManager();
+ const events: AgentEvent[] = [];
+ manager.onEvent((event) => {
+ events.push(event);
+ });
+
+ await manager.processMessage("tab-reasoning", "think please");
+
+ const reasoningEndEvents = events.filter((e) => e.type === "reasoning-end");
+ expect(reasoningEndEvents.length).toBeGreaterThan(0);
+ expect(reasoningEndEvents[0]).toMatchObject({
+ type: "reasoning-end",
+ metadata: { anthropic: { signature: "mock-sig" } },
+ });
+ });
+
+ it("reasoning-end is passed to appendEventToChunks for persistence", async () => {
+ appendEventToChunksSpy.mockClear();
+ const manager = new AgentManager();
+
+ await manager.processMessage("tab-persist", "think and persist");
+
+ // Find all calls to appendEventToChunks that received a reasoning-end event
+ const reasoningEndCalls = appendEventToChunksSpy.mock.calls.filter(
+ ([_chunks, event]) => (event as AgentEvent).type === "reasoning-end",
+ );
+ expect(reasoningEndCalls.length).toBeGreaterThan(0);
+
+ // The event should carry the metadata blob
+ const [, reasoningEndEvent] = reasoningEndCalls[0] as [unknown[], AgentEvent];
+ expect(reasoningEndEvent).toMatchObject({
+ type: "reasoning-end",
+ metadata: { anthropic: { signature: "mock-sig" } },
+ });
+ });
+
+ it("reasoning-end follows reasoning-delta in broadcast order (chunk accumulator ordering)", async () => {
+ const manager = new AgentManager();
+ const events: AgentEvent[] = [];
+ manager.onEvent((event) => {
+ events.push(event);
+ });
+
+ await manager.processMessage("tab-ordering", "think in order");
+
+ const types = events.map((e) => e.type);
+ const deltaIdx = types.indexOf("reasoning-delta");
+ const endIdx = types.indexOf("reasoning-end");
+
+ // Both must be present
+ expect(deltaIdx).toBeGreaterThanOrEqual(0);
+ expect(endIdx).toBeGreaterThanOrEqual(0);
+
+ // reasoning-end must come AFTER reasoning-delta
+ expect(endIdx).toBeGreaterThan(deltaIdx);
+
+ // reasoning-end must come BEFORE any text-delta (reasoning precedes text)
+ const textDeltaIdx = types.indexOf("text-delta");
+ if (textDeltaIdx >= 0) {
+ expect(endIdx).toBeLessThan(textDeltaIdx);
+ }
+ });
+
+ it("done event includes a thinking chunk with metadata in its message", async () => {
+ const manager = new AgentManager();
+ const events: AgentEvent[] = [];
+ manager.onEvent((event) => {
+ events.push(event);
+ });
+
+ await manager.processMessage("tab-done-chunks", "think and respond");
+
+ const doneEvent = events.find((e) => e.type === "done") as
+ | Extract<AgentEvent, { type: "done" }>
+ | undefined;
+ expect(doneEvent).toBeDefined();
+
+ const thinkingChunk = doneEvent?.message.chunks.find((c) => c.type === "thinking");
+ expect(thinkingChunk).toBeDefined();
+ expect(thinkingChunk).toMatchObject({
+ type: "thinking",
+ text: "thinking about it",
+ metadata: { anthropic: { signature: "mock-sig" } },
+ });
+ });
});