diff --git a/apps/sim/blocks/blocks/azure_devops.test.ts b/apps/sim/blocks/blocks/azure_devops.test.ts new file mode 100644 index 00000000000..51ea85ded52 --- /dev/null +++ b/apps/sim/blocks/blocks/azure_devops.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest' +import { AzureDevOpsBlock } from '@/blocks/blocks/azure_devops' +import type { BlockConfig } from '@/blocks/types' + +/** The block's `tools.config.params` mapper, which every block under test defines. */ +function requireParamsMapper(block: BlockConfig) { + const mapper = block.tools.config?.params + if (!mapper) throw new Error(`${block.type} has no params mapper`) + return mapper +} + +/** + * On an agent call the model supplies `continuationToken`/`top`/`iterationPath`/`format` under the + * tool's own names; the per-operation fields (`pipelinesContinuationToken`, `queryTop`, …) are never + * rendered in the agent tool input. The handler forwards `{ ...inputs, ...mapped }`. + */ +describe('AzureDevOpsBlock agent tool calls', () => { + const buildParams = requireParamsMapper(AzureDevOpsBlock) + const agentCall = (operation: string, args: Record): Record => { + const inputs = { ...args, operation, organization: 'org', project: 'proj' } + return { ...inputs, ...buildParams(inputs, { operation }) } + } + + it.each(['azure_devops_list_pipelines', 'azure_devops_get_comments'])( + 'keeps the model continuationToken on %s', + (operation) => { + expect( + agentCall(operation, { workItemId: 1, continuationToken: 'ct' }).continuationToken + ).toBe('ct') + } + ) + + it('keeps the model top on azure_devops_query_work_items', () => { + expect(agentCall('azure_devops_query_work_items', { wiqlQuery: 'q', top: 5 }).top).toBe(5) + }) + + it('keeps the model iterationPath on azure_devops_update_work_item', () => { + const mapped = agentCall('azure_devops_update_work_item', { + workItemId: 1, + iterationPath: 'proj\\Sprint 2', + }) + expect(mapped.iterationPath).toBe('proj\\Sprint 2') + }) + + it('keeps the model format on azure_devops_add_comment', () => { + const mapped = agentCall('azure_devops_add_comment', { workItemId: 1, format: 'markdown' }) + expect(mapped.format).toBe('markdown') + }) + + it('drops a List Builds token left over on the canvas under list pipelines', () => { + const inputs = { operation: 'azure_devops_list_pipelines', continuationToken: 'stale' } + expect({ ...inputs, ...buildParams(inputs) }.continuationToken).toBeUndefined() + }) + + it('drops a Create Work Item iteration path left over on the canvas under update', () => { + const inputs = { + operation: 'azure_devops_update_work_item', + workItemId: 1, + iterationPath: 'stale', + } + expect({ ...inputs, ...buildParams(inputs) }.iterationPath).toBeUndefined() + }) +}) diff --git a/apps/sim/blocks/blocks/azure_devops.ts b/apps/sim/blocks/blocks/azure_devops.ts index 6ef93a310c0..ae89e9aa189 100644 --- a/apps/sim/blocks/blocks/azure_devops.ts +++ b/apps/sim/blocks/blocks/azure_devops.ts @@ -718,7 +718,7 @@ export const AzureDevOpsBlock: BlockConfig = { ], config: { tool: (params) => params.operation as string, - params: (params) => { + params: (params, context) => { const base = { accessToken: params.accessToken as string, organization: params.organization as string, @@ -729,7 +729,10 @@ export const AzureDevOpsBlock: BlockConfig = { return { ...base, top: resolveTop(params.pipelinesTop, params.top), - continuationToken: (params.pipelinesContinuationToken as string) || undefined, + continuationToken: + ((params.pipelinesContinuationToken || + (context?.operation ? params.continuationToken : undefined)) as string) || + undefined, } case 'azure_devops_get_pipeline': return { ...base, pipelineId: Number(params.pipelineId) } @@ -774,7 +777,7 @@ export const AzureDevOpsBlock: BlockConfig = { return { ...base, wiqlQuery: params.wiqlQuery as string, - top: params.queryTop ? Number(params.queryTop) : undefined, + top: resolveTop(params.queryTop, context?.operation ? params.top : undefined), } case 'azure_devops_get_work_item': return { ...base, workItemId: Number(params.workItemId) } @@ -814,7 +817,9 @@ export const AzureDevOpsBlock: BlockConfig = { completedWork: params.completedWork ? Number(params.completedWork) : undefined, description: (params.description as string) || undefined, areaPath: (params.areaPath as string) || undefined, - iterationPath: (params.updateIterationPath as string) || undefined, + iterationPath: + ((params.updateIterationPath || + (context?.operation ? params.iterationPath : undefined)) as string) || undefined, tags: (params.tags as string) || undefined, } case 'azure_devops_add_comment': @@ -822,14 +827,19 @@ export const AzureDevOpsBlock: BlockConfig = { ...base, workItemId: Number(params.workItemId), text: params.commentText as string, - format: (params.commentFormat as string) || undefined, + format: + ((params.commentFormat || + (context?.operation ? params.format : undefined)) as string) || undefined, } case 'azure_devops_get_comments': return { ...base, workItemId: Number(params.workItemId), top: resolveTop(params.commentsTop, params.top), - continuationToken: (params.commentsContinuationToken as string) || undefined, + continuationToken: + ((params.commentsContinuationToken || + (context?.operation ? params.continuationToken : undefined)) as string) || + undefined, } default: return base diff --git a/apps/sim/blocks/blocks/gmail.test.ts b/apps/sim/blocks/blocks/gmail.test.ts new file mode 100644 index 00000000000..615d4ae1f15 --- /dev/null +++ b/apps/sim/blocks/blocks/gmail.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import { GmailV2Block } from '@/blocks/blocks/gmail' +import type { BlockConfig } from '@/blocks/types' + +/** The block's `tools.config.params` mapper, which every block under test defines. */ +function requireParamsMapper(block: BlockConfig) { + const mapper = block.tools.config?.params + if (!mapper) throw new Error(`${block.type} has no params mapper`) + return mapper +} + +/** + * On an agent call the model supplies `threadId`/`format`/`name`/`query`/`labelIds` under the + * tool's own names; the dedicated V2 fields (`targetThreadId`, `threadFormat`, `labelName`, + * `listQuery`, `threadLabelIds`) are never rendered in the agent tool input. + */ +describe('GmailV2Block agent tool calls', () => { + const buildParams = requireParamsMapper(GmailV2Block) + const agentCall = (operation: string, args: Record): Record => { + const inputs = { ...args, operation } + return { ...inputs, ...buildParams(inputs, { operation }) } + } + + it('keeps the model threadId and format on get_thread_gmail', () => { + const mapped = agentCall('get_thread_gmail', { threadId: 't-1', format: 'minimal' }) + expect(mapped.threadId).toBe('t-1') + expect(mapped.format).toBe('minimal') + }) + + it.each(['create_label_gmail', 'update_label_gmail'])( + 'keeps the model name on %s', + (operation) => { + expect(agentCall(operation, { labelId: 'l-1', name: 'Receipts' }).name).toBe('Receipts') + } + ) + + it.each(['list_drafts_gmail', 'list_threads_gmail'])( + 'keeps the model query and labelIds on %s', + (operation) => { + const mapped = agentCall(operation, { query: 'is:unread', labelIds: 'INBOX' }) + expect(mapped.query).toBe('is:unread') + expect(mapped.labelIds).toBe('INBOX') + } + ) + + it('drops a Send threadId left over on the canvas under get_thread_gmail', () => { + const inputs = { operation: 'get_thread_gmail', threadId: 'stale' } + expect({ ...inputs, ...buildParams(inputs) }.threadId).toBeUndefined() + }) +}) diff --git a/apps/sim/blocks/blocks/gmail.ts b/apps/sim/blocks/blocks/gmail.ts index 924929e8fb0..02b13b64735 100644 --- a/apps/sim/blocks/blocks/gmail.ts +++ b/apps/sim/blocks/blocks/gmail.ts @@ -969,8 +969,10 @@ export const GmailV2Block: BlockConfig = { suffix: '_v2', fallbackToolId: 'gmail_send_v2', }), - params: (params) => { - const mapped = GmailBlock.tools?.config?.params?.(params) ?? params + params: (params, context) => { + const mapped = GmailBlock.tools?.config?.params?.(params, context) ?? params + /** Agent calls carry the model's tool-named args; on the canvas those keys may be stale. */ + const agentArg = (value: unknown) => (context?.operation ? value : undefined) const { labelName, targetThreadId, @@ -982,17 +984,18 @@ export const GmailV2Block: BlockConfig = { ...rest } = mapped if (LABEL_WRITE_OPERATIONS.includes(rest.operation)) { - rest.name = labelName || undefined + rest.name = labelName || agentArg(rest.name) || undefined rest.messageListVisibility = messageListVisibility || undefined rest.labelListVisibility = labelListVisibility || undefined } if (THREAD_TARGET_OPERATIONS.includes(rest.operation)) { - rest.threadId = targetThreadId - rest.format = threadFormat || undefined + rest.threadId = context?.operation ? targetThreadId || rest.threadId : targetThreadId + rest.format = threadFormat || agentArg(rest.format) || undefined } if (LIST_OPERATIONS.includes(rest.operation)) { - rest.query = listQuery || undefined - rest.labelIds = threadLabelIds ? String(threadLabelIds).trim() : undefined + rest.query = listQuery || agentArg(rest.query) || undefined + const labelIds = threadLabelIds || agentArg(rest.labelIds) + rest.labelIds = labelIds ? String(labelIds).trim() : undefined } return rest }, diff --git a/apps/sim/blocks/blocks/google_calendar.test.ts b/apps/sim/blocks/blocks/google_calendar.test.ts new file mode 100644 index 00000000000..033f526f7a8 --- /dev/null +++ b/apps/sim/blocks/blocks/google_calendar.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { GoogleCalendarBlock } from '@/blocks/blocks/google_calendar' +import type { BlockConfig } from '@/blocks/types' + +/** The block's `tools.config.params` mapper, which every block under test defines. */ +function requireParamsMapper(block: BlockConfig) { + const mapper = block.tools.config?.params + if (!mapper) throw new Error(`${block.type} has no params mapper`) + return mapper +} + +/** + * On an agent call the model supplies `timeZone` under the tool's own name; the dedicated + * `listTimeZone` field is never rendered in the agent tool input. + */ +describe('GoogleCalendarBlock list timeZone', () => { + const buildParams = requireParamsMapper(GoogleCalendarBlock) + + it('keeps the model timeZone on an agent list call', () => { + const inputs = { operation: 'list', timeZone: 'America/New_York' } + const mapped = buildParams(inputs, { operation: 'list' }) + expect({ ...inputs, ...mapped }.timeZone).toBe('America/New_York') + }) + + it('drops a Create/Update timeZone left over on the canvas under list', () => { + const inputs = { operation: 'list', timeZone: 'America/New_York' } + expect({ ...inputs, ...buildParams(inputs) }.timeZone).toBeUndefined() + }) +}) diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index c3ff09602b3..806fe14f46b 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -991,7 +991,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, throw new Error(`Invalid Google Calendar operation: ${params.operation}`) } }, - params: (params) => { + params: (params, context) => { const { oauthCredential, operation, @@ -1075,7 +1075,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, processedParams.timeZone = typeof listTimeZone === 'string' && listTimeZone.trim() ? listTimeZone.trim() - : undefined + : context?.operation + ? processedParams.timeZone + : undefined } return { diff --git a/apps/sim/blocks/blocks/google_drive.test.ts b/apps/sim/blocks/blocks/google_drive.test.ts index b3b10a362e7..000389f90ca 100644 --- a/apps/sim/blocks/blocks/google_drive.test.ts +++ b/apps/sim/blocks/blocks/google_drive.test.ts @@ -1,10 +1,18 @@ import { triggersMock } from '@sim/testing/mocks/triggers.mock' import { describe, expect, it, vi } from 'vitest' +import type { BlockConfig } from '@/blocks/types' vi.mock('@/triggers', () => triggersMock) import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' +/** The block's `tools.config.params` mapper, which every block under test defines. */ +function requireParamsMapper(block: BlockConfig) { + const mapper = block.tools.config?.params + if (!mapper) throw new Error(`${block.type} has no params mapper`) + return mapper +} + const paginationCases = [ { operation: 'list', subBlockId: 'pageToken' }, { operation: 'search', subBlockId: 'searchPageToken' }, @@ -14,7 +22,7 @@ const paginationCases = [ ] as const describe('GoogleDriveBlock pagination', () => { - const buildParams = GoogleDriveBlock.tools.config.params! + const buildParams = requireParamsMapper(GoogleDriveBlock) describe.each(paginationCases)('$operation', ({ operation, subBlockId }) => { /** @@ -70,3 +78,32 @@ describe('GoogleDriveBlock pagination', () => { expect(params.searchPageToken).toBeUndefined() }) }) + +/** + * On an agent call the model supplies `pageSize`/`pageToken`/`query` under the tool's own names; + * the per-operation fields (`permissionsPageSize`, `searchPageToken`, `searchQuery`) are never rendered. + */ +describe('GoogleDriveBlock agent tool calls', () => { + const buildParams = requireParamsMapper(GoogleDriveBlock) + const agentCall = (operation: string, args: Record): Record => { + const inputs = { ...args, operation } + return { ...inputs, ...buildParams(inputs, { operation }) } + } + + it.each(['search', 'list_permissions', 'list_revisions', 'list_comments'])( + 'keeps the model pageSize and pageToken on %s', + (operation) => { + const mapped = agentCall(operation, { fileId: 'f-1', pageSize: 5, pageToken: 'next' }) + expect(mapped.pageSize).toBe(5) + expect(mapped.pageToken).toBe('next') + } + ) + + it('keeps the model query on search', () => { + expect(agentCall('search', { query: 'budget' }).query).toBe('budget') + }) + + it('drops a list page token left over on the canvas under search', () => { + expect(buildParams({ operation: 'search', pageToken: 'stale' }).pageToken).toBeUndefined() + }) +}) diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 183c02dcb2d..0d10ba8aaf6 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -1530,7 +1530,7 @@ Return ONLY the comment text - no explanations, no quotes, no extra formatting.` throw new Error(`Invalid Google Drive operation: ${params.operation}`) } }, - params: (params) => { + params: (params, context) => { const { oauthCredential, uploadFolderId, @@ -1685,19 +1685,33 @@ Return ONLY the comment text - no explanations, no quotes, no extra formatting.` includeDeleted === 'true' ? true : includeDeleted === 'false' ? false : undefined let effectivePageSize: string | undefined = pageSize - if (params.operation === 'search') effectivePageSize = searchPageSize - else if (params.operation === 'list_revisions') effectivePageSize = revisionsPageSize - else if (params.operation === 'list_comments') effectivePageSize = commentsPageSize - else if (params.operation === 'list_permissions') effectivePageSize = permissionsPageSize + // Basic `pageSize`/`query` and advanced `pageToken` belong to List; only an agent call's value is trusted for other operations + const agentPageSize = context?.operation ? pageSize : undefined + if (params.operation === 'search') effectivePageSize = searchPageSize || agentPageSize + else if (params.operation === 'list_revisions') + effectivePageSize = revisionsPageSize || agentPageSize + else if (params.operation === 'list_comments') + effectivePageSize = commentsPageSize || agentPageSize + else if (params.operation === 'list_permissions') + effectivePageSize = permissionsPageSize || agentPageSize + const agentPageToken = context?.operation ? pageToken : undefined let effectivePageToken: string | undefined = pageToken - if (params.operation === 'search') effectivePageToken = searchPageToken - else if (params.operation === 'list_permissions') effectivePageToken = permissionsPageToken - else if (params.operation === 'list_revisions') effectivePageToken = revisionsPageToken - else if (params.operation === 'list_comments') effectivePageToken = commentsPageToken + if (params.operation === 'search') effectivePageToken = searchPageToken || agentPageToken + else if (params.operation === 'list_permissions') + effectivePageToken = permissionsPageToken || agentPageToken + else if (params.operation === 'list_revisions') + effectivePageToken = revisionsPageToken || agentPageToken + else if (params.operation === 'list_comments') + effectivePageToken = commentsPageToken || agentPageToken else if (params.operation !== 'list') effectivePageToken = undefined - const effectiveQuery = params.operation === 'search' ? searchQuery : query + const effectiveQuery = + params.operation === 'search' + ? context?.operation && !searchQuery + ? query + : searchQuery + : query const effectiveOrderBy = params.operation === 'list' diff --git a/apps/sim/blocks/blocks/google_groups.test.ts b/apps/sim/blocks/blocks/google_groups.test.ts index 3a218434b93..8889b65fa97 100644 --- a/apps/sim/blocks/blocks/google_groups.test.ts +++ b/apps/sim/blocks/blocks/google_groups.test.ts @@ -44,3 +44,11 @@ describe('GoogleGroupsBlock update settings', () => { expect(mapped.whoCanJoin).toBe('INVITED_CAN_JOIN') }) }) + +describe('GoogleGroupsBlock agent tool calls', () => { + it('keeps the model orderBy on list_groups when no sort order is chosen', () => { + const inputs = { operation: 'list_groups', customer: 'my_customer', orderBy: 'email' } + const mapped = GoogleGroupsBlock.tools.config?.params?.(inputs, { operation: 'list_groups' }) + expect({ ...inputs, ...mapped }.orderBy).toBe('email') + }) +}) diff --git a/apps/sim/blocks/blocks/google_groups.ts b/apps/sim/blocks/blocks/google_groups.ts index 4b94aa69d7c..a526b6dbef1 100644 --- a/apps/sim/blocks/blocks/google_groups.ts +++ b/apps/sim/blocks/blocks/google_groups.ts @@ -726,7 +726,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, throw new Error(`Invalid Google Groups operation: ${params.operation}`) } }, - params: (params) => { + params: (params, context) => { const { oauthCredential, operation, ...rest } = params switch (operation) { @@ -739,7 +739,9 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, maxResults: rest.maxResults ? Number(rest.maxResults) : undefined, pageToken: rest.pageToken || undefined, userKey: rest.userKey || undefined, - orderBy: rest.sortOrder ? 'email' : undefined, + orderBy: + (context?.operation ? rest.orderBy : undefined) || + (rest.sortOrder ? 'email' : undefined), sortOrder: rest.sortOrder || undefined, } case 'get_group': diff --git a/apps/sim/blocks/blocks/google_vault.test.ts b/apps/sim/blocks/blocks/google_vault.test.ts new file mode 100644 index 00000000000..4bb1cc038c6 --- /dev/null +++ b/apps/sim/blocks/blocks/google_vault.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { GoogleVaultBlock } from '@/blocks/blocks/google_vault' +import type { BlockConfig } from '@/blocks/types' + +/** The block's `tools.config.params` mapper, which every block under test defines. */ +function requireParamsMapper(block: BlockConfig) { + const mapper = block.tools.config?.params + if (!mapper) throw new Error(`${block.type} has no params mapper`) + return mapper +} + +/** + * On an agent call the model supplies `includeSharedDrives` under the tool's own name; the + * dedicated `exportIncludeSharedDrives` switch is never rendered in the agent tool input. + */ +describe('GoogleVaultBlock includeSharedDrives', () => { + const buildParams = requireParamsMapper(GoogleVaultBlock) + + it.each(['create_matters_export', 'create_saved_query'])( + 'keeps the model value on an agent %s call', + (operation) => { + const inputs = { operation, matterId: 'm-1', corpus: 'DRIVE', includeSharedDrives: true } + expect(buildParams(inputs, { operation }).includeSharedDrives).toBe(true) + } + ) + + it('still sends false on the canvas for an unset switch', () => { + const inputs = { + operation: 'create_matters_export', + corpus: 'DRIVE', + includeSharedDrives: true, + } + expect(buildParams(inputs).includeSharedDrives).toBe(false) + }) +}) diff --git a/apps/sim/blocks/blocks/google_vault.ts b/apps/sim/blocks/blocks/google_vault.ts index ea4c05148fe..c35e7b7d576 100644 --- a/apps/sim/blocks/blocks/google_vault.ts +++ b/apps/sim/blocks/blocks/google_vault.ts @@ -3,7 +3,7 @@ import { GoogleVaultIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' -import { SERVICE_ACCOUNT_SUBBLOCKS } from '@/blocks/utils' +import { parseOptionalBooleanInput, SERVICE_ACCOUNT_SUBBLOCKS } from '@/blocks/utils' export const GoogleVaultBlock: BlockConfig = { type: 'google_vault', @@ -906,7 +906,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, throw new Error(`Invalid Google Vault operation: ${params.operation}`) } }, - params: (params) => { + params: (params, context) => { const { oauthCredential, holdStartTime, @@ -958,7 +958,9 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, ...(params.operation === 'list_matters' && matterState && { state: matterState }), ...(isQueryOperation && { includeSharedDrives: - exportIncludeSharedDrives === true || exportIncludeSharedDrives === 'true', + context?.operation && exportIncludeSharedDrives == null + ? (parseOptionalBooleanInput(rest.includeSharedDrives) ?? false) + : exportIncludeSharedDrives === true || exportIncludeSharedDrives === 'true', ...(dataScope && { dataScope }), }), ...(params.operation === 'create_matters_export' && exportFormat && { exportFormat }), diff --git a/apps/sim/blocks/blocks/microsoft_ad.test.ts b/apps/sim/blocks/blocks/microsoft_ad.test.ts index 88265e4736b..f78e3b0fc93 100644 --- a/apps/sim/blocks/blocks/microsoft_ad.test.ts +++ b/apps/sim/blocks/blocks/microsoft_ad.test.ts @@ -1,5 +1,13 @@ import { describe, expect, it } from 'vitest' import { MicrosoftAdBlock } from '@/blocks/blocks/microsoft_ad' +import type { BlockConfig } from '@/blocks/types' + +/** The block's `tools.config.params` mapper, which every block under test defines. */ +function requireParamsMapper(block: BlockConfig) { + const mapper = block.tools.config?.params + if (!mapper) throw new Error(`${block.type} has no params mapper`) + return mapper +} /** * The tri-state assertions run against `{ ...inputs, ...buildParams(inputs) }`, the shape the @@ -8,7 +16,7 @@ import { MicrosoftAdBlock } from '@/blocks/blocks/microsoft_ad' * pass against the broken code. */ describe('MicrosoftAdBlock', () => { - const buildParams = MicrosoftAdBlock.tools.config.params! + const buildParams = requireParamsMapper(MicrosoftAdBlock) const subBlock = (id: string) => MicrosoftAdBlock.subBlocks.find((candidate) => candidate.id === id)! @@ -73,3 +81,36 @@ describe('MicrosoftAdBlock', () => { }) }) }) + +/** + * On an agent call the model supplies `filter`/`search`/`transitive` under the tool's own names; + * the per-operation fields (`signInFilter`, `deviceSearch`, `membershipScope`) are never rendered. + */ +describe('MicrosoftAdBlock agent tool calls', () => { + const buildParams = requireParamsMapper(MicrosoftAdBlock) + const agentCall = (operation: string, args: Record): Record => { + const inputs = { ...args, operation } + return { ...inputs, ...buildParams(inputs, { operation }) } + } + + it('keeps the model filter on an operation with a dedicated filter field', () => { + const mapped = agentCall('list_sign_ins', { filter: 'status/errorCode eq 0' }) + expect(mapped.filter).toBe('status/errorCode eq 0') + }) + + it('keeps the model search on an operation with a dedicated search field', () => { + const mapped = agentCall('list_devices', { search: '"displayName:laptop"' }) + expect(mapped.search).toBe('"displayName:laptop"') + }) + + it('keeps the model transitive flag when no membership scope is chosen', () => { + expect(agentCall('list_user_memberships', { userId: 'u-1', transitive: true }).transitive).toBe( + true + ) + }) + + it('drops a shared filter left over on the canvas for an operation with its own field', () => { + const inputs = { operation: 'list_sign_ins', filter: 'stale' } + expect({ ...inputs, ...buildParams(inputs) }.filter).toBeUndefined() + }) +}) diff --git a/apps/sim/blocks/blocks/microsoft_ad.ts b/apps/sim/blocks/blocks/microsoft_ad.ts index 26efed35581..3c15a07d583 100644 --- a/apps/sim/blocks/blocks/microsoft_ad.ts +++ b/apps/sim/blocks/blocks/microsoft_ad.ts @@ -2,6 +2,7 @@ import { AzureIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { parseOptionalBooleanInput } from '@/blocks/utils' /** Operations that act on a single user and therefore require the User ID field. */ const USER_ID_OPERATIONS = [ @@ -854,19 +855,29 @@ export const MicrosoftAdBlock: BlockConfig = { ], config: { tool: (params) => `microsoft_ad_${params.operation}`, - params: (params) => { + params: (params, context) => { const result: Record = {} const top = Number(params.top) result.top = params.top && Number.isFinite(top) ? top : undefined if (params.nextLink) result.nextLink = params.nextLink const values = params as Record - result.filter = values[FILTER_FIELD_BY_OPERATION[params.operation]] || undefined - result.search = values[SEARCH_FIELD_BY_OPERATION[params.operation]] || undefined + // Agent calls carry the model's `filter`/`search`; on the canvas they may be stale advanced fields + const agentArg = (value: unknown) => (context?.operation ? value : undefined) + result.filter = + values[FILTER_FIELD_BY_OPERATION[params.operation]] || + agentArg(params.filter) || + undefined + result.search = + values[SEARCH_FIELD_BY_OPERATION[params.operation]] || + agentArg(params.search) || + undefined if (params.operation === 'get_user') { result.includeManager = params.includeManager === true || params.includeManager === 'true' } if (params.operation === 'list_user_memberships') { - result.transitive = params.membershipScope === 'transitive' + result.transitive = params.membershipScope + ? params.membershipScope === 'transitive' + : (parseOptionalBooleanInput(agentArg(params.transitive)) ?? false) } if (params.operation === 'set_password') { result.forceChangePasswordNextSignIn = params.forceChangePasswordNextSignIn !== 'false' diff --git a/apps/sim/blocks/blocks/outlook.test.ts b/apps/sim/blocks/blocks/outlook.test.ts index d9e7b52d457..a305043d206 100644 --- a/apps/sim/blocks/blocks/outlook.test.ts +++ b/apps/sim/blocks/blocks/outlook.test.ts @@ -39,14 +39,14 @@ const CALENDAR_SUBBLOCK_VALUES: Record = { describe('OutlookBlock calendar operations', () => { it('always sends sendResponse so an unset dropdown cannot read as "do not notify"', () => { - const withoutChoice = block.tools.config.params?.({ + const withoutChoice = block.tools.config?.params?.({ operation: 'respond_calendar', ...CALENDAR_SUBBLOCK_VALUES, calSendResponse: undefined, }) expect(withoutChoice?.sendResponse).toBe(true) - const declined = block.tools.config.params?.({ + const declined = block.tools.config?.params?.({ operation: 'respond_calendar', ...CALENDAR_SUBBLOCK_VALUES, calSendResponse: 'false', @@ -94,3 +94,35 @@ describe('OutlookBlock values stored for another operation', () => { } }) }) + +/** + * On an agent call the model supplies `cc`/`bcc`/`importance` under the tool's own names, and the + * dedicated `replyCc`/`replyBcc`/`mailImportance` fields are never rendered in the agent tool input. + */ +describe('OutlookBlock agent tool calls', () => { + const agentCall = (operation: string, args: Record): Record => { + const inputs = { ...args, operation, oauthCredential: 'cred-1' } + return { ...inputs, ...block.tools.config?.params?.(inputs, { operation }) } + } + + it.each(['reply_outlook', 'forward_outlook'])('keeps the model cc and bcc on %s', (operation) => { + const mapped = agentCall(operation, { cc: 'cc@example.com', bcc: 'bcc@example.com' }) + expect(mapped.cc).toBe('cc@example.com') + expect(mapped.bcc).toBe('bcc@example.com') + }) + + it('keeps the model bcc but never a cc on reply_all_outlook', () => { + const mapped = agentCall('reply_all_outlook', { cc: 'cc@example.com', bcc: 'bcc@example.com' }) + expect(mapped.cc).toBeNull() + expect(mapped.bcc).toBe('bcc@example.com') + }) + + it.each(['send_outlook', 'draft_outlook'])('keeps the model importance on %s', (operation) => { + expect(agentCall(operation, { importance: 'high' }).importance).toBe('high') + }) + + it('prefers the dedicated field when the stored tool params carry one', () => { + const mapped = agentCall('reply_outlook', { replyCc: 'stored@example.com', cc: 'model@x.com' }) + expect(mapped.cc).toBe('stored@example.com') + }) +}) diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts index 2f8a5c7d6a0..127d4a47c33 100644 --- a/apps/sim/blocks/blocks/outlook.ts +++ b/apps/sim/blocks/blocks/outlook.ts @@ -949,7 +949,7 @@ Return ONLY the comma-separated email list - no explanations, no extra text.`, throw new Error(`Invalid Outlook operation: ${params.operation}`) } }, - params: (params) => { + params: (params, context) => { const { oauthCredential, folder, @@ -994,6 +994,9 @@ Return ONLY the comma-separated email list - no explanations, no extra text.`, // Agent calls may deliver booleans as the strings "true"/"false" const toBool = (value: unknown): boolean => value === true || value === 'true' + // Agent calls carry the model's tool-named arg; the canvas may hold a stale one from another operation + const agentArg = (value: unknown) => (context?.operation ? value : null) || null + const effectiveFolder = folder ? String(folder).trim() : '' const normalizedAttachments = normalizeFileInput(attachments) @@ -1010,13 +1013,13 @@ Return ONLY the comma-separated email list - no explanations, no extra text.`, } if (['send_outlook', 'draft_outlook'].includes(rest.operation)) { - rest.importance = mailImportance || null + rest.importance = mailImportance || agentArg(rest.importance) } // Assign explicitly so a cc/bcc/importance stored for another operation never reaches these tools if (['reply_outlook', 'reply_all_outlook', 'forward_outlook'].includes(rest.operation)) { - rest.cc = rest.operation === 'reply_all_outlook' ? null : replyCc || null - rest.bcc = replyBcc || null + rest.cc = rest.operation === 'reply_all_outlook' ? null : replyCc || agentArg(rest.cc) + rest.bcc = replyBcc || agentArg(rest.bcc) rest.importance = undefined } diff --git a/scripts/check-explicit-any.baseline.json b/scripts/check-explicit-any.baseline.json index dadc2b5d5a3..fabb54ee1dc 100644 --- a/scripts/check-explicit-any.baseline.json +++ b/scripts/check-explicit-any.baseline.json @@ -1228,7 +1228,6 @@ "apps/sim/blocks/blocks/ashby.test.ts": 11, "apps/sim/blocks/blocks/cbinsights.test.ts": 2, "apps/sim/blocks/blocks/crunchbase.test.ts": 2, - "apps/sim/blocks/blocks/google_drive.test.ts": 1, "apps/sim/blocks/blocks/google_slides.ts": 3, "apps/sim/blocks/blocks/grain.ts": 3, "apps/sim/blocks/blocks/harmonic.test.ts": 2, @@ -1238,7 +1237,7 @@ "apps/sim/blocks/blocks/kalshi.ts": 1, "apps/sim/blocks/blocks/logs.test.ts": 4, "apps/sim/blocks/blocks/mem0.test.ts": 1, - "apps/sim/blocks/blocks/microsoft_ad.test.ts": 2, + "apps/sim/blocks/blocks/microsoft_ad.test.ts": 1, "apps/sim/blocks/blocks/mintlify.test.ts": 1, "apps/sim/blocks/blocks/modal.test.ts": 2, "apps/sim/blocks/blocks/mssql.test.ts": 1, diff --git a/scripts/check-test-types.baseline.json b/scripts/check-test-types.baseline.json index 94d455e53de..b4e50071554 100644 --- a/scripts/check-test-types.baseline.json +++ b/scripts/check-test-types.baseline.json @@ -648,9 +648,6 @@ "apps/sim/blocks/blocks/gitlab.test.ts": { "TS18048": 7 }, - "apps/sim/blocks/blocks/google_drive.test.ts": { - "TS18048": 1 - }, "apps/sim/blocks/blocks/instagram.test.ts": { "TS18048": 1 }, @@ -661,9 +658,6 @@ "apps/sim/blocks/blocks/mem0.test.ts": { "TS18048": 1 }, - "apps/sim/blocks/blocks/microsoft_ad.test.ts": { - "TS18048": 1 - }, "apps/sim/blocks/blocks/mintlify.test.ts": { "TS18048": 1 }, @@ -671,9 +665,6 @@ "TS18048": 1, "TS2339": 2 }, - "apps/sim/blocks/blocks/outlook.test.ts": { - "TS18048": 2 - }, "apps/sim/blocks/blocks/slack.test.ts": { "TS2339": 1 },