Skip to content

Commit c65cc33

Browse files
committed
improvement(assistant): streamline account checks and bound catalog caching
1 parent b0857a3 commit c65cc33

19 files changed

Lines changed: 350 additions & 226 deletions

apps/sim/app/api/mothership/execute/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
236236
workspaceAccess,
237237
secretMountPolicy,
238238
}),
239-
buildIntegrationToolSchemas(userId, messageId, undefined, workspaceId),
239+
buildIntegrationToolSchemas(userId, undefined, workspaceId),
240240
mothershipToolsPromise,
241241
computeWorkspaceEntitlements(workspaceId, userId),
242242
processContextsServer(

apps/sim/app/api/v2/chat/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ export const POST = withRouteHandler(
326326
const [workspaceContext, integrationTools, entitlements, billingAttribution] =
327327
await Promise.all([
328328
generateWorkspaceContext(workspaceId, userId, { workspaceAccess, secretMountPolicy }),
329-
buildIntegrationToolSchemas(userId, messageId, undefined, workspaceId),
329+
buildIntegrationToolSchemas(userId, undefined, workspaceId),
330330
computeWorkspaceEntitlements(workspaceId, userId),
331331
// Hosted execution refuses to run without an attribution snapshot;
332332
// the executor path receives it as a header, this path resolves it

apps/sim/lib/copilot/chat/payload.test.ts

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,6 @@ describe('buildIntegrationToolSchemas', () => {
189189
expect(gmailTool?.description).toBe('Send emails using Gmail')
190190
})
191191

192-
it('still builds integration tools when subscription lookup fails', async () => {
193-
mockGetHighestPrioritySubscription.mockRejectedValue(new Error('db unavailable'))
194-
195-
const toolSchemas = await buildIntegrationToolSchemas('user-error')
196-
const gmailTool = toolSchemas.find((tool) => tool.name === 'gmail_send')
197-
const brandfetchTool = toolSchemas.find((tool) => tool.name === 'brandfetch_search')
198-
199-
expect(mockGetHighestPrioritySubscription).toHaveBeenCalledWith('user-error')
200-
expect(gmailTool?.description).toBe('Send emails using Gmail')
201-
expect(brandfetchTool?.description).toBe('Search for brands by company name')
202-
})
203-
204192
it('emits executeLocally for dynamic client tools only', async () => {
205193
mockGetHighestPrioritySubscription.mockResolvedValue({ plan: 'pro', status: 'active' })
206194

@@ -264,7 +252,6 @@ describe('buildIntegrationToolSchemas', () => {
264252

265253
const toolSchemas = await buildIntegrationToolSchemas(
266254
'user-intersection',
267-
undefined,
268255
{ schemaSurface: 'copilot' },
269256
'workspace-1'
270257
)
@@ -292,7 +279,6 @@ describe('buildIntegrationToolSchemas', () => {
292279
await expect(
293280
buildIntegrationToolSchemas(
294281
'user-permission-error',
295-
undefined,
296282
{ schemaSurface: 'copilot' },
297283
'workspace-1'
298284
)
@@ -314,13 +300,72 @@ describe('buildIntegrationToolSchemas', () => {
314300
expect(second[0].outputs).not.toHaveProperty('mutated')
315301
})
316302

303+
it('isolates nested schemas and required fields between requests', async () => {
304+
mockCreateUserToolSchema.mockReturnValueOnce({
305+
type: 'object',
306+
properties: { recipients: { type: 'array', items: { type: 'string' } } },
307+
required: ['recipients'],
308+
})
309+
const first = await buildIntegrationToolSchemas('user-nested-schema')
310+
const properties = first[0].input_schema.properties as Record<string, unknown>
311+
properties.recipients = { type: 'number' }
312+
;(first[0].input_schema.required as string[]).push('forged')
313+
314+
const second = await buildIntegrationToolSchemas('user-nested-schema')
315+
expect(second[0].input_schema.properties).toEqual({
316+
recipients: { type: 'array', items: { type: 'string' } },
317+
})
318+
expect(second[0].input_schema.required).toEqual(['recipients'])
319+
})
320+
321+
it('coalesces simultaneous catalog builds', async () => {
322+
const catalogs = await Promise.all(
323+
Array.from({ length: 20 }, () => buildIntegrationToolSchemas('concurrent-user'))
324+
)
325+
expect(mockGetHighestPrioritySubscription).toHaveBeenCalledTimes(1)
326+
expect(mockCreateUserToolSchema).toHaveBeenCalledTimes(3)
327+
expect(catalogs.every((catalog) => catalog.length === 3)).toBe(true)
328+
})
329+
330+
it('propagates schema failures without caching a partial catalog', async () => {
331+
mockCreateUserToolSchema.mockImplementationOnce(() => {
332+
throw new Error('invalid tool schema')
333+
})
334+
await expect(buildIntegrationToolSchemas('schema-failure')).rejects.toThrow(
335+
'invalid tool schema'
336+
)
337+
const recovered = await buildIntegrationToolSchemas('schema-failure')
338+
expect(recovered).toHaveLength(3)
339+
expect(mockGetHighestPrioritySubscription).toHaveBeenCalledTimes(2)
340+
})
341+
342+
it('propagates subscription failures without caching a guessed catalog', async () => {
343+
mockGetHighestPrioritySubscription.mockRejectedValueOnce(new Error('billing unavailable'))
344+
await expect(buildIntegrationToolSchemas('subscription-failure')).rejects.toThrow(
345+
'billing unavailable'
346+
)
347+
expect(mockCreateUserToolSchema).not.toHaveBeenCalled()
348+
expect(await buildIntegrationToolSchemas('subscription-failure')).toHaveLength(3)
349+
})
350+
351+
it('evicts catalogs by their byte size before reaching the entry cap', async () => {
352+
mockCreateUserToolSchema.mockImplementation(() => ({
353+
type: 'object',
354+
properties: { large: { type: 'string', description: 'x'.repeat(1024 * 1024) } },
355+
}))
356+
for (let i = 0; i < 12; i++) await buildIntegrationToolSchemas(`sized-user-${i}`)
357+
expect(mockGetHighestPrioritySubscription).toHaveBeenCalledTimes(12)
358+
await buildIntegrationToolSchemas('sized-user-0')
359+
expect(mockGetHighestPrioritySubscription).toHaveBeenCalledTimes(13)
360+
mockCreateUserToolSchema.mockImplementation(() => ({ type: 'object', properties: {} }))
361+
})
362+
317363
it('rebuilds instead of serving a cache entry from the previous policy', async () => {
318364
mockGetHighestPrioritySubscription.mockResolvedValue({ plan: 'pro', status: 'active' })
319365
mockGetUserPermissionConfig.mockResolvedValue({ allowedIntegrations: null, deniedTools: [] })
320366

321367
const before = await buildIntegrationToolSchemas(
322368
'user-policy',
323-
undefined,
324369
{ schemaSurface: 'copilot' },
325370
'workspace-policy'
326371
)
@@ -335,7 +380,6 @@ describe('buildIntegrationToolSchemas', () => {
335380

336381
const after = await buildIntegrationToolSchemas(
337382
'user-policy',
338-
undefined,
339383
{ schemaSurface: 'copilot' },
340384
'workspace-policy'
341385
)

0 commit comments

Comments
 (0)