@tambo-ai/react 0.69.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/hooks/use-tambo-threads.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -5
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +4 -5
- package/dist/mcp/index.js.map +1 -1
- package/dist/model/component-metadata.d.ts +88 -241
- package/dist/model/component-metadata.d.ts.map +1 -1
- package/dist/model/component-metadata.js.map +1 -1
- package/dist/model/mcp-server-info.d.ts +3 -3
- package/dist/model/mcp-server-info.js.map +1 -1
- package/dist/providers/hooks/use-tambo-session-token.test.js.map +1 -1
- package/dist/providers/tambo-component-provider.d.ts +2 -2
- package/dist/providers/tambo-component-provider.d.ts.map +1 -1
- package/dist/providers/tambo-component-provider.js.map +1 -1
- package/dist/providers/tambo-interactable-provider.d.ts +1 -1
- package/dist/providers/tambo-registry-provider.d.ts +4 -4
- package/dist/providers/tambo-registry-provider.d.ts.map +1 -1
- package/dist/providers/tambo-registry-provider.js +11 -8
- package/dist/providers/tambo-registry-provider.js.map +1 -1
- package/dist/providers/tambo-registry-provider.test.js +31 -0
- package/dist/providers/tambo-registry-provider.test.js.map +1 -1
- package/dist/providers/tambo-registry-schema-compat.test.js +42 -52
- package/dist/providers/tambo-registry-schema-compat.test.js.map +1 -1
- package/dist/providers/tambo-stubs.d.ts +2 -2
- package/dist/providers/tambo-stubs.d.ts.map +1 -1
- package/dist/providers/tambo-stubs.js.map +1 -1
- package/dist/providers/tambo-thread-provider-initial-messages.test.js.map +1 -1
- package/dist/providers/tambo-thread-provider.d.ts.map +1 -1
- package/dist/providers/tambo-thread-provider.js +110 -142
- package/dist/providers/tambo-thread-provider.js.map +1 -1
- package/dist/providers/tambo-thread-provider.test.js +362 -445
- package/dist/providers/tambo-thread-provider.test.js.map +1 -1
- package/dist/schema/index.d.ts +1 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -5
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/schema.d.ts +7 -24
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +34 -105
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/schema.test.js +26 -124
- package/dist/schema/schema.test.js.map +1 -1
- package/dist/testing/tools.d.ts +2 -12
- package/dist/testing/tools.d.ts.map +1 -1
- package/dist/testing/tools.js +1 -20
- package/dist/testing/tools.js.map +1 -1
- package/dist/testing/types.d.ts +2 -2
- package/dist/testing/types.d.ts.map +1 -1
- package/dist/testing/types.js.map +1 -1
- package/dist/util/registry-validators.d.ts +2 -2
- package/dist/util/registry-validators.d.ts.map +1 -1
- package/dist/util/registry-validators.js +37 -17
- package/dist/util/registry-validators.js.map +1 -1
- package/dist/util/registry-validators.test.js +64 -25
- package/dist/util/registry-validators.test.js.map +1 -1
- package/dist/util/registry.d.ts +4 -10
- package/dist/util/registry.d.ts.map +1 -1
- package/dist/util/registry.js +6 -22
- package/dist/util/registry.js.map +1 -1
- package/dist/util/registry.test.js +1 -47
- package/dist/util/registry.test.js.map +1 -1
- package/dist/util/tool-caller.d.ts +2 -2
- package/dist/util/tool-caller.d.ts.map +1 -1
- package/dist/util/tool-caller.js +5 -12
- package/dist/util/tool-caller.js.map +1 -1
- package/dist/v1/index.d.ts +35 -0
- package/dist/v1/index.d.ts.map +1 -0
- package/dist/v1/index.js +47 -0
- package/dist/v1/index.js.map +1 -0
- package/dist/v1/types/component.d.ts +47 -0
- package/dist/v1/types/component.d.ts.map +1 -0
- package/dist/v1/types/component.js +11 -0
- package/dist/v1/types/component.js.map +1 -0
- package/dist/v1/types/event.d.ts +63 -0
- package/dist/v1/types/event.d.ts.map +1 -0
- package/dist/v1/types/event.js +9 -0
- package/dist/v1/types/event.js.map +1 -0
- package/dist/v1/types/message.d.ts +39 -0
- package/dist/v1/types/message.d.ts.map +1 -0
- package/dist/v1/types/message.js +10 -0
- package/dist/v1/types/message.js.map +1 -0
- package/dist/v1/types/thread.d.ts +54 -0
- package/dist/v1/types/thread.d.ts.map +1 -0
- package/dist/v1/types/thread.js +9 -0
- package/dist/v1/types/thread.js.map +1 -0
- package/dist/v1/types/tool.d.ts +52 -0
- package/dist/v1/types/tool.d.ts.map +1 -0
- package/dist/v1/types/tool.js +11 -0
- package/dist/v1/types/tool.js.map +1 -0
- package/esm/hooks/use-tambo-threads.test.js.map +1 -1
- package/esm/index.d.ts +1 -1
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js.map +1 -1
- package/esm/mcp/index.d.ts +4 -5
- package/esm/mcp/index.d.ts.map +1 -1
- package/esm/mcp/index.js +4 -5
- package/esm/mcp/index.js.map +1 -1
- package/esm/model/component-metadata.d.ts +88 -241
- package/esm/model/component-metadata.d.ts.map +1 -1
- package/esm/model/component-metadata.js.map +1 -1
- package/esm/model/mcp-server-info.d.ts +3 -3
- package/esm/model/mcp-server-info.js.map +1 -1
- package/esm/providers/hooks/use-tambo-session-token.test.js.map +1 -1
- package/esm/providers/tambo-component-provider.d.ts +2 -2
- package/esm/providers/tambo-component-provider.d.ts.map +1 -1
- package/esm/providers/tambo-component-provider.js.map +1 -1
- package/esm/providers/tambo-interactable-provider.d.ts +1 -1
- package/esm/providers/tambo-registry-provider.d.ts +4 -4
- package/esm/providers/tambo-registry-provider.d.ts.map +1 -1
- package/esm/providers/tambo-registry-provider.js +11 -8
- package/esm/providers/tambo-registry-provider.js.map +1 -1
- package/esm/providers/tambo-registry-provider.test.js +31 -0
- package/esm/providers/tambo-registry-provider.test.js.map +1 -1
- package/esm/providers/tambo-registry-schema-compat.test.js +42 -52
- package/esm/providers/tambo-registry-schema-compat.test.js.map +1 -1
- package/esm/providers/tambo-stubs.d.ts +2 -2
- package/esm/providers/tambo-stubs.d.ts.map +1 -1
- package/esm/providers/tambo-stubs.js.map +1 -1
- package/esm/providers/tambo-thread-provider-initial-messages.test.js.map +1 -1
- package/esm/providers/tambo-thread-provider.d.ts.map +1 -1
- package/esm/providers/tambo-thread-provider.js +110 -142
- package/esm/providers/tambo-thread-provider.js.map +1 -1
- package/esm/providers/tambo-thread-provider.test.js +329 -445
- package/esm/providers/tambo-thread-provider.test.js.map +1 -1
- package/esm/schema/index.d.ts +1 -2
- package/esm/schema/index.d.ts.map +1 -1
- package/esm/schema/index.js +1 -2
- package/esm/schema/index.js.map +1 -1
- package/esm/schema/schema.d.ts +7 -24
- package/esm/schema/schema.d.ts.map +1 -1
- package/esm/schema/schema.js +34 -103
- package/esm/schema/schema.js.map +1 -1
- package/esm/schema/schema.test.js +27 -125
- package/esm/schema/schema.test.js.map +1 -1
- package/esm/testing/tools.d.ts +2 -12
- package/esm/testing/tools.d.ts.map +1 -1
- package/esm/testing/tools.js +2 -20
- package/esm/testing/tools.js.map +1 -1
- package/esm/testing/types.d.ts +2 -2
- package/esm/testing/types.d.ts.map +1 -1
- package/esm/testing/types.js.map +1 -1
- package/esm/util/registry-validators.d.ts +2 -2
- package/esm/util/registry-validators.d.ts.map +1 -1
- package/esm/util/registry-validators.js +38 -18
- package/esm/util/registry-validators.js.map +1 -1
- package/esm/util/registry-validators.test.js +64 -25
- package/esm/util/registry-validators.test.js.map +1 -1
- package/esm/util/registry.d.ts +4 -10
- package/esm/util/registry.d.ts.map +1 -1
- package/esm/util/registry.js +7 -22
- package/esm/util/registry.js.map +1 -1
- package/esm/util/registry.test.js +3 -49
- package/esm/util/registry.test.js.map +1 -1
- package/esm/util/tool-caller.d.ts +2 -2
- package/esm/util/tool-caller.d.ts.map +1 -1
- package/esm/util/tool-caller.js +5 -12
- package/esm/util/tool-caller.js.map +1 -1
- package/esm/v1/index.d.ts +35 -0
- package/esm/v1/index.d.ts.map +1 -0
- package/esm/v1/index.js +46 -0
- package/esm/v1/index.js.map +1 -0
- package/esm/v1/types/component.d.ts +47 -0
- package/esm/v1/types/component.d.ts.map +1 -0
- package/esm/v1/types/component.js +10 -0
- package/esm/v1/types/component.js.map +1 -0
- package/esm/v1/types/event.d.ts +63 -0
- package/esm/v1/types/event.d.ts.map +1 -0
- package/esm/v1/types/event.js +8 -0
- package/esm/v1/types/event.js.map +1 -0
- package/esm/v1/types/message.d.ts +39 -0
- package/esm/v1/types/message.d.ts.map +1 -0
- package/esm/v1/types/message.js +9 -0
- package/esm/v1/types/message.js.map +1 -0
- package/esm/v1/types/thread.d.ts +54 -0
- package/esm/v1/types/thread.d.ts.map +1 -0
- package/esm/v1/types/thread.js +8 -0
- package/esm/v1/types/thread.js.map +1 -0
- package/esm/v1/types/tool.d.ts +52 -0
- package/esm/v1/types/tool.d.ts.map +1 -0
- package/esm/v1/types/tool.js +10 -0
- package/esm/v1/types/tool.js.map +1 -0
- package/package.json +18 -8
- package/dist/schema/zod.d.ts +0 -57
- package/dist/schema/zod.d.ts.map +0 -1
- package/dist/schema/zod.js +0 -191
- package/dist/schema/zod.js.map +0 -1
- package/dist/schema/zod.test.d.ts +0 -2
- package/dist/schema/zod.test.d.ts.map +0 -1
- package/dist/schema/zod.test.js +0 -663
- package/dist/schema/zod.test.js.map +0 -1
- package/esm/schema/zod.d.ts +0 -57
- package/esm/schema/zod.d.ts.map +0 -1
- package/esm/schema/zod.js +0 -180
- package/esm/schema/zod.js.map +0 -1
- package/esm/schema/zod.test.d.ts +0 -2
- package/esm/schema/zod.test.d.ts.map +0 -1
- package/esm/schema/zod.test.js +0 -628
- package/esm/schema/zod.test.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { advanceStream } from "@tambo-ai/typescript-sdk";
|
|
1
|
+
import TamboAI, { advanceStream } from "@tambo-ai/typescript-sdk";
|
|
2
2
|
import { act, renderHook } from "@testing-library/react";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { z } from "zod/v4";
|
|
@@ -23,9 +23,14 @@ jest.mock("./tambo-client-provider", () => {
|
|
|
23
23
|
TamboClientContext: React.createContext(undefined),
|
|
24
24
|
};
|
|
25
25
|
});
|
|
26
|
-
jest.mock("@tambo-ai/typescript-sdk", () =>
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
jest.mock("@tambo-ai/typescript-sdk", () => {
|
|
27
|
+
const actual = jest.requireActual("@tambo-ai/typescript-sdk");
|
|
28
|
+
return {
|
|
29
|
+
__esModule: true,
|
|
30
|
+
...actual,
|
|
31
|
+
advanceStream: jest.fn(),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
29
34
|
// Mock the getCustomContext
|
|
30
35
|
jest.mock("../util/registry", () => ({
|
|
31
36
|
...jest.requireActual("../util/registry"),
|
|
@@ -68,26 +73,9 @@ const createMockAdvanceResponse = (overrides = {}) => ({
|
|
|
68
73
|
});
|
|
69
74
|
describe("TamboThreadProvider", () => {
|
|
70
75
|
const mockThread = createMockThread();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
retrieve: jest.fn(),
|
|
76
|
-
advance: jest.fn(),
|
|
77
|
-
advanceByID: jest.fn(),
|
|
78
|
-
generateName: jest.fn(),
|
|
79
|
-
};
|
|
80
|
-
const mockProjectsApi = {
|
|
81
|
-
getCurrent: jest.fn(),
|
|
82
|
-
};
|
|
83
|
-
const mockBeta = {
|
|
84
|
-
threads: mockThreadsApi,
|
|
85
|
-
projects: mockProjectsApi,
|
|
86
|
-
};
|
|
87
|
-
const mockTamboAI = {
|
|
88
|
-
apiKey: "",
|
|
89
|
-
beta: mockBeta,
|
|
90
|
-
};
|
|
76
|
+
let mockTamboAI;
|
|
77
|
+
let mockThreadsApi;
|
|
78
|
+
let mockProjectsApi;
|
|
91
79
|
let mockQueryClient;
|
|
92
80
|
const mockRegistry = [
|
|
93
81
|
{
|
|
@@ -139,8 +127,19 @@ describe("TamboThreadProvider", () => {
|
|
|
139
127
|
};
|
|
140
128
|
// Default wrapper for most tests
|
|
141
129
|
const Wrapper = createWrapper();
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
jest.restoreAllMocks();
|
|
132
|
+
});
|
|
142
133
|
beforeEach(() => {
|
|
143
134
|
jest.clearAllMocks();
|
|
135
|
+
mockTamboAI = new TamboAI({
|
|
136
|
+
apiKey: "",
|
|
137
|
+
fetch: () => {
|
|
138
|
+
throw new Error("Unexpected network call in test");
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
mockThreadsApi = mockTamboAI.beta.threads;
|
|
142
|
+
mockProjectsApi = mockTamboAI.beta.projects;
|
|
144
143
|
// Setup mock query client
|
|
145
144
|
mockQueryClient = {
|
|
146
145
|
invalidateQueries: jest.fn().mockResolvedValue(undefined),
|
|
@@ -149,21 +148,22 @@ describe("TamboThreadProvider", () => {
|
|
|
149
148
|
jest
|
|
150
149
|
.mocked(useTamboQueryClient)
|
|
151
150
|
.mockReturnValue(mockQueryClient);
|
|
152
|
-
jest.
|
|
151
|
+
jest.spyOn(mockThreadsApi, "retrieve").mockResolvedValue(mockThread);
|
|
153
152
|
jest
|
|
154
|
-
.
|
|
153
|
+
.spyOn(mockThreadsApi.messages, "create")
|
|
155
154
|
.mockResolvedValue(createMockMessage());
|
|
156
155
|
jest
|
|
157
|
-
.
|
|
156
|
+
.spyOn(mockThreadsApi, "advance")
|
|
158
157
|
.mockResolvedValue(createMockAdvanceResponse());
|
|
159
158
|
jest
|
|
160
|
-
.
|
|
159
|
+
.spyOn(mockThreadsApi, "advanceByID")
|
|
161
160
|
.mockResolvedValue(createMockAdvanceResponse());
|
|
162
|
-
jest.
|
|
161
|
+
jest.spyOn(mockThreadsApi, "generateName").mockResolvedValue({
|
|
163
162
|
...mockThread,
|
|
164
163
|
name: "Generated Thread Name",
|
|
165
164
|
});
|
|
166
|
-
jest.
|
|
165
|
+
jest.spyOn(mockThreadsApi, "update").mockResolvedValue({});
|
|
166
|
+
jest.spyOn(mockProjectsApi, "getCurrent").mockResolvedValue({
|
|
167
167
|
id: "test-project-id",
|
|
168
168
|
name: "Test Project",
|
|
169
169
|
isTokenRequired: false,
|
|
@@ -231,7 +231,7 @@ describe("TamboThreadProvider", () => {
|
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
it("should send a message and update thread state", async () => {
|
|
234
|
-
const
|
|
234
|
+
const mockStreamResponse = {
|
|
235
235
|
responseMessageDto: {
|
|
236
236
|
id: "response-1",
|
|
237
237
|
content: [{ type: "text", text: "Response" }],
|
|
@@ -244,14 +244,17 @@ describe("TamboThreadProvider", () => {
|
|
|
244
244
|
generationStage: GenerationStage.COMPLETE,
|
|
245
245
|
mcpAccessToken: "test-mcp-access-token",
|
|
246
246
|
};
|
|
247
|
-
|
|
248
|
-
.
|
|
249
|
-
|
|
247
|
+
const mockAsyncIterator = {
|
|
248
|
+
[Symbol.asyncIterator]: async function* () {
|
|
249
|
+
yield mockStreamResponse;
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
|
|
250
253
|
const { result } = renderHook(() => useTamboThread(), { wrapper: Wrapper });
|
|
251
254
|
await act(async () => {
|
|
252
255
|
await result.current.sendThreadMessage("Hello", {
|
|
253
256
|
threadId: "test-thread-1",
|
|
254
|
-
streamResponse:
|
|
257
|
+
streamResponse: true,
|
|
255
258
|
additionalContext: {
|
|
256
259
|
custom: {
|
|
257
260
|
message: "additional instructions",
|
|
@@ -259,7 +262,7 @@ describe("TamboThreadProvider", () => {
|
|
|
259
262
|
},
|
|
260
263
|
});
|
|
261
264
|
});
|
|
262
|
-
expect(
|
|
265
|
+
expect(advanceStream).toHaveBeenCalledWith(expect.anything(), {
|
|
263
266
|
messageToAppend: {
|
|
264
267
|
content: [{ type: "text", text: "Hello" }],
|
|
265
268
|
role: "user",
|
|
@@ -273,7 +276,7 @@ describe("TamboThreadProvider", () => {
|
|
|
273
276
|
contextKey: undefined,
|
|
274
277
|
clientTools: [],
|
|
275
278
|
toolCallCounts: {},
|
|
276
|
-
});
|
|
279
|
+
}, "test-thread-1");
|
|
277
280
|
expect(result.current.generationStage).toBe(GenerationStage.COMPLETE);
|
|
278
281
|
});
|
|
279
282
|
it("should handle streaming responses", async () => {
|
|
@@ -308,7 +311,7 @@ describe("TamboThreadProvider", () => {
|
|
|
308
311
|
expect(result.current.generationStage).toBe(GenerationStage.COMPLETE);
|
|
309
312
|
});
|
|
310
313
|
it("should handle tool calls during message processing.", async () => {
|
|
311
|
-
const
|
|
314
|
+
const mockToolCallChunk = {
|
|
312
315
|
responseMessageDto: {
|
|
313
316
|
id: "tool-call-1",
|
|
314
317
|
content: [{ type: "text", text: "Tool response" }],
|
|
@@ -324,10 +327,7 @@ describe("TamboThreadProvider", () => {
|
|
|
324
327
|
generationStage: GenerationStage.COMPLETE,
|
|
325
328
|
mcpAccessToken: "test-mcp-access-token",
|
|
326
329
|
};
|
|
327
|
-
|
|
328
|
-
.mocked(mockThreadsApi.advanceByID)
|
|
329
|
-
.mockResolvedValueOnce(mockToolCallResponse)
|
|
330
|
-
.mockResolvedValueOnce({
|
|
330
|
+
const mockFinalChunk = {
|
|
331
331
|
responseMessageDto: {
|
|
332
332
|
id: "advance-response2",
|
|
333
333
|
content: [{ type: "text", text: "response 2" }],
|
|
@@ -338,12 +338,26 @@ describe("TamboThreadProvider", () => {
|
|
|
338
338
|
},
|
|
339
339
|
generationStage: GenerationStage.COMPLETE,
|
|
340
340
|
mcpAccessToken: "test-mcp-access-token",
|
|
341
|
-
}
|
|
341
|
+
};
|
|
342
|
+
const mockAsyncIterator = {
|
|
343
|
+
[Symbol.asyncIterator]: async function* () {
|
|
344
|
+
yield mockToolCallChunk;
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
const mockAsyncIterator2 = {
|
|
348
|
+
[Symbol.asyncIterator]: async function* () {
|
|
349
|
+
yield mockFinalChunk;
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
jest
|
|
353
|
+
.mocked(advanceStream)
|
|
354
|
+
.mockResolvedValueOnce(mockAsyncIterator)
|
|
355
|
+
.mockResolvedValueOnce(mockAsyncIterator2);
|
|
342
356
|
const { result } = renderHook(() => useTamboThread(), { wrapper: Wrapper });
|
|
343
357
|
await act(async () => {
|
|
344
358
|
await result.current.sendThreadMessage("Use tool", {
|
|
345
359
|
threadId: "test-thread-1",
|
|
346
|
-
streamResponse:
|
|
360
|
+
streamResponse: true,
|
|
347
361
|
});
|
|
348
362
|
});
|
|
349
363
|
expect(result.current.generationStage).toBe(GenerationStage.COMPLETE);
|
|
@@ -356,7 +370,7 @@ describe("TamboThreadProvider", () => {
|
|
|
356
370
|
const mockOnCallUnregisteredTool = jest
|
|
357
371
|
.fn()
|
|
358
372
|
.mockResolvedValue("unregistered-tool-result");
|
|
359
|
-
const
|
|
373
|
+
const mockUnregisteredToolCallChunk = {
|
|
360
374
|
responseMessageDto: {
|
|
361
375
|
id: "unregistered-tool-call-1",
|
|
362
376
|
content: [{ type: "text", text: "Unregistered tool response" }],
|
|
@@ -374,10 +388,7 @@ describe("TamboThreadProvider", () => {
|
|
|
374
388
|
generationStage: GenerationStage.COMPLETE,
|
|
375
389
|
mcpAccessToken: "test-mcp-access-token",
|
|
376
390
|
};
|
|
377
|
-
|
|
378
|
-
.mocked(mockThreadsApi.advanceByID)
|
|
379
|
-
.mockResolvedValueOnce(mockUnregisteredToolCallResponse)
|
|
380
|
-
.mockResolvedValueOnce({
|
|
391
|
+
const mockFinalChunk = {
|
|
381
392
|
responseMessageDto: {
|
|
382
393
|
id: "advance-response2",
|
|
383
394
|
content: [{ type: "text", text: "response 2" }],
|
|
@@ -388,7 +399,21 @@ describe("TamboThreadProvider", () => {
|
|
|
388
399
|
},
|
|
389
400
|
generationStage: GenerationStage.COMPLETE,
|
|
390
401
|
mcpAccessToken: "test-mcp-access-token",
|
|
391
|
-
}
|
|
402
|
+
};
|
|
403
|
+
const mockAsyncIterator = {
|
|
404
|
+
[Symbol.asyncIterator]: async function* () {
|
|
405
|
+
yield mockUnregisteredToolCallChunk;
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
const mockAsyncIterator2 = {
|
|
409
|
+
[Symbol.asyncIterator]: async function* () {
|
|
410
|
+
yield mockFinalChunk;
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
jest
|
|
414
|
+
.mocked(advanceStream)
|
|
415
|
+
.mockResolvedValueOnce(mockAsyncIterator)
|
|
416
|
+
.mockResolvedValueOnce(mockAsyncIterator2);
|
|
392
417
|
const { result } = renderHook(() => useTamboThread(), {
|
|
393
418
|
wrapper: createWrapper({
|
|
394
419
|
onCallUnregisteredTool: mockOnCallUnregisteredTool,
|
|
@@ -397,14 +422,14 @@ describe("TamboThreadProvider", () => {
|
|
|
397
422
|
await act(async () => {
|
|
398
423
|
await result.current.sendThreadMessage("Use unregistered tool", {
|
|
399
424
|
threadId: "test-thread-1",
|
|
400
|
-
streamResponse:
|
|
425
|
+
streamResponse: true,
|
|
401
426
|
});
|
|
402
427
|
});
|
|
403
428
|
expect(result.current.generationStage).toBe(GenerationStage.COMPLETE);
|
|
404
429
|
expect(mockOnCallUnregisteredTool).toHaveBeenCalledWith("unregistered-tool", [{ parameterName: "input", parameterValue: "test-input" }]);
|
|
405
430
|
});
|
|
406
431
|
it("should handle unregistered tool calls without onCallUnregisteredTool", async () => {
|
|
407
|
-
const
|
|
432
|
+
const mockUnregisteredToolCallChunk = {
|
|
408
433
|
responseMessageDto: {
|
|
409
434
|
id: "unregistered-tool-call-1",
|
|
410
435
|
content: [{ type: "text", text: "Unregistered tool response" }],
|
|
@@ -422,10 +447,7 @@ describe("TamboThreadProvider", () => {
|
|
|
422
447
|
generationStage: GenerationStage.COMPLETE,
|
|
423
448
|
mcpAccessToken: "test-mcp-access-token",
|
|
424
449
|
};
|
|
425
|
-
|
|
426
|
-
.mocked(mockThreadsApi.advanceByID)
|
|
427
|
-
.mockResolvedValueOnce(mockUnregisteredToolCallResponse)
|
|
428
|
-
.mockResolvedValueOnce({
|
|
450
|
+
const mockFinalChunk = {
|
|
429
451
|
responseMessageDto: {
|
|
430
452
|
id: "advance-response2",
|
|
431
453
|
content: [{ type: "text", text: "response 2" }],
|
|
@@ -436,12 +458,26 @@ describe("TamboThreadProvider", () => {
|
|
|
436
458
|
},
|
|
437
459
|
generationStage: GenerationStage.COMPLETE,
|
|
438
460
|
mcpAccessToken: "test-mcp-access-token",
|
|
439
|
-
}
|
|
461
|
+
};
|
|
462
|
+
const mockAsyncIterator = {
|
|
463
|
+
[Symbol.asyncIterator]: async function* () {
|
|
464
|
+
yield mockUnregisteredToolCallChunk;
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
const mockAsyncIterator2 = {
|
|
468
|
+
[Symbol.asyncIterator]: async function* () {
|
|
469
|
+
yield mockFinalChunk;
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
jest
|
|
473
|
+
.mocked(advanceStream)
|
|
474
|
+
.mockResolvedValueOnce(mockAsyncIterator)
|
|
475
|
+
.mockResolvedValueOnce(mockAsyncIterator2);
|
|
440
476
|
const { result } = renderHook(() => useTamboThread(), { wrapper: Wrapper });
|
|
441
477
|
await act(async () => {
|
|
442
478
|
await result.current.sendThreadMessage("Use unregistered tool", {
|
|
443
479
|
threadId: "test-thread-1",
|
|
444
|
-
streamResponse:
|
|
480
|
+
streamResponse: true,
|
|
445
481
|
});
|
|
446
482
|
});
|
|
447
483
|
expect(result.current.generationStage).toBe(GenerationStage.COMPLETE);
|
|
@@ -502,77 +538,16 @@ describe("TamboThreadProvider", () => {
|
|
|
502
538
|
expect(mockThreadsApi.advance).not.toHaveBeenCalled();
|
|
503
539
|
expect(mockThreadsApi.advanceByID).not.toHaveBeenCalled();
|
|
504
540
|
});
|
|
505
|
-
it("should
|
|
506
|
-
// Use wrapper with streaming=true to show that explicit streamResponse=false overrides provider setting
|
|
541
|
+
it("should throw error when streamResponse=false (non-streaming not supported)", async () => {
|
|
507
542
|
const { result } = renderHook(() => useTamboThread(), {
|
|
508
543
|
wrapper: createWrapper({ streaming: true }),
|
|
509
544
|
});
|
|
510
545
|
await act(async () => {
|
|
511
|
-
await result.current.sendThreadMessage("Hello non-streaming", {
|
|
546
|
+
await expect(result.current.sendThreadMessage("Hello non-streaming", {
|
|
512
547
|
threadId: "test-thread-1",
|
|
513
548
|
streamResponse: false,
|
|
514
|
-
|
|
515
|
-
custom: {
|
|
516
|
-
message: "additional instructions",
|
|
517
|
-
},
|
|
518
|
-
},
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
expect(mockThreadsApi.advanceByID).toHaveBeenCalledWith("test-thread-1", {
|
|
522
|
-
messageToAppend: {
|
|
523
|
-
content: [{ type: "text", text: "Hello non-streaming" }],
|
|
524
|
-
role: "user",
|
|
525
|
-
additionalContext: {
|
|
526
|
-
custom: {
|
|
527
|
-
message: "additional instructions",
|
|
528
|
-
},
|
|
529
|
-
},
|
|
530
|
-
},
|
|
531
|
-
availableComponents: serializeRegistry(mockRegistry),
|
|
532
|
-
contextKey: undefined,
|
|
533
|
-
clientTools: [],
|
|
534
|
-
forceToolChoice: undefined,
|
|
535
|
-
toolCallCounts: {},
|
|
536
|
-
});
|
|
537
|
-
// Should not call advance or advanceStream
|
|
538
|
-
expect(mockThreadsApi.advance).not.toHaveBeenCalled();
|
|
539
|
-
expect(advanceStream).not.toHaveBeenCalled();
|
|
540
|
-
});
|
|
541
|
-
it("should call advanceById when streamResponse is undefined and provider streaming=false", async () => {
|
|
542
|
-
// Use wrapper with streaming=false to test that undefined streamResponse respects provider setting
|
|
543
|
-
const { result } = renderHook(() => useTamboThread(), {
|
|
544
|
-
wrapper: createWrapper({ streaming: false }),
|
|
545
|
-
});
|
|
546
|
-
await act(async () => {
|
|
547
|
-
await result.current.sendThreadMessage("Hello default", {
|
|
548
|
-
threadId: "test-thread-1",
|
|
549
|
-
// streamResponse is undefined, should use provider's streaming=false
|
|
550
|
-
additionalContext: {
|
|
551
|
-
custom: {
|
|
552
|
-
message: "additional instructions",
|
|
553
|
-
},
|
|
554
|
-
},
|
|
555
|
-
});
|
|
556
|
-
});
|
|
557
|
-
expect(mockThreadsApi.advanceByID).toHaveBeenCalledWith("test-thread-1", {
|
|
558
|
-
messageToAppend: {
|
|
559
|
-
content: [{ type: "text", text: "Hello default" }],
|
|
560
|
-
role: "user",
|
|
561
|
-
additionalContext: {
|
|
562
|
-
custom: {
|
|
563
|
-
message: "additional instructions",
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
availableComponents: serializeRegistry(mockRegistry),
|
|
568
|
-
contextKey: undefined,
|
|
569
|
-
clientTools: [],
|
|
570
|
-
forceToolChoice: undefined,
|
|
571
|
-
toolCallCounts: {},
|
|
549
|
+
})).rejects.toThrow();
|
|
572
550
|
});
|
|
573
|
-
// Should not call advance or advanceStream
|
|
574
|
-
expect(mockThreadsApi.advance).not.toHaveBeenCalled();
|
|
575
|
-
expect(advanceStream).not.toHaveBeenCalled();
|
|
576
551
|
});
|
|
577
552
|
it("should call advanceStream when streamResponse is undefined and provider streaming=true (default)", async () => {
|
|
578
553
|
const mockStreamResponse = {
|
|
@@ -628,44 +603,6 @@ describe("TamboThreadProvider", () => {
|
|
|
628
603
|
expect(mockThreadsApi.advance).not.toHaveBeenCalled();
|
|
629
604
|
expect(mockThreadsApi.advanceByID).not.toHaveBeenCalled();
|
|
630
605
|
});
|
|
631
|
-
it("should call advance when streamResponse=false for placeholder thread", async () => {
|
|
632
|
-
// Use wrapper with streaming=true to show that explicit streamResponse=false overrides provider setting
|
|
633
|
-
const { result } = renderHook(() => useTamboThread(), {
|
|
634
|
-
wrapper: createWrapper({ streaming: true }),
|
|
635
|
-
});
|
|
636
|
-
// Start with placeholder thread (which is the default state)
|
|
637
|
-
expect(result.current.thread.id).toBe("placeholder");
|
|
638
|
-
await act(async () => {
|
|
639
|
-
await result.current.sendThreadMessage("Hello new thread", {
|
|
640
|
-
threadId: "placeholder",
|
|
641
|
-
streamResponse: false,
|
|
642
|
-
additionalContext: {
|
|
643
|
-
custom: {
|
|
644
|
-
message: "additional instructions",
|
|
645
|
-
},
|
|
646
|
-
},
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
expect(mockThreadsApi.advance).toHaveBeenCalledWith({
|
|
650
|
-
messageToAppend: {
|
|
651
|
-
content: [{ type: "text", text: "Hello new thread" }],
|
|
652
|
-
role: "user",
|
|
653
|
-
additionalContext: {
|
|
654
|
-
custom: {
|
|
655
|
-
message: "additional instructions",
|
|
656
|
-
},
|
|
657
|
-
},
|
|
658
|
-
},
|
|
659
|
-
availableComponents: serializeRegistry(mockRegistry),
|
|
660
|
-
contextKey: undefined,
|
|
661
|
-
clientTools: [],
|
|
662
|
-
forceToolChoice: undefined,
|
|
663
|
-
toolCallCounts: {},
|
|
664
|
-
});
|
|
665
|
-
// Should not call advanceById or advanceStream
|
|
666
|
-
expect(mockThreadsApi.advanceByID).not.toHaveBeenCalled();
|
|
667
|
-
expect(advanceStream).not.toHaveBeenCalled();
|
|
668
|
-
});
|
|
669
606
|
it("should call advanceStream when streamResponse=true for placeholder thread", async () => {
|
|
670
607
|
const mockStreamResponse = {
|
|
671
608
|
responseMessageDto: {
|
|
@@ -722,26 +659,96 @@ describe("TamboThreadProvider", () => {
|
|
|
722
659
|
expect(mockThreadsApi.advance).not.toHaveBeenCalled();
|
|
723
660
|
expect(mockThreadsApi.advanceByID).not.toHaveBeenCalled();
|
|
724
661
|
});
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
//
|
|
730
|
-
|
|
662
|
+
it("should handle multiple sequential messages during streaming (server tool scenario)", async () => {
|
|
663
|
+
// This test verifies the fix for the bug where the second message doesn't render
|
|
664
|
+
// during server tool response streaming. The scenario:
|
|
665
|
+
// 1. First message: "I will call the tool..." with statusMessage
|
|
666
|
+
// 2. Second message: The tool result response streaming in
|
|
667
|
+
// First message - tool announcement (server tools don't have componentName set during streaming)
|
|
668
|
+
const mockFirstMessage = {
|
|
669
|
+
responseMessageDto: {
|
|
670
|
+
id: "msg-first",
|
|
671
|
+
content: [{ type: "text", text: "I will search the docs..." }],
|
|
672
|
+
role: "assistant",
|
|
673
|
+
threadId: "test-thread-1",
|
|
674
|
+
component: {
|
|
675
|
+
componentName: "",
|
|
676
|
+
componentState: {},
|
|
677
|
+
message: "",
|
|
678
|
+
props: {},
|
|
679
|
+
statusMessage: "searching the Tambo docs...",
|
|
680
|
+
},
|
|
681
|
+
componentState: {},
|
|
682
|
+
createdAt: new Date().toISOString(),
|
|
683
|
+
},
|
|
684
|
+
generationStage: GenerationStage.STREAMING_RESPONSE,
|
|
685
|
+
mcpAccessToken: "test-mcp-access-token",
|
|
686
|
+
};
|
|
687
|
+
// Second message - tool result (different ID!)
|
|
688
|
+
const mockSecondMessageChunk1 = {
|
|
689
|
+
responseMessageDto: {
|
|
690
|
+
id: "msg-second",
|
|
691
|
+
content: [{ type: "text", text: "Here's what I found..." }],
|
|
692
|
+
role: "assistant",
|
|
693
|
+
threadId: "test-thread-1",
|
|
694
|
+
componentState: {},
|
|
695
|
+
createdAt: new Date().toISOString(),
|
|
696
|
+
},
|
|
697
|
+
generationStage: GenerationStage.STREAMING_RESPONSE,
|
|
698
|
+
mcpAccessToken: "test-mcp-access-token",
|
|
699
|
+
};
|
|
700
|
+
const mockSecondMessageChunk2 = {
|
|
701
|
+
responseMessageDto: {
|
|
702
|
+
id: "msg-second",
|
|
703
|
+
content: [
|
|
704
|
+
{
|
|
705
|
+
type: "text",
|
|
706
|
+
text: "Here's what I found in the documentation about that topic.",
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
role: "assistant",
|
|
710
|
+
threadId: "test-thread-1",
|
|
711
|
+
componentState: {},
|
|
712
|
+
createdAt: new Date().toISOString(),
|
|
713
|
+
},
|
|
714
|
+
generationStage: GenerationStage.COMPLETE,
|
|
715
|
+
mcpAccessToken: "test-mcp-access-token",
|
|
716
|
+
};
|
|
717
|
+
const mockAsyncIterator = {
|
|
718
|
+
[Symbol.asyncIterator]: async function* () {
|
|
719
|
+
yield mockFirstMessage;
|
|
720
|
+
yield mockSecondMessageChunk1;
|
|
721
|
+
yield mockSecondMessageChunk2;
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
|
|
731
725
|
const { result } = renderHook(() => useTamboThread(), {
|
|
732
|
-
wrapper:
|
|
726
|
+
wrapper: createWrapper({ streaming: true }),
|
|
733
727
|
});
|
|
734
|
-
// Expect the error to be thrown
|
|
735
728
|
await act(async () => {
|
|
736
|
-
await result.current.
|
|
737
|
-
await expect(result.current.sendThreadMessage("Hello", {
|
|
729
|
+
await result.current.sendThreadMessage("Search the docs", {
|
|
738
730
|
threadId: "test-thread-1",
|
|
739
|
-
streamResponse:
|
|
740
|
-
})
|
|
731
|
+
streamResponse: true,
|
|
732
|
+
});
|
|
741
733
|
});
|
|
742
|
-
//
|
|
743
|
-
expect(result.current.
|
|
734
|
+
// Thread should have 3 messages: user message + 2 assistant messages
|
|
735
|
+
expect(result.current.thread.messages).toHaveLength(3);
|
|
736
|
+
// Filter to assistant messages only
|
|
737
|
+
const assistantMessages = result.current.thread.messages.filter((m) => m.role === "assistant");
|
|
738
|
+
expect(assistantMessages).toHaveLength(2);
|
|
739
|
+
// First assistant message should have the tool status
|
|
740
|
+
const firstMsg = result.current.thread.messages.find((m) => m.id === "msg-first");
|
|
741
|
+
expect(firstMsg).toBeDefined();
|
|
742
|
+
expect(firstMsg?.content[0]?.text).toContain("search the docs");
|
|
743
|
+
// Second assistant message should have the final content
|
|
744
|
+
const secondMsg = result.current.thread.messages.find((m) => m.id === "msg-second");
|
|
745
|
+
expect(secondMsg).toBeDefined();
|
|
746
|
+
expect(secondMsg?.content[0]?.text).toContain("what I found in the documentation");
|
|
747
|
+
// Generation should be complete
|
|
748
|
+
expect(result.current.generationStage).toBe(GenerationStage.COMPLETE);
|
|
744
749
|
});
|
|
750
|
+
});
|
|
751
|
+
describe("error handling", () => {
|
|
745
752
|
it("should set generation stage to ERROR when streaming sendThreadMessage fails", async () => {
|
|
746
753
|
const testError = new Error("Streaming API call failed");
|
|
747
754
|
// Mock advanceStream to throw an error
|
|
@@ -760,24 +767,85 @@ describe("TamboThreadProvider", () => {
|
|
|
760
767
|
// Verify generation stage is set to ERROR
|
|
761
768
|
expect(result.current.generationStage).toBe(GenerationStage.ERROR);
|
|
762
769
|
});
|
|
763
|
-
it("should
|
|
764
|
-
const testError = new Error("
|
|
765
|
-
|
|
766
|
-
jest.mocked(mockThreadsApi.advance).mockRejectedValue(testError);
|
|
770
|
+
it("should rollback optimistic user message when sendThreadMessage fails", async () => {
|
|
771
|
+
const testError = new Error("API call failed");
|
|
772
|
+
jest.mocked(advanceStream).mockRejectedValue(testError);
|
|
767
773
|
const { result } = renderHook(() => useTamboThread(), {
|
|
768
774
|
wrapper: Wrapper,
|
|
769
775
|
});
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
776
|
+
await act(async () => {
|
|
777
|
+
await result.current.switchCurrentThread("test-thread-1");
|
|
778
|
+
});
|
|
779
|
+
const initialMessageCount = result.current.thread.messages.length;
|
|
773
780
|
await act(async () => {
|
|
774
781
|
await expect(result.current.sendThreadMessage("Hello", {
|
|
775
|
-
threadId: "
|
|
776
|
-
streamResponse:
|
|
777
|
-
})).rejects.toThrow("
|
|
782
|
+
threadId: "test-thread-1",
|
|
783
|
+
streamResponse: true,
|
|
784
|
+
})).rejects.toThrow("API call failed");
|
|
778
785
|
});
|
|
779
|
-
// Verify
|
|
780
|
-
expect(result.current.
|
|
786
|
+
// Verify user message was rolled back
|
|
787
|
+
expect(result.current.thread.messages.length).toBe(initialMessageCount);
|
|
788
|
+
});
|
|
789
|
+
it("should rollback optimistic message when addThreadMessage fails", async () => {
|
|
790
|
+
const testError = new Error("Create message failed");
|
|
791
|
+
jest.mocked(mockThreadsApi.messages.create).mockRejectedValue(testError);
|
|
792
|
+
const { result } = renderHook(() => useTamboThread(), {
|
|
793
|
+
wrapper: Wrapper,
|
|
794
|
+
});
|
|
795
|
+
await act(async () => {
|
|
796
|
+
await result.current.switchCurrentThread("test-thread-1");
|
|
797
|
+
});
|
|
798
|
+
const initialMessageCount = result.current.thread.messages.length;
|
|
799
|
+
const newMessage = createMockMessage({ threadId: "test-thread-1" });
|
|
800
|
+
await act(async () => {
|
|
801
|
+
await expect(result.current.addThreadMessage(newMessage, true)).rejects.toThrow("Create message failed");
|
|
802
|
+
});
|
|
803
|
+
// Verify message was rolled back
|
|
804
|
+
expect(result.current.thread.messages.length).toBe(initialMessageCount);
|
|
805
|
+
});
|
|
806
|
+
it("should rollback optimistic update when updateThreadMessage fails", async () => {
|
|
807
|
+
const testError = new Error("Update message failed");
|
|
808
|
+
jest.mocked(mockThreadsApi.messages.create).mockRejectedValue(testError);
|
|
809
|
+
const { result } = renderHook(() => useTamboThread(), {
|
|
810
|
+
wrapper: Wrapper,
|
|
811
|
+
});
|
|
812
|
+
await act(async () => {
|
|
813
|
+
await result.current.switchCurrentThread("test-thread-1");
|
|
814
|
+
});
|
|
815
|
+
const existingMessage = createMockMessage({
|
|
816
|
+
id: "existing-msg",
|
|
817
|
+
threadId: "test-thread-1",
|
|
818
|
+
content: [{ type: "text", text: "Old content" }],
|
|
819
|
+
});
|
|
820
|
+
await act(async () => {
|
|
821
|
+
await result.current.addThreadMessage(existingMessage, false);
|
|
822
|
+
});
|
|
823
|
+
const initialMessageCount = result.current.thread.messages.length;
|
|
824
|
+
await act(async () => {
|
|
825
|
+
await expect(result.current.updateThreadMessage("existing-msg", {
|
|
826
|
+
threadId: "test-thread-1",
|
|
827
|
+
content: [{ type: "text", text: "New content" }],
|
|
828
|
+
role: "assistant",
|
|
829
|
+
}, true)).rejects.toThrow("Update message failed");
|
|
830
|
+
});
|
|
831
|
+
// Verify message was rolled back
|
|
832
|
+
expect(result.current.thread.messages.length).toBe(initialMessageCount - 1);
|
|
833
|
+
});
|
|
834
|
+
it("should rollback optimistic name update when updateThreadName fails", async () => {
|
|
835
|
+
const testError = new Error("Update name failed");
|
|
836
|
+
jest.mocked(mockThreadsApi.update).mockRejectedValue(testError);
|
|
837
|
+
const { result } = renderHook(() => useTamboThread(), {
|
|
838
|
+
wrapper: Wrapper,
|
|
839
|
+
});
|
|
840
|
+
await act(async () => {
|
|
841
|
+
await result.current.switchCurrentThread("test-thread-1");
|
|
842
|
+
});
|
|
843
|
+
const initialName = result.current.thread.name;
|
|
844
|
+
await act(async () => {
|
|
845
|
+
await expect(result.current.updateThreadName("New Name", "test-thread-1")).rejects.toThrow("Update name failed");
|
|
846
|
+
});
|
|
847
|
+
// Verify name was rolled back
|
|
848
|
+
expect(result.current.thread.name).toBe(initialName);
|
|
781
849
|
});
|
|
782
850
|
});
|
|
783
851
|
describe("refetch threads list behavior", () => {
|
|
@@ -785,8 +853,8 @@ describe("TamboThreadProvider", () => {
|
|
|
785
853
|
const { result } = renderHook(() => useTamboThread(), {
|
|
786
854
|
wrapper: Wrapper,
|
|
787
855
|
});
|
|
788
|
-
// Mock the
|
|
789
|
-
const
|
|
856
|
+
// Mock the stream response to return a new thread ID
|
|
857
|
+
const mockStreamResponse = {
|
|
790
858
|
responseMessageDto: {
|
|
791
859
|
id: "response-1",
|
|
792
860
|
content: [{ type: "text", text: "Response" }],
|
|
@@ -799,16 +867,19 @@ describe("TamboThreadProvider", () => {
|
|
|
799
867
|
generationStage: GenerationStage.COMPLETE,
|
|
800
868
|
mcpAccessToken: "test-mcp-access-token",
|
|
801
869
|
};
|
|
802
|
-
|
|
803
|
-
.
|
|
804
|
-
|
|
870
|
+
const mockAsyncIterator = {
|
|
871
|
+
[Symbol.asyncIterator]: async function* () {
|
|
872
|
+
yield mockStreamResponse;
|
|
873
|
+
},
|
|
874
|
+
};
|
|
875
|
+
jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
|
|
805
876
|
// Start with placeholder thread
|
|
806
877
|
expect(result.current.thread.id).toBe("placeholder");
|
|
807
878
|
// Send a message which will create a new thread with contextKey
|
|
808
879
|
await act(async () => {
|
|
809
880
|
await result.current.sendThreadMessage("Hello", {
|
|
810
881
|
threadId: "placeholder",
|
|
811
|
-
streamResponse:
|
|
882
|
+
streamResponse: true,
|
|
812
883
|
contextKey: "test-context-key",
|
|
813
884
|
});
|
|
814
885
|
});
|
|
@@ -846,93 +917,6 @@ describe("TamboThreadProvider", () => {
|
|
|
846
917
|
});
|
|
847
918
|
});
|
|
848
919
|
describe("transformToContent", () => {
|
|
849
|
-
it("should use custom transformToContent when provided (non-streaming)", async () => {
|
|
850
|
-
const mockTransformToContent = jest.fn().mockReturnValue([
|
|
851
|
-
{ type: "text", text: "Custom transformed content" },
|
|
852
|
-
{
|
|
853
|
-
type: "image_url",
|
|
854
|
-
image_url: { url: "https://example.com/image.png" },
|
|
855
|
-
},
|
|
856
|
-
]);
|
|
857
|
-
const customToolRegistry = [
|
|
858
|
-
{
|
|
859
|
-
name: "TestComponent",
|
|
860
|
-
component: () => React.createElement("div", null, "Test"),
|
|
861
|
-
description: "Test",
|
|
862
|
-
propsSchema: z.object({ test: z.string() }),
|
|
863
|
-
associatedTools: [
|
|
864
|
-
{
|
|
865
|
-
name: "custom-tool",
|
|
866
|
-
tool: jest.fn().mockResolvedValue({ data: "tool result" }),
|
|
867
|
-
description: "Tool with custom transform",
|
|
868
|
-
inputSchema: z.object({ input: z.string() }),
|
|
869
|
-
outputSchema: z.object({ data: z.string() }),
|
|
870
|
-
transformToContent: mockTransformToContent,
|
|
871
|
-
},
|
|
872
|
-
],
|
|
873
|
-
},
|
|
874
|
-
];
|
|
875
|
-
const mockToolCallResponse = {
|
|
876
|
-
responseMessageDto: {
|
|
877
|
-
id: "tool-call-1",
|
|
878
|
-
content: [{ type: "text", text: "Tool response" }],
|
|
879
|
-
role: "tool",
|
|
880
|
-
threadId: "test-thread-1",
|
|
881
|
-
toolCallRequest: {
|
|
882
|
-
toolName: "custom-tool",
|
|
883
|
-
parameters: [{ parameterName: "input", parameterValue: "test" }],
|
|
884
|
-
},
|
|
885
|
-
componentState: {},
|
|
886
|
-
createdAt: new Date().toISOString(),
|
|
887
|
-
},
|
|
888
|
-
generationStage: GenerationStage.COMPLETE,
|
|
889
|
-
mcpAccessToken: "test-mcp-access-token",
|
|
890
|
-
};
|
|
891
|
-
jest
|
|
892
|
-
.mocked(mockThreadsApi.advanceByID)
|
|
893
|
-
.mockResolvedValueOnce(mockToolCallResponse)
|
|
894
|
-
.mockResolvedValueOnce({
|
|
895
|
-
responseMessageDto: {
|
|
896
|
-
id: "final-response",
|
|
897
|
-
content: [{ type: "text", text: "Final response" }],
|
|
898
|
-
role: "assistant",
|
|
899
|
-
threadId: "test-thread-1",
|
|
900
|
-
componentState: {},
|
|
901
|
-
createdAt: new Date().toISOString(),
|
|
902
|
-
},
|
|
903
|
-
generationStage: GenerationStage.COMPLETE,
|
|
904
|
-
mcpAccessToken: "test-mcp-access-token",
|
|
905
|
-
});
|
|
906
|
-
const { result } = renderHook(() => useTamboThread(), {
|
|
907
|
-
wrapper: createWrapper({ components: customToolRegistry }),
|
|
908
|
-
});
|
|
909
|
-
await act(async () => {
|
|
910
|
-
await result.current.sendThreadMessage("Use custom tool", {
|
|
911
|
-
threadId: "test-thread-1",
|
|
912
|
-
streamResponse: false,
|
|
913
|
-
});
|
|
914
|
-
});
|
|
915
|
-
// Verify the tool was called with single object arg (new inputSchema interface)
|
|
916
|
-
expect(customToolRegistry[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith({ input: "test" });
|
|
917
|
-
// Verify transformToContent was called with the tool result
|
|
918
|
-
expect(mockTransformToContent).toHaveBeenCalledWith({
|
|
919
|
-
data: "tool result",
|
|
920
|
-
});
|
|
921
|
-
// Verify the second advance call included the transformed content
|
|
922
|
-
expect(mockThreadsApi.advanceByID).toHaveBeenCalledTimes(2);
|
|
923
|
-
expect(mockThreadsApi.advanceByID).toHaveBeenLastCalledWith("test-thread-1", expect.objectContaining({
|
|
924
|
-
messageToAppend: expect.objectContaining({
|
|
925
|
-
content: [
|
|
926
|
-
{ type: "text", text: "Custom transformed content" },
|
|
927
|
-
{
|
|
928
|
-
type: "image_url",
|
|
929
|
-
image_url: { url: "https://example.com/image.png" },
|
|
930
|
-
},
|
|
931
|
-
],
|
|
932
|
-
role: "tool",
|
|
933
|
-
}),
|
|
934
|
-
}));
|
|
935
|
-
});
|
|
936
920
|
it("should use custom async transformToContent when provided (streaming)", async () => {
|
|
937
921
|
const mockTransformToContent = jest
|
|
938
922
|
.fn()
|
|
@@ -1029,169 +1013,6 @@ describe("TamboThreadProvider", () => {
|
|
|
1029
1013
|
}),
|
|
1030
1014
|
}), "test-thread-1");
|
|
1031
1015
|
});
|
|
1032
|
-
it("should fallback to stringified text when transformToContent is not provided", async () => {
|
|
1033
|
-
const toolWithoutTransform = [
|
|
1034
|
-
{
|
|
1035
|
-
name: "TestComponent",
|
|
1036
|
-
component: () => React.createElement("div", null, "Test"),
|
|
1037
|
-
description: "Test",
|
|
1038
|
-
propsSchema: z.object({ test: z.string() }),
|
|
1039
|
-
associatedTools: [
|
|
1040
|
-
{
|
|
1041
|
-
name: "no-transform-tool",
|
|
1042
|
-
tool: jest
|
|
1043
|
-
.fn()
|
|
1044
|
-
.mockResolvedValue({ complex: "data", nested: { value: 42 } }),
|
|
1045
|
-
description: "Tool without custom transform",
|
|
1046
|
-
inputSchema: z.object({ input: z.string() }),
|
|
1047
|
-
outputSchema: z.object({
|
|
1048
|
-
complex: z.string(),
|
|
1049
|
-
nested: z.object({ value: z.number() }),
|
|
1050
|
-
}),
|
|
1051
|
-
// No transformToContent provided
|
|
1052
|
-
},
|
|
1053
|
-
],
|
|
1054
|
-
},
|
|
1055
|
-
];
|
|
1056
|
-
const mockToolCallResponse = {
|
|
1057
|
-
responseMessageDto: {
|
|
1058
|
-
id: "tool-call-1",
|
|
1059
|
-
content: [{ type: "text", text: "Tool call" }],
|
|
1060
|
-
role: "tool",
|
|
1061
|
-
threadId: "test-thread-1",
|
|
1062
|
-
toolCallRequest: {
|
|
1063
|
-
toolName: "no-transform-tool",
|
|
1064
|
-
parameters: [{ parameterName: "input", parameterValue: "test" }],
|
|
1065
|
-
},
|
|
1066
|
-
componentState: {},
|
|
1067
|
-
createdAt: new Date().toISOString(),
|
|
1068
|
-
},
|
|
1069
|
-
generationStage: GenerationStage.COMPLETE,
|
|
1070
|
-
mcpAccessToken: "test-mcp-access-token",
|
|
1071
|
-
};
|
|
1072
|
-
jest
|
|
1073
|
-
.mocked(mockThreadsApi.advanceByID)
|
|
1074
|
-
.mockResolvedValueOnce(mockToolCallResponse)
|
|
1075
|
-
.mockResolvedValueOnce({
|
|
1076
|
-
responseMessageDto: {
|
|
1077
|
-
id: "final-response",
|
|
1078
|
-
content: [{ type: "text", text: "Final response" }],
|
|
1079
|
-
role: "assistant",
|
|
1080
|
-
threadId: "test-thread-1",
|
|
1081
|
-
componentState: {},
|
|
1082
|
-
createdAt: new Date().toISOString(),
|
|
1083
|
-
},
|
|
1084
|
-
generationStage: GenerationStage.COMPLETE,
|
|
1085
|
-
mcpAccessToken: "test-mcp-access-token",
|
|
1086
|
-
});
|
|
1087
|
-
const { result } = renderHook(() => useTamboThread(), {
|
|
1088
|
-
wrapper: createWrapper({ components: toolWithoutTransform }),
|
|
1089
|
-
});
|
|
1090
|
-
await act(async () => {
|
|
1091
|
-
await result.current.sendThreadMessage("Use tool without transform", {
|
|
1092
|
-
threadId: "test-thread-1",
|
|
1093
|
-
streamResponse: false,
|
|
1094
|
-
});
|
|
1095
|
-
});
|
|
1096
|
-
// Verify the tool was called with single object arg (new inputSchema interface)
|
|
1097
|
-
expect(toolWithoutTransform[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith({ input: "test" });
|
|
1098
|
-
// Verify the second advance call used stringified content
|
|
1099
|
-
expect(mockThreadsApi.advanceByID).toHaveBeenLastCalledWith("test-thread-1", expect.objectContaining({
|
|
1100
|
-
messageToAppend: expect.objectContaining({
|
|
1101
|
-
content: [
|
|
1102
|
-
{
|
|
1103
|
-
type: "text",
|
|
1104
|
-
text: '{"complex":"data","nested":{"value":42}}',
|
|
1105
|
-
},
|
|
1106
|
-
],
|
|
1107
|
-
role: "tool",
|
|
1108
|
-
}),
|
|
1109
|
-
}));
|
|
1110
|
-
});
|
|
1111
|
-
it("should always return text for error responses even with transformToContent", async () => {
|
|
1112
|
-
const mockTransformToContent = jest.fn().mockReturnValue([
|
|
1113
|
-
{
|
|
1114
|
-
type: "image_url",
|
|
1115
|
-
image_url: { url: "https://example.com/error.png" },
|
|
1116
|
-
},
|
|
1117
|
-
]);
|
|
1118
|
-
const toolWithTransform = [
|
|
1119
|
-
{
|
|
1120
|
-
name: "TestComponent",
|
|
1121
|
-
component: () => React.createElement("div", null, "Test"),
|
|
1122
|
-
description: "Test",
|
|
1123
|
-
propsSchema: z.object({ test: z.string() }),
|
|
1124
|
-
associatedTools: [
|
|
1125
|
-
{
|
|
1126
|
-
name: "error-tool",
|
|
1127
|
-
tool: jest
|
|
1128
|
-
.fn()
|
|
1129
|
-
.mockRejectedValue(new Error("Tool execution failed")),
|
|
1130
|
-
description: "Tool that errors",
|
|
1131
|
-
inputSchema: z.object({ input: z.string() }),
|
|
1132
|
-
outputSchema: z.string(),
|
|
1133
|
-
transformToContent: mockTransformToContent,
|
|
1134
|
-
},
|
|
1135
|
-
],
|
|
1136
|
-
},
|
|
1137
|
-
];
|
|
1138
|
-
const mockToolCallResponse = {
|
|
1139
|
-
responseMessageDto: {
|
|
1140
|
-
id: "tool-call-1",
|
|
1141
|
-
content: [{ type: "text", text: "Tool call" }],
|
|
1142
|
-
role: "tool",
|
|
1143
|
-
threadId: "test-thread-1",
|
|
1144
|
-
toolCallRequest: {
|
|
1145
|
-
toolName: "error-tool",
|
|
1146
|
-
parameters: [{ parameterName: "input", parameterValue: "test" }],
|
|
1147
|
-
},
|
|
1148
|
-
componentState: {},
|
|
1149
|
-
createdAt: new Date().toISOString(),
|
|
1150
|
-
},
|
|
1151
|
-
generationStage: GenerationStage.COMPLETE,
|
|
1152
|
-
mcpAccessToken: "test-mcp-access-token",
|
|
1153
|
-
};
|
|
1154
|
-
jest
|
|
1155
|
-
.mocked(mockThreadsApi.advanceByID)
|
|
1156
|
-
.mockResolvedValueOnce(mockToolCallResponse)
|
|
1157
|
-
.mockResolvedValueOnce({
|
|
1158
|
-
responseMessageDto: {
|
|
1159
|
-
id: "final-response",
|
|
1160
|
-
content: [{ type: "text", text: "Final response" }],
|
|
1161
|
-
role: "assistant",
|
|
1162
|
-
threadId: "test-thread-1",
|
|
1163
|
-
componentState: {},
|
|
1164
|
-
createdAt: new Date().toISOString(),
|
|
1165
|
-
},
|
|
1166
|
-
generationStage: GenerationStage.COMPLETE,
|
|
1167
|
-
mcpAccessToken: "test-mcp-access-token",
|
|
1168
|
-
});
|
|
1169
|
-
const { result } = renderHook(() => useTamboThread(), {
|
|
1170
|
-
wrapper: createWrapper({ components: toolWithTransform }),
|
|
1171
|
-
});
|
|
1172
|
-
await act(async () => {
|
|
1173
|
-
await result.current.sendThreadMessage("Use error tool", {
|
|
1174
|
-
threadId: "test-thread-1",
|
|
1175
|
-
streamResponse: false,
|
|
1176
|
-
});
|
|
1177
|
-
});
|
|
1178
|
-
// Verify the tool was called with single object arg (new inputSchema interface)
|
|
1179
|
-
expect(toolWithTransform[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith({ input: "test" });
|
|
1180
|
-
// Verify transformToContent was NOT called for error responses
|
|
1181
|
-
expect(mockTransformToContent).not.toHaveBeenCalled();
|
|
1182
|
-
// Verify the second advance call used text content with the error message
|
|
1183
|
-
expect(mockThreadsApi.advanceByID).toHaveBeenLastCalledWith("test-thread-1", expect.objectContaining({
|
|
1184
|
-
messageToAppend: expect.objectContaining({
|
|
1185
|
-
content: [
|
|
1186
|
-
expect.objectContaining({
|
|
1187
|
-
type: "text",
|
|
1188
|
-
// Error message should be in text format
|
|
1189
|
-
}),
|
|
1190
|
-
],
|
|
1191
|
-
role: "tool",
|
|
1192
|
-
}),
|
|
1193
|
-
}));
|
|
1194
|
-
});
|
|
1195
1016
|
});
|
|
1196
1017
|
describe("tamboStreamableHint streaming behavior", () => {
|
|
1197
1018
|
it("should call streamable tool during streaming when tamboStreamableHint is true", async () => {
|
|
@@ -1524,6 +1345,25 @@ describe("TamboThreadProvider", () => {
|
|
|
1524
1345
|
});
|
|
1525
1346
|
describe("auto-generate thread name", () => {
|
|
1526
1347
|
it("should auto-generate thread name after reaching threshold", async () => {
|
|
1348
|
+
const mockStreamResponse = {
|
|
1349
|
+
responseMessageDto: {
|
|
1350
|
+
id: "response-1",
|
|
1351
|
+
content: [{ type: "text", text: "Response" }],
|
|
1352
|
+
role: "assistant",
|
|
1353
|
+
threadId: "test-thread-1",
|
|
1354
|
+
component: undefined,
|
|
1355
|
+
componentState: {},
|
|
1356
|
+
createdAt: new Date().toISOString(),
|
|
1357
|
+
},
|
|
1358
|
+
generationStage: GenerationStage.COMPLETE,
|
|
1359
|
+
mcpAccessToken: "test-mcp-access-token",
|
|
1360
|
+
};
|
|
1361
|
+
const mockAsyncIterator = {
|
|
1362
|
+
[Symbol.asyncIterator]: async function* () {
|
|
1363
|
+
yield mockStreamResponse;
|
|
1364
|
+
},
|
|
1365
|
+
};
|
|
1366
|
+
jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
|
|
1527
1367
|
const { result } = renderHook(() => useTamboThread(), {
|
|
1528
1368
|
wrapper: createWrapper({ autoGenerateNameThreshold: 2 }),
|
|
1529
1369
|
});
|
|
@@ -1555,13 +1395,34 @@ describe("TamboThreadProvider", () => {
|
|
|
1555
1395
|
}), false);
|
|
1556
1396
|
});
|
|
1557
1397
|
await act(async () => {
|
|
1558
|
-
await result.current.sendThreadMessage("Test message"
|
|
1398
|
+
await result.current.sendThreadMessage("Test message", {
|
|
1399
|
+
streamResponse: true,
|
|
1400
|
+
});
|
|
1559
1401
|
});
|
|
1560
1402
|
expect(mockThreadsApi.generateName).toHaveBeenCalledWith("test-thread-1");
|
|
1561
1403
|
expect(result.current.thread.name).toBe("Generated Thread Name");
|
|
1562
1404
|
expect(mockQueryClient.setQueryData).toHaveBeenCalledWith(["threads", "test-project-id", undefined], expect.any(Function));
|
|
1563
1405
|
});
|
|
1564
1406
|
it("should NOT auto-generate when autoGenerateThreadName is false", async () => {
|
|
1407
|
+
const mockStreamResponse = {
|
|
1408
|
+
responseMessageDto: {
|
|
1409
|
+
id: "response-1",
|
|
1410
|
+
content: [{ type: "text", text: "Response" }],
|
|
1411
|
+
role: "assistant",
|
|
1412
|
+
threadId: "test-thread-1",
|
|
1413
|
+
component: undefined,
|
|
1414
|
+
componentState: {},
|
|
1415
|
+
createdAt: new Date().toISOString(),
|
|
1416
|
+
},
|
|
1417
|
+
generationStage: GenerationStage.COMPLETE,
|
|
1418
|
+
mcpAccessToken: "test-mcp-access-token",
|
|
1419
|
+
};
|
|
1420
|
+
const mockAsyncIterator = {
|
|
1421
|
+
[Symbol.asyncIterator]: async function* () {
|
|
1422
|
+
yield mockStreamResponse;
|
|
1423
|
+
},
|
|
1424
|
+
};
|
|
1425
|
+
jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
|
|
1565
1426
|
const { result } = renderHook(() => useTamboThread(), {
|
|
1566
1427
|
wrapper: createWrapper({
|
|
1567
1428
|
autoGenerateThreadName: false,
|
|
@@ -1593,12 +1454,33 @@ describe("TamboThreadProvider", () => {
|
|
|
1593
1454
|
}), false);
|
|
1594
1455
|
});
|
|
1595
1456
|
await act(async () => {
|
|
1596
|
-
await result.current.sendThreadMessage("Test message"
|
|
1457
|
+
await result.current.sendThreadMessage("Test message", {
|
|
1458
|
+
streamResponse: true,
|
|
1459
|
+
});
|
|
1597
1460
|
});
|
|
1598
1461
|
// Should NOT generate name because feature is disabled
|
|
1599
1462
|
expect(mockThreadsApi.generateName).not.toHaveBeenCalled();
|
|
1600
1463
|
});
|
|
1601
1464
|
it("should NOT auto-generate when thread already has a name", async () => {
|
|
1465
|
+
const mockStreamResponse = {
|
|
1466
|
+
responseMessageDto: {
|
|
1467
|
+
id: "response-1",
|
|
1468
|
+
content: [{ type: "text", text: "Response" }],
|
|
1469
|
+
role: "assistant",
|
|
1470
|
+
threadId: "test-thread-1",
|
|
1471
|
+
component: undefined,
|
|
1472
|
+
componentState: {},
|
|
1473
|
+
createdAt: new Date().toISOString(),
|
|
1474
|
+
},
|
|
1475
|
+
generationStage: GenerationStage.COMPLETE,
|
|
1476
|
+
mcpAccessToken: "test-mcp-access-token",
|
|
1477
|
+
};
|
|
1478
|
+
const mockAsyncIterator = {
|
|
1479
|
+
[Symbol.asyncIterator]: async function* () {
|
|
1480
|
+
yield mockStreamResponse;
|
|
1481
|
+
},
|
|
1482
|
+
};
|
|
1483
|
+
jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
|
|
1602
1484
|
const { result } = renderHook(() => useTamboThread(), {
|
|
1603
1485
|
wrapper: createWrapper({ autoGenerateNameThreshold: 2 }),
|
|
1604
1486
|
});
|
|
@@ -1632,7 +1514,9 @@ describe("TamboThreadProvider", () => {
|
|
|
1632
1514
|
expect(result.current.thread.messages).toHaveLength(2);
|
|
1633
1515
|
// Send another message to reach threshold (3 messages total)
|
|
1634
1516
|
await act(async () => {
|
|
1635
|
-
await result.current.sendThreadMessage("Test message"
|
|
1517
|
+
await result.current.sendThreadMessage("Test message", {
|
|
1518
|
+
streamResponse: true,
|
|
1519
|
+
});
|
|
1636
1520
|
});
|
|
1637
1521
|
// Should NOT generate name because thread already has one
|
|
1638
1522
|
expect(mockThreadsApi.generateName).not.toHaveBeenCalled();
|