@tambo-ai/react 0.67.1 → 0.68.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 +2 -4
- package/dist/context-helpers/current-interactables-context-helper.d.ts.map +1 -1
- package/dist/context-helpers/current-interactables-context-helper.js +4 -1
- package/dist/context-helpers/current-interactables-context-helper.js.map +1 -1
- package/dist/hoc/with-tambo-interactable.d.ts +50 -4
- package/dist/hoc/with-tambo-interactable.d.ts.map +1 -1
- package/dist/hoc/with-tambo-interactable.js +20 -5
- package/dist/hoc/with-tambo-interactable.js.map +1 -1
- package/dist/hooks/use-component-state.d.ts +3 -8
- package/dist/hooks/use-component-state.d.ts.map +1 -1
- package/dist/hooks/use-component-state.js +8 -0
- package/dist/hooks/use-component-state.js.map +1 -1
- package/dist/hooks/use-component-state.test.js +37 -0
- package/dist/hooks/use-component-state.test.js.map +1 -1
- package/dist/hooks/use-tambo-threads.js +1 -1
- package/dist/hooks/use-tambo-threads.js.map +1 -1
- package/dist/mcp/mcp-constants.d.ts +19 -0
- package/dist/mcp/mcp-constants.d.ts.map +1 -0
- package/dist/mcp/mcp-constants.js +21 -0
- package/dist/mcp/mcp-constants.js.map +1 -0
- package/dist/mcp/mcp-hooks.d.ts +32 -3
- package/dist/mcp/mcp-hooks.d.ts.map +1 -1
- package/dist/mcp/mcp-hooks.js +40 -29
- package/dist/mcp/mcp-hooks.js.map +1 -1
- package/dist/mcp/mcp-hooks.test.js +8 -5
- package/dist/mcp/mcp-hooks.test.js.map +1 -1
- package/dist/mcp/tambo-mcp-provider.d.ts +7 -0
- package/dist/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/dist/mcp/tambo-mcp-provider.js +202 -155
- package/dist/mcp/tambo-mcp-provider.js.map +1 -1
- package/dist/model/component-metadata.d.ts +1 -1
- package/dist/model/component-metadata.d.ts.map +1 -1
- package/dist/model/component-metadata.js.map +1 -1
- package/dist/model/tambo-interactable.d.ts +7 -5
- package/dist/model/tambo-interactable.d.ts.map +1 -1
- package/dist/model/tambo-interactable.js.map +1 -1
- package/dist/providers/__tests__/thread-input-resource-resolution.test.d.ts +2 -0
- package/dist/providers/__tests__/thread-input-resource-resolution.test.d.ts.map +1 -0
- package/dist/providers/__tests__/thread-input-resource-resolution.test.js +592 -0
- package/dist/providers/__tests__/thread-input-resource-resolution.test.js.map +1 -0
- package/dist/providers/tambo-interactable-provider-partial-updates.test.js +22 -21
- package/dist/providers/tambo-interactable-provider-partial-updates.test.js.map +1 -1
- package/dist/providers/tambo-interactable-provider.d.ts +3 -2
- package/dist/providers/tambo-interactable-provider.d.ts.map +1 -1
- package/dist/providers/tambo-interactable-provider.js +98 -14
- package/dist/providers/tambo-interactable-provider.js.map +1 -1
- package/dist/providers/tambo-interactable-provider.test.js +242 -0
- package/dist/providers/tambo-interactable-provider.test.js.map +1 -1
- package/dist/providers/tambo-provider.d.ts.map +1 -1
- package/dist/providers/tambo-provider.js +7 -5
- package/dist/providers/tambo-provider.js.map +1 -1
- package/dist/providers/tambo-thread-input-provider.d.ts.map +1 -1
- package/dist/providers/tambo-thread-input-provider.js +21 -3
- package/dist/providers/tambo-thread-input-provider.js.map +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/json-schema.d.ts +7 -0
- package/dist/schema/json-schema.d.ts.map +1 -1
- package/dist/schema/json-schema.js +11 -0
- package/dist/schema/json-schema.js.map +1 -1
- package/dist/schema/json-schema.test.d.ts +2 -0
- package/dist/schema/json-schema.test.d.ts.map +1 -0
- package/dist/schema/json-schema.test.js +204 -0
- package/dist/schema/json-schema.test.js.map +1 -0
- package/dist/setupTests.js +3 -0
- package/dist/setupTests.js.map +1 -1
- package/dist/util/message-builder.d.ts +3 -1
- package/dist/util/message-builder.d.ts.map +1 -1
- package/dist/util/message-builder.js +20 -3
- package/dist/util/message-builder.js.map +1 -1
- package/dist/util/message-builder.test.js +269 -0
- package/dist/util/message-builder.test.js.map +1 -1
- package/dist/util/resource-content-resolver.d.ts +20 -0
- package/dist/util/resource-content-resolver.d.ts.map +1 -0
- package/dist/util/resource-content-resolver.js +93 -0
- package/dist/util/resource-content-resolver.js.map +1 -0
- package/dist/util/resource-content-resolver.test.d.ts +2 -0
- package/dist/util/resource-content-resolver.test.d.ts.map +1 -0
- package/dist/util/resource-content-resolver.test.js +254 -0
- package/dist/util/resource-content-resolver.test.js.map +1 -0
- package/esm/context-helpers/current-interactables-context-helper.d.ts.map +1 -1
- package/esm/context-helpers/current-interactables-context-helper.js +4 -1
- package/esm/context-helpers/current-interactables-context-helper.js.map +1 -1
- package/esm/hoc/with-tambo-interactable.d.ts +50 -4
- package/esm/hoc/with-tambo-interactable.d.ts.map +1 -1
- package/esm/hoc/with-tambo-interactable.js +20 -5
- package/esm/hoc/with-tambo-interactable.js.map +1 -1
- package/esm/hooks/use-component-state.d.ts +3 -8
- package/esm/hooks/use-component-state.d.ts.map +1 -1
- package/esm/hooks/use-component-state.js +8 -0
- package/esm/hooks/use-component-state.js.map +1 -1
- package/esm/hooks/use-component-state.test.js +37 -0
- package/esm/hooks/use-component-state.test.js.map +1 -1
- package/esm/hooks/use-tambo-threads.js +1 -1
- package/esm/hooks/use-tambo-threads.js.map +1 -1
- package/esm/mcp/mcp-constants.d.ts +19 -0
- package/esm/mcp/mcp-constants.d.ts.map +1 -0
- package/esm/mcp/mcp-constants.js +18 -0
- package/esm/mcp/mcp-constants.js.map +1 -0
- package/esm/mcp/mcp-hooks.d.ts +32 -3
- package/esm/mcp/mcp-hooks.d.ts.map +1 -1
- package/esm/mcp/mcp-hooks.js +40 -30
- package/esm/mcp/mcp-hooks.js.map +1 -1
- package/esm/mcp/mcp-hooks.test.js +8 -5
- package/esm/mcp/mcp-hooks.test.js.map +1 -1
- package/esm/mcp/tambo-mcp-provider.d.ts +7 -0
- package/esm/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/esm/mcp/tambo-mcp-provider.js +201 -154
- package/esm/mcp/tambo-mcp-provider.js.map +1 -1
- package/esm/model/component-metadata.d.ts +1 -1
- package/esm/model/component-metadata.d.ts.map +1 -1
- package/esm/model/component-metadata.js.map +1 -1
- package/esm/model/tambo-interactable.d.ts +7 -5
- package/esm/model/tambo-interactable.d.ts.map +1 -1
- package/esm/model/tambo-interactable.js.map +1 -1
- package/esm/providers/__tests__/thread-input-resource-resolution.test.d.ts +2 -0
- package/esm/providers/__tests__/thread-input-resource-resolution.test.d.ts.map +1 -0
- package/esm/providers/__tests__/thread-input-resource-resolution.test.js +587 -0
- package/esm/providers/__tests__/thread-input-resource-resolution.test.js.map +1 -0
- package/esm/providers/tambo-interactable-provider-partial-updates.test.js +22 -21
- package/esm/providers/tambo-interactable-provider-partial-updates.test.js.map +1 -1
- package/esm/providers/tambo-interactable-provider.d.ts +3 -2
- package/esm/providers/tambo-interactable-provider.d.ts.map +1 -1
- package/esm/providers/tambo-interactable-provider.js +98 -14
- package/esm/providers/tambo-interactable-provider.js.map +1 -1
- package/esm/providers/tambo-interactable-provider.test.js +242 -0
- package/esm/providers/tambo-interactable-provider.test.js.map +1 -1
- package/esm/providers/tambo-provider.d.ts.map +1 -1
- package/esm/providers/tambo-provider.js +7 -5
- package/esm/providers/tambo-provider.js.map +1 -1
- package/esm/providers/tambo-thread-input-provider.d.ts.map +1 -1
- package/esm/providers/tambo-thread-input-provider.js +21 -3
- package/esm/providers/tambo-thread-input-provider.js.map +1 -1
- package/esm/schema/index.d.ts +1 -1
- package/esm/schema/index.d.ts.map +1 -1
- package/esm/schema/index.js +1 -1
- package/esm/schema/index.js.map +1 -1
- package/esm/schema/json-schema.d.ts +7 -0
- package/esm/schema/json-schema.d.ts.map +1 -1
- package/esm/schema/json-schema.js +10 -0
- package/esm/schema/json-schema.js.map +1 -1
- package/esm/schema/json-schema.test.d.ts +2 -0
- package/esm/schema/json-schema.test.d.ts.map +1 -0
- package/esm/schema/json-schema.test.js +202 -0
- package/esm/schema/json-schema.test.js.map +1 -0
- package/esm/setupTests.js +3 -0
- package/esm/setupTests.js.map +1 -1
- package/esm/util/message-builder.d.ts +3 -1
- package/esm/util/message-builder.d.ts.map +1 -1
- package/esm/util/message-builder.js +20 -3
- package/esm/util/message-builder.js.map +1 -1
- package/esm/util/message-builder.test.js +269 -0
- package/esm/util/message-builder.test.js.map +1 -1
- package/esm/util/resource-content-resolver.d.ts +20 -0
- package/esm/util/resource-content-resolver.d.ts.map +1 -0
- package/esm/util/resource-content-resolver.js +89 -0
- package/esm/util/resource-content-resolver.js.map +1 -0
- package/esm/util/resource-content-resolver.test.d.ts +2 -0
- package/esm/util/resource-content-resolver.test.d.ts.map +1 -0
- package/esm/util/resource-content-resolver.test.js +252 -0
- package/esm/util/resource-content-resolver.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const react_query_1 = require("@tanstack/react-query");
|
|
7
|
+
const react_1 = require("@testing-library/react");
|
|
8
|
+
const react_2 = __importDefault(require("react"));
|
|
9
|
+
const mcp_constants_1 = require("../../mcp/mcp-constants");
|
|
10
|
+
const tambo_provider_1 = require("../tambo-provider");
|
|
11
|
+
const tambo_thread_input_provider_1 = require("../tambo-thread-input-provider");
|
|
12
|
+
// Mock the Tambo client provider to avoid needing real API credentials
|
|
13
|
+
jest.mock("../tambo-client-provider", () => {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
|
+
const ReactModule = require("react");
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
|
+
const { QueryClient: QC } = require("@tanstack/react-query");
|
|
18
|
+
const mockQueryClient = new QC();
|
|
19
|
+
const MockContext = ReactModule.createContext({
|
|
20
|
+
client: {},
|
|
21
|
+
queryClient: mockQueryClient,
|
|
22
|
+
isUpdatingToken: false,
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
TamboClientProvider: ({ children }) => (react_2.default.createElement(MockContext.Provider, { value: {
|
|
26
|
+
client: {},
|
|
27
|
+
queryClient: mockQueryClient,
|
|
28
|
+
isUpdatingToken: false,
|
|
29
|
+
} }, children)),
|
|
30
|
+
TamboClientContext: MockContext,
|
|
31
|
+
useTamboClient: () => ({
|
|
32
|
+
client: {},
|
|
33
|
+
queryClient: mockQueryClient,
|
|
34
|
+
isUpdatingToken: false,
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
// Mock the thread provider to capture sendThreadMessage calls
|
|
39
|
+
const mockSendThreadMessage = jest.fn();
|
|
40
|
+
const mockContextKey = "test-context-key";
|
|
41
|
+
jest.mock("../tambo-thread-provider", () => ({
|
|
42
|
+
TamboThreadProvider: ({ children }) => children,
|
|
43
|
+
useTamboThread: () => ({
|
|
44
|
+
thread: { id: "test-thread-id" },
|
|
45
|
+
sendThreadMessage: mockSendThreadMessage,
|
|
46
|
+
contextKey: mockContextKey,
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
49
|
+
// Mock servers array - will be updated per test
|
|
50
|
+
let mockServers = [];
|
|
51
|
+
// Mock the MCP provider to avoid real MCP connections
|
|
52
|
+
jest.mock("../../mcp/tambo-mcp-provider", () => ({
|
|
53
|
+
TamboMcpProvider: ({ children }) => children,
|
|
54
|
+
useTamboMcpServers: () => mockServers,
|
|
55
|
+
}));
|
|
56
|
+
// Mock the MCP token provider
|
|
57
|
+
jest.mock("../tambo-mcp-token-provider", () => ({
|
|
58
|
+
TamboMcpTokenProvider: ({ children }) => children,
|
|
59
|
+
useTamboMcpToken: () => ({
|
|
60
|
+
mcpAccessToken: null,
|
|
61
|
+
tamboBaseUrl: null,
|
|
62
|
+
}),
|
|
63
|
+
}));
|
|
64
|
+
// Helper to create internal server
|
|
65
|
+
const createMockInternalServer = (serverKey) => ({
|
|
66
|
+
key: serverKey,
|
|
67
|
+
serverKey,
|
|
68
|
+
url: "https://api.tambo.ai/mcp",
|
|
69
|
+
name: "__tambo_internal_mcp_server__",
|
|
70
|
+
transport: "http",
|
|
71
|
+
serverType: mcp_constants_1.ServerType.TAMBO_INTERNAL,
|
|
72
|
+
connectionError: undefined, // Internal servers resolved by backend
|
|
73
|
+
});
|
|
74
|
+
// Mock the message images hook
|
|
75
|
+
jest.mock("../../hooks/use-message-images", () => ({
|
|
76
|
+
useMessageImages: () => ({
|
|
77
|
+
images: [],
|
|
78
|
+
addImage: jest.fn(),
|
|
79
|
+
addImages: jest.fn(),
|
|
80
|
+
removeImage: jest.fn(),
|
|
81
|
+
clearImages: jest.fn(),
|
|
82
|
+
}),
|
|
83
|
+
}));
|
|
84
|
+
// Mock the mutation hook to execute the mutation function directly
|
|
85
|
+
jest.mock("../../hooks/react-query-hooks", () => ({
|
|
86
|
+
useTamboMutation: ({ mutationFn }) => ({
|
|
87
|
+
mutateAsync: mutationFn,
|
|
88
|
+
mutate: mutationFn,
|
|
89
|
+
isLoading: false,
|
|
90
|
+
isError: false,
|
|
91
|
+
isSuccess: false,
|
|
92
|
+
error: null,
|
|
93
|
+
reset: jest.fn(),
|
|
94
|
+
}),
|
|
95
|
+
}));
|
|
96
|
+
describe("TamboProvider - Resource Content Resolution Integration", () => {
|
|
97
|
+
let queryClient;
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
jest.clearAllMocks();
|
|
100
|
+
mockSendThreadMessage.mockResolvedValue(undefined);
|
|
101
|
+
// Default: no MCP servers (registry resources don't need a server entry)
|
|
102
|
+
mockServers = [];
|
|
103
|
+
queryClient = new react_query_1.QueryClient({
|
|
104
|
+
defaultOptions: {
|
|
105
|
+
queries: { retry: false },
|
|
106
|
+
mutations: { retry: false },
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
queryClient.clear();
|
|
112
|
+
mockServers = [];
|
|
113
|
+
});
|
|
114
|
+
const createWrapper = (options) => {
|
|
115
|
+
function Wrapper({ children }) {
|
|
116
|
+
return (react_2.default.createElement(react_query_1.QueryClientProvider, { client: queryClient },
|
|
117
|
+
react_2.default.createElement(tambo_provider_1.TamboProvider, { apiKey: "test-api-key", tamboUrl: "https://api.tambo.ai", listResources: options?.listResources,
|
|
118
|
+
// Cast to any because test mocks return simplified types
|
|
119
|
+
getResource: options?.getResource, resources: options?.resources, contextKey: mockContextKey }, children)));
|
|
120
|
+
}
|
|
121
|
+
return Wrapper;
|
|
122
|
+
};
|
|
123
|
+
it("should resolve registry resource content when submitting a message", async () => {
|
|
124
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
125
|
+
contents: [
|
|
126
|
+
{
|
|
127
|
+
uri: "file:///my-document.txt",
|
|
128
|
+
mimeType: "text/plain",
|
|
129
|
+
text: "This is the document content from the registry",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
const mockListResources = jest.fn().mockResolvedValue([
|
|
134
|
+
{
|
|
135
|
+
uri: "file:///my-document.txt",
|
|
136
|
+
name: "My Document",
|
|
137
|
+
mimeType: "text/plain",
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
141
|
+
wrapper: createWrapper({
|
|
142
|
+
listResources: mockListResources,
|
|
143
|
+
getResource: mockGetResource,
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
// Set the input value with a registry resource reference
|
|
147
|
+
(0, react_1.act)(() => {
|
|
148
|
+
result.current.setValue("Show me the contents of @registry:file:///my-document.txt in a table");
|
|
149
|
+
});
|
|
150
|
+
// Submit the message
|
|
151
|
+
await (0, react_1.act)(async () => {
|
|
152
|
+
await result.current.submit();
|
|
153
|
+
});
|
|
154
|
+
// Verify getResource was called with the correct URI (without the registry: prefix)
|
|
155
|
+
expect(mockGetResource).toHaveBeenCalledWith("file:///my-document.txt");
|
|
156
|
+
// Verify sendThreadMessage was called with the resolved content
|
|
157
|
+
expect(mockSendThreadMessage).toHaveBeenCalledTimes(1);
|
|
158
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
159
|
+
const content = options.content;
|
|
160
|
+
// Should have: text "Show me the contents of ", resource with resolved content, text " in a table"
|
|
161
|
+
expect(content).toMatchInlineSnapshot(`
|
|
162
|
+
[
|
|
163
|
+
{
|
|
164
|
+
"text": "Show me the contents of ",
|
|
165
|
+
"type": "text",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"resource": {
|
|
169
|
+
"mimeType": "text/plain",
|
|
170
|
+
"text": "This is the document content from the registry",
|
|
171
|
+
"uri": "file:///my-document.txt",
|
|
172
|
+
},
|
|
173
|
+
"type": "resource",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"text": " in a table",
|
|
177
|
+
"type": "text",
|
|
178
|
+
},
|
|
179
|
+
]
|
|
180
|
+
`);
|
|
181
|
+
});
|
|
182
|
+
it("should resolve multiple registry resources in a single message", async () => {
|
|
183
|
+
const mockGetResource = jest
|
|
184
|
+
.fn()
|
|
185
|
+
.mockImplementation(async (uri) => {
|
|
186
|
+
if (uri === "file:///doc1.txt") {
|
|
187
|
+
return { contents: [{ uri, text: "Content of doc1" }] };
|
|
188
|
+
}
|
|
189
|
+
if (uri === "file:///doc2.txt") {
|
|
190
|
+
return { contents: [{ uri, text: "Content of doc2" }] };
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
});
|
|
194
|
+
const mockListResources = jest.fn().mockResolvedValue([]);
|
|
195
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
196
|
+
wrapper: createWrapper({
|
|
197
|
+
listResources: mockListResources,
|
|
198
|
+
getResource: mockGetResource,
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
(0, react_1.act)(() => {
|
|
202
|
+
result.current.setValue("Compare @registry:file:///doc1.txt with @registry:file:///doc2.txt");
|
|
203
|
+
});
|
|
204
|
+
await (0, react_1.act)(async () => {
|
|
205
|
+
await result.current.submit();
|
|
206
|
+
});
|
|
207
|
+
// Both resources should have been fetched
|
|
208
|
+
expect(mockGetResource).toHaveBeenCalledWith("file:///doc1.txt");
|
|
209
|
+
expect(mockGetResource).toHaveBeenCalledWith("file:///doc2.txt");
|
|
210
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
211
|
+
const content = options.content;
|
|
212
|
+
expect(content).toMatchInlineSnapshot(`
|
|
213
|
+
[
|
|
214
|
+
{
|
|
215
|
+
"text": "Compare ",
|
|
216
|
+
"type": "text",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"resource": {
|
|
220
|
+
"text": "Content of doc1",
|
|
221
|
+
"uri": "file:///doc1.txt",
|
|
222
|
+
},
|
|
223
|
+
"type": "resource",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"text": " with ",
|
|
227
|
+
"type": "text",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"resource": {
|
|
231
|
+
"text": "Content of doc2",
|
|
232
|
+
"uri": "file:///doc2.txt",
|
|
233
|
+
},
|
|
234
|
+
"type": "resource",
|
|
235
|
+
},
|
|
236
|
+
]
|
|
237
|
+
`);
|
|
238
|
+
});
|
|
239
|
+
it("should resolve registry resource with blob content", async () => {
|
|
240
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
241
|
+
contents: [
|
|
242
|
+
{
|
|
243
|
+
uri: "file:///image.png",
|
|
244
|
+
mimeType: "image/png",
|
|
245
|
+
blob: "base64encodedimagedata",
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
const mockListResources = jest.fn().mockResolvedValue([]);
|
|
250
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
251
|
+
wrapper: createWrapper({
|
|
252
|
+
listResources: mockListResources,
|
|
253
|
+
getResource: mockGetResource,
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
(0, react_1.act)(() => {
|
|
257
|
+
result.current.setValue("Analyze @registry:file:///image.png");
|
|
258
|
+
});
|
|
259
|
+
await (0, react_1.act)(async () => {
|
|
260
|
+
await result.current.submit();
|
|
261
|
+
});
|
|
262
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
263
|
+
const content = options.content;
|
|
264
|
+
expect(content).toMatchInlineSnapshot(`
|
|
265
|
+
[
|
|
266
|
+
{
|
|
267
|
+
"text": "Analyze ",
|
|
268
|
+
"type": "text",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"resource": {
|
|
272
|
+
"blob": "base64encodedimagedata",
|
|
273
|
+
"mimeType": "image/png",
|
|
274
|
+
"uri": "file:///image.png",
|
|
275
|
+
},
|
|
276
|
+
"type": "resource",
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
`);
|
|
280
|
+
});
|
|
281
|
+
it("should continue with submission even if resource fetch fails (graceful fallback)", async () => {
|
|
282
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
283
|
+
const mockGetResource = jest
|
|
284
|
+
.fn()
|
|
285
|
+
.mockRejectedValue(new Error("Resource fetch failed"));
|
|
286
|
+
const mockListResources = jest.fn().mockResolvedValue([]);
|
|
287
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
288
|
+
wrapper: createWrapper({
|
|
289
|
+
listResources: mockListResources,
|
|
290
|
+
getResource: mockGetResource,
|
|
291
|
+
}),
|
|
292
|
+
});
|
|
293
|
+
(0, react_1.act)(() => {
|
|
294
|
+
result.current.setValue("Check @registry:file:///missing.txt");
|
|
295
|
+
});
|
|
296
|
+
// Should not throw, should continue with submission
|
|
297
|
+
await (0, react_1.act)(async () => {
|
|
298
|
+
await result.current.submit();
|
|
299
|
+
});
|
|
300
|
+
// Resource fetch was attempted
|
|
301
|
+
expect(mockGetResource).toHaveBeenCalledWith("file:///missing.txt");
|
|
302
|
+
// Warning should be logged
|
|
303
|
+
expect(consoleSpy).toHaveBeenCalledWith("Failed to fetch resource content for registry:file:///missing.txt:", expect.any(Error));
|
|
304
|
+
// Message should still be sent (without the resolved content)
|
|
305
|
+
expect(mockSendThreadMessage).toHaveBeenCalledTimes(1);
|
|
306
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
307
|
+
const content = options.content;
|
|
308
|
+
// Resource should be present but without text/blob content
|
|
309
|
+
expect(content).toMatchInlineSnapshot(`
|
|
310
|
+
[
|
|
311
|
+
{
|
|
312
|
+
"text": "Check ",
|
|
313
|
+
"type": "text",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"resource": {
|
|
317
|
+
"uri": "file:///missing.txt",
|
|
318
|
+
},
|
|
319
|
+
"type": "resource",
|
|
320
|
+
},
|
|
321
|
+
]
|
|
322
|
+
`);
|
|
323
|
+
consoleSpy.mockRestore();
|
|
324
|
+
});
|
|
325
|
+
it("should NOT resolve internal server resources (serverType: TAMBO_INTERNAL)", async () => {
|
|
326
|
+
// Add internal server to mock servers
|
|
327
|
+
mockServers = [createMockInternalServer("tambo-abc123")];
|
|
328
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
329
|
+
contents: [{ uri: "tambo:test://resource/1", text: "Should not fetch" }],
|
|
330
|
+
});
|
|
331
|
+
const mockListResources = jest.fn().mockResolvedValue([]);
|
|
332
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
333
|
+
wrapper: createWrapper({
|
|
334
|
+
listResources: mockListResources,
|
|
335
|
+
getResource: mockGetResource,
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
(0, react_1.act)(() => {
|
|
339
|
+
result.current.setValue("Check @tambo-abc123:tambo:test://internal/resource/1");
|
|
340
|
+
});
|
|
341
|
+
await (0, react_1.act)(async () => {
|
|
342
|
+
await result.current.submit();
|
|
343
|
+
});
|
|
344
|
+
// getResource should NOT be called for internal server resources
|
|
345
|
+
expect(mockGetResource).not.toHaveBeenCalled();
|
|
346
|
+
// Message should still be sent with resource (but without resolved content)
|
|
347
|
+
expect(mockSendThreadMessage).toHaveBeenCalledTimes(1);
|
|
348
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
349
|
+
const content = options.content;
|
|
350
|
+
expect(content).toMatchInlineSnapshot(`
|
|
351
|
+
[
|
|
352
|
+
{
|
|
353
|
+
"text": "Check ",
|
|
354
|
+
"type": "text",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"resource": {
|
|
358
|
+
"uri": "tambo:test://internal/resource/1",
|
|
359
|
+
},
|
|
360
|
+
"type": "resource",
|
|
361
|
+
},
|
|
362
|
+
]
|
|
363
|
+
`);
|
|
364
|
+
});
|
|
365
|
+
it("should handle mixed registry and internal server resources", async () => {
|
|
366
|
+
// Add internal server
|
|
367
|
+
mockServers = [createMockInternalServer("tambo-xyz")];
|
|
368
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
369
|
+
contents: [{ uri: "file:///registry-doc.txt", text: "Registry content" }],
|
|
370
|
+
});
|
|
371
|
+
const mockListResources = jest.fn().mockResolvedValue([]);
|
|
372
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
373
|
+
wrapper: createWrapper({
|
|
374
|
+
listResources: mockListResources,
|
|
375
|
+
getResource: mockGetResource,
|
|
376
|
+
}),
|
|
377
|
+
});
|
|
378
|
+
(0, react_1.act)(() => {
|
|
379
|
+
result.current.setValue("@registry:file:///registry-doc.txt and @tambo-xyz:tambo:test://internal");
|
|
380
|
+
});
|
|
381
|
+
await (0, react_1.act)(async () => {
|
|
382
|
+
await result.current.submit();
|
|
383
|
+
});
|
|
384
|
+
// Only the registry resource should be fetched
|
|
385
|
+
expect(mockGetResource).toHaveBeenCalledTimes(1);
|
|
386
|
+
expect(mockGetResource).toHaveBeenCalledWith("file:///registry-doc.txt");
|
|
387
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
388
|
+
const content = options.content;
|
|
389
|
+
expect(content).toMatchInlineSnapshot(`
|
|
390
|
+
[
|
|
391
|
+
{
|
|
392
|
+
"resource": {
|
|
393
|
+
"text": "Registry content",
|
|
394
|
+
"uri": "file:///registry-doc.txt",
|
|
395
|
+
},
|
|
396
|
+
"type": "resource",
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"text": " and ",
|
|
400
|
+
"type": "text",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
"resource": {
|
|
404
|
+
"uri": "tambo:test://internal",
|
|
405
|
+
},
|
|
406
|
+
"type": "resource",
|
|
407
|
+
},
|
|
408
|
+
]
|
|
409
|
+
`);
|
|
410
|
+
});
|
|
411
|
+
it("should include resource names when provided in submit options", async () => {
|
|
412
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
413
|
+
contents: [
|
|
414
|
+
{
|
|
415
|
+
uri: "file:///doc.txt",
|
|
416
|
+
text: "Document content",
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
const mockListResources = jest.fn().mockResolvedValue([]);
|
|
421
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
422
|
+
wrapper: createWrapper({
|
|
423
|
+
listResources: mockListResources,
|
|
424
|
+
getResource: mockGetResource,
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
(0, react_1.act)(() => {
|
|
428
|
+
result.current.setValue("Check @registry:file:///doc.txt");
|
|
429
|
+
});
|
|
430
|
+
await (0, react_1.act)(async () => {
|
|
431
|
+
await result.current.submit({
|
|
432
|
+
resourceNames: {
|
|
433
|
+
"registry:file:///doc.txt": "Important Document.txt",
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
438
|
+
const content = options.content;
|
|
439
|
+
expect(content).toMatchInlineSnapshot(`
|
|
440
|
+
[
|
|
441
|
+
{
|
|
442
|
+
"text": "Check ",
|
|
443
|
+
"type": "text",
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"resource": {
|
|
447
|
+
"name": "Important Document.txt",
|
|
448
|
+
"text": "Document content",
|
|
449
|
+
"uri": "file:///doc.txt",
|
|
450
|
+
},
|
|
451
|
+
"type": "resource",
|
|
452
|
+
},
|
|
453
|
+
]
|
|
454
|
+
`);
|
|
455
|
+
});
|
|
456
|
+
it("should handle message without any resource references", async () => {
|
|
457
|
+
const mockGetResource = jest.fn();
|
|
458
|
+
const mockListResources = jest.fn().mockResolvedValue([]);
|
|
459
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
460
|
+
wrapper: createWrapper({
|
|
461
|
+
listResources: mockListResources,
|
|
462
|
+
getResource: mockGetResource,
|
|
463
|
+
}),
|
|
464
|
+
});
|
|
465
|
+
(0, react_1.act)(() => {
|
|
466
|
+
result.current.setValue("Just a regular message without resources");
|
|
467
|
+
});
|
|
468
|
+
await (0, react_1.act)(async () => {
|
|
469
|
+
await result.current.submit();
|
|
470
|
+
});
|
|
471
|
+
// No resource fetch should happen
|
|
472
|
+
expect(mockGetResource).not.toHaveBeenCalled();
|
|
473
|
+
// Message should be sent as plain text
|
|
474
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
475
|
+
const content = options.content;
|
|
476
|
+
expect(content).toMatchInlineSnapshot(`
|
|
477
|
+
[
|
|
478
|
+
{
|
|
479
|
+
"text": "Just a regular message without resources",
|
|
480
|
+
"type": "text",
|
|
481
|
+
},
|
|
482
|
+
]
|
|
483
|
+
`);
|
|
484
|
+
});
|
|
485
|
+
it("should handle null returned from getResource", async () => {
|
|
486
|
+
const mockGetResource = jest.fn().mockResolvedValue(null);
|
|
487
|
+
const mockListResources = jest.fn().mockResolvedValue([]);
|
|
488
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
489
|
+
wrapper: createWrapper({
|
|
490
|
+
listResources: mockListResources,
|
|
491
|
+
getResource: mockGetResource,
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
(0, react_1.act)(() => {
|
|
495
|
+
result.current.setValue("Check @registry:file:///unknown.txt");
|
|
496
|
+
});
|
|
497
|
+
await (0, react_1.act)(async () => {
|
|
498
|
+
await result.current.submit();
|
|
499
|
+
});
|
|
500
|
+
expect(mockGetResource).toHaveBeenCalled();
|
|
501
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
502
|
+
const content = options.content;
|
|
503
|
+
// Resource should be present but without content
|
|
504
|
+
expect(content).toMatchInlineSnapshot(`
|
|
505
|
+
[
|
|
506
|
+
{
|
|
507
|
+
"text": "Check ",
|
|
508
|
+
"type": "text",
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
"resource": {
|
|
512
|
+
"uri": "file:///unknown.txt",
|
|
513
|
+
},
|
|
514
|
+
"type": "resource",
|
|
515
|
+
},
|
|
516
|
+
]
|
|
517
|
+
`);
|
|
518
|
+
});
|
|
519
|
+
it("should warn when no resourceSource is available for registry resource", async () => {
|
|
520
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
521
|
+
// No listResources/getResource provided
|
|
522
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
523
|
+
wrapper: createWrapper(),
|
|
524
|
+
});
|
|
525
|
+
(0, react_1.act)(() => {
|
|
526
|
+
result.current.setValue("Check @registry:file:///doc.txt");
|
|
527
|
+
});
|
|
528
|
+
await (0, react_1.act)(async () => {
|
|
529
|
+
await result.current.submit();
|
|
530
|
+
});
|
|
531
|
+
expect(consoleSpy).toHaveBeenCalledWith("No resource source available to resolve registry resource: registry:file:///doc.txt");
|
|
532
|
+
consoleSpy.mockRestore();
|
|
533
|
+
});
|
|
534
|
+
it("should resolve static resources passed via resources prop", async () => {
|
|
535
|
+
// When using static resources, we need both listResources and getResource
|
|
536
|
+
// to form the resourceSource, even though the resources are static
|
|
537
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
538
|
+
contents: [
|
|
539
|
+
{
|
|
540
|
+
uri: "static://my-static-resource",
|
|
541
|
+
text: "Static resource content",
|
|
542
|
+
mimeType: "text/plain",
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
});
|
|
546
|
+
const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_input_provider_1.useTamboThreadInput)(), {
|
|
547
|
+
wrapper: createWrapper({
|
|
548
|
+
resources: [
|
|
549
|
+
{
|
|
550
|
+
uri: "static://my-static-resource",
|
|
551
|
+
name: "My Static Resource",
|
|
552
|
+
mimeType: "text/plain",
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
// Need getResource to actually fetch the content
|
|
556
|
+
getResource: mockGetResource,
|
|
557
|
+
listResources: async () => [],
|
|
558
|
+
}),
|
|
559
|
+
});
|
|
560
|
+
(0, react_1.act)(() => {
|
|
561
|
+
result.current.setValue("Show me @registry:static://my-static-resource please");
|
|
562
|
+
});
|
|
563
|
+
await (0, react_1.act)(async () => {
|
|
564
|
+
await result.current.submit();
|
|
565
|
+
});
|
|
566
|
+
// getResource should be called to fetch the content
|
|
567
|
+
expect(mockGetResource).toHaveBeenCalledWith("static://my-static-resource");
|
|
568
|
+
const [, options] = mockSendThreadMessage.mock.calls[0];
|
|
569
|
+
const content = options.content;
|
|
570
|
+
expect(content).toMatchInlineSnapshot(`
|
|
571
|
+
[
|
|
572
|
+
{
|
|
573
|
+
"text": "Show me ",
|
|
574
|
+
"type": "text",
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
"resource": {
|
|
578
|
+
"mimeType": "text/plain",
|
|
579
|
+
"text": "Static resource content",
|
|
580
|
+
"uri": "static://my-static-resource",
|
|
581
|
+
},
|
|
582
|
+
"type": "resource",
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
"text": " please",
|
|
586
|
+
"type": "text",
|
|
587
|
+
},
|
|
588
|
+
]
|
|
589
|
+
`);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
//# sourceMappingURL=thread-input-resource-resolution.test.js.map
|