Skip to content

Commit 9fca01f

Browse files
committed
improvement(blocks): share the tool-call fallback helper and tidy the Hex/Stripe validation code
1 parent a944b97 commit 9fca01f

30 files changed

Lines changed: 210 additions & 337 deletions

‎apps/sim/blocks/blocks/azure_devops.test.ts‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import { describe, expect, it } from 'vitest'
22
import { AzureDevOpsBlock } from '@/blocks/blocks/azure_devops'
3-
import type { BlockConfig } from '@/blocks/types'
3+
import type { BlockToolParamsContext } from '@/blocks/types'
44

5-
/** The block's `tools.config.params` mapper, which every block under test defines. */
6-
function requireParamsMapper(block: BlockConfig) {
7-
const mapper = block.tools.config?.params
8-
if (!mapper) throw new Error(`${block.type} has no params mapper`)
9-
return mapper
10-
}
5+
/** The block's `tools.config.params` mapper; `context` is set on a tool call, absent on the canvas. */
6+
const buildParams = (
7+
inputs: Record<string, unknown>,
8+
context?: BlockToolParamsContext
9+
): Record<string, unknown> => AzureDevOpsBlock.tools.config?.params?.(inputs, context) ?? {}
1110

1211
/**
1312
* On an agent call the model supplies `continuationToken`/`top`/`iterationPath`/`format` under the
1413
* tool's own names; the per-operation fields (`pipelinesContinuationToken`, `queryTop`, …) are never
1514
* rendered in the agent tool input. The handler forwards `{ ...inputs, ...mapped }`.
1615
*/
1716
describe('AzureDevOpsBlock agent tool calls', () => {
18-
const buildParams = requireParamsMapper(AzureDevOpsBlock)
1917
const agentCall = (operation: string, args: Record<string, unknown>): Record<string, unknown> => {
2018
const inputs = { ...args, operation, organization: 'org', project: 'proj' }
2119
return { ...inputs, ...buildParams(inputs, { operation }) }

‎apps/sim/blocks/blocks/azure_devops.ts‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AzureIcon } from '@/components/icons'
22
import type { BlockConfig, BlockMeta } from '@/blocks/types'
33
import { AuthMode, IntegrationType } from '@/blocks/types'
4-
import { withSavedCredential } from '@/blocks/utils'
4+
import { toolCallArg, withSavedCredential } from '@/blocks/utils'
55
import type { AzureDevOpsBasicWorkItemType } from '@/tools/azure_devops/types'
66
import { AZURE_DEVOPS_BASIC_WORK_ITEM_STATES } from '@/tools/azure_devops/utils'
77
import { getTrigger } from '@/triggers'
@@ -730,8 +730,8 @@ export const AzureDevOpsBlock: BlockConfig = {
730730
...base,
731731
top: resolveTop(params.pipelinesTop, params.top),
732732
continuationToken:
733-
((params.pipelinesContinuationToken ||
734-
(context?.operation ? params.continuationToken : undefined)) as string) ||
733+
(params.pipelinesContinuationToken as string) ||
734+
toolCallArg(context, params.continuationToken) ||
735735
undefined,
736736
}
737737
case 'azure_devops_get_pipeline':
@@ -777,7 +777,7 @@ export const AzureDevOpsBlock: BlockConfig = {
777777
return {
778778
...base,
779779
wiqlQuery: params.wiqlQuery as string,
780-
top: resolveTop(params.queryTop, context?.operation ? params.top : undefined),
780+
top: resolveTop(params.queryTop, toolCallArg(context, params.top)),
781781
}
782782
case 'azure_devops_get_work_item':
783783
return { ...base, workItemId: Number(params.workItemId) }
@@ -818,8 +818,9 @@ export const AzureDevOpsBlock: BlockConfig = {
818818
description: (params.description as string) || undefined,
819819
areaPath: (params.areaPath as string) || undefined,
820820
iterationPath:
821-
((params.updateIterationPath ||
822-
(context?.operation ? params.iterationPath : undefined)) as string) || undefined,
821+
(params.updateIterationPath as string) ||
822+
toolCallArg(context, params.iterationPath) ||
823+
undefined,
823824
tags: (params.tags as string) || undefined,
824825
}
825826
case 'azure_devops_add_comment':
@@ -828,17 +829,18 @@ export const AzureDevOpsBlock: BlockConfig = {
828829
workItemId: Number(params.workItemId),
829830
text: params.commentText as string,
830831
format:
831-
((params.commentFormat ||
832-
(context?.operation ? params.format : undefined)) as string) || undefined,
832+
(params.commentFormat as string) ||
833+
toolCallArg(context, params.format) ||
834+
undefined,
833835
}
834836
case 'azure_devops_get_comments':
835837
return {
836838
...base,
837839
workItemId: Number(params.workItemId),
838840
top: resolveTop(params.commentsTop, params.top),
839841
continuationToken:
840-
((params.commentsContinuationToken ||
841-
(context?.operation ? params.continuationToken : undefined)) as string) ||
842+
(params.commentsContinuationToken as string) ||
843+
toolCallArg(context, params.continuationToken) ||
842844
undefined,
843845
}
844846
default:

‎apps/sim/blocks/blocks/gmail.test.ts‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import { describe, expect, it } from 'vitest'
22
import { GmailV2Block } from '@/blocks/blocks/gmail'
3-
import type { BlockConfig } from '@/blocks/types'
3+
import type { BlockToolParamsContext } from '@/blocks/types'
44

5-
/** The block's `tools.config.params` mapper, which every block under test defines. */
6-
function requireParamsMapper(block: BlockConfig) {
7-
const mapper = block.tools.config?.params
8-
if (!mapper) throw new Error(`${block.type} has no params mapper`)
9-
return mapper
10-
}
5+
/** The block's `tools.config.params` mapper; `context` is set on a tool call, absent on the canvas. */
6+
const buildParams = (
7+
inputs: Record<string, unknown>,
8+
context?: BlockToolParamsContext
9+
): Record<string, unknown> => GmailV2Block.tools.config?.params?.(inputs, context) ?? {}
1110

1211
/**
1312
* On an agent call the model supplies `threadId`/`format`/`name`/`query`/`labelIds` under the
1413
* tool's own names; the dedicated V2 fields (`targetThreadId`, `threadFormat`, `labelName`,
1514
* `listQuery`, `threadLabelIds`) are never rendered in the agent tool input.
1615
*/
1716
describe('GmailV2Block agent tool calls', () => {
18-
const buildParams = requireParamsMapper(GmailV2Block)
1917
const agentCall = (operation: string, args: Record<string, unknown>): Record<string, unknown> => {
2018
const inputs = { ...args, operation }
2119
return { ...inputs, ...buildParams(inputs, { operation }) }

‎apps/sim/blocks/blocks/gmail.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import type { BlockConfig, BlockMeta } from '@/blocks/types'
55
import { AuthMode, IntegrationType } from '@/blocks/types'
66
import {
77
createVersionedToolSelector,
8+
isToolCall,
89
normalizeFileInput,
910
SERVICE_ACCOUNT_SUBBLOCKS,
11+
toolCallArg,
1012
} from '@/blocks/utils'
1113
import { getTrigger } from '@/triggers'
1214

@@ -971,8 +973,6 @@ export const GmailV2Block: BlockConfig = {
971973
}),
972974
params: (params, context) => {
973975
const mapped = GmailBlock.tools?.config?.params?.(params, context) ?? params
974-
/** Agent calls carry the model's tool-named args; on the canvas those keys may be stale. */
975-
const agentArg = (value: unknown) => (context?.operation ? value : undefined)
976976
const {
977977
labelName,
978978
targetThreadId,
@@ -984,17 +984,17 @@ export const GmailV2Block: BlockConfig = {
984984
...rest
985985
} = mapped
986986
if (LABEL_WRITE_OPERATIONS.includes(rest.operation)) {
987-
rest.name = labelName || agentArg(rest.name) || undefined
987+
rest.name = labelName || toolCallArg(context, rest.name) || undefined
988988
rest.messageListVisibility = messageListVisibility || undefined
989989
rest.labelListVisibility = labelListVisibility || undefined
990990
}
991991
if (THREAD_TARGET_OPERATIONS.includes(rest.operation)) {
992-
rest.threadId = context?.operation ? targetThreadId || rest.threadId : targetThreadId
993-
rest.format = threadFormat || agentArg(rest.format) || undefined
992+
rest.threadId = isToolCall(context) ? targetThreadId || rest.threadId : targetThreadId
993+
rest.format = threadFormat || toolCallArg(context, rest.format) || undefined
994994
}
995995
if (LIST_OPERATIONS.includes(rest.operation)) {
996-
rest.query = listQuery || agentArg(rest.query) || undefined
997-
const labelIds = threadLabelIds || agentArg(rest.labelIds)
996+
rest.query = listQuery || toolCallArg(context, rest.query) || undefined
997+
const labelIds = threadLabelIds || toolCallArg(context, rest.labelIds)
998998
rest.labelIds = labelIds ? String(labelIds).trim() : undefined
999999
}
10001000
return rest

‎apps/sim/blocks/blocks/google_calendar.test.ts‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import { describe, expect, it } from 'vitest'
22
import { GoogleCalendarBlock } from '@/blocks/blocks/google_calendar'
3-
import type { BlockConfig } from '@/blocks/types'
3+
import type { BlockToolParamsContext } from '@/blocks/types'
44

5-
/** The block's `tools.config.params` mapper, which every block under test defines. */
6-
function requireParamsMapper(block: BlockConfig) {
7-
const mapper = block.tools.config?.params
8-
if (!mapper) throw new Error(`${block.type} has no params mapper`)
9-
return mapper
10-
}
5+
/** The block's `tools.config.params` mapper; `context` is set on a tool call, absent on the canvas. */
6+
const buildParams = (
7+
inputs: Record<string, unknown>,
8+
context?: BlockToolParamsContext
9+
): Record<string, unknown> => GoogleCalendarBlock.tools.config?.params?.(inputs, context) ?? {}
1110

1211
/**
1312
* On an agent call the model supplies `timeZone` under the tool's own name; the dedicated
1413
* `listTimeZone` field is never rendered in the agent tool input.
1514
*/
1615
describe('GoogleCalendarBlock list timeZone', () => {
17-
const buildParams = requireParamsMapper(GoogleCalendarBlock)
18-
1916
it('keeps the model timeZone on an agent list call', () => {
2017
const inputs = { operation: 'list', timeZone: 'America/New_York' }
2118
const mapped = buildParams(inputs, { operation: 'list' })

‎apps/sim/blocks/blocks/google_calendar.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { GoogleCalendarIcon } from '@/components/icons'
22
import { getScopesForService } from '@/lib/oauth/utils'
33
import type { BlockConfig, BlockMeta } from '@/blocks/types'
44
import { AuthMode, IntegrationType } from '@/blocks/types'
5-
import { createVersionedToolSelector, SERVICE_ACCOUNT_SUBBLOCKS } from '@/blocks/utils'
5+
import { createVersionedToolSelector, SERVICE_ACCOUNT_SUBBLOCKS, toolCallArg } from '@/blocks/utils'
66
import { getTrigger } from '@/triggers'
77

88
const CALENDAR_FIELD = ['calendarId', 'manualCalendarId'] as const
@@ -1075,9 +1075,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
10751075
processedParams.timeZone =
10761076
typeof listTimeZone === 'string' && listTimeZone.trim()
10771077
? listTimeZone.trim()
1078-
: context?.operation
1079-
? processedParams.timeZone
1080-
: undefined
1078+
: toolCallArg(context, processedParams.timeZone)
10811079
}
10821080

10831081
return {
Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import { triggersMock } from '@sim/testing/mocks/triggers.mock'
22
import { describe, expect, it, vi } from 'vitest'
3-
import type { BlockConfig } from '@/blocks/types'
3+
import type { BlockToolParamsContext } from '@/blocks/types'
44

55
vi.mock('@/triggers', () => triggersMock)
66

77
import { GoogleDriveBlock } from '@/blocks/blocks/google_drive'
88

9-
/** The block's `tools.config.params` mapper, which every block under test defines. */
10-
function requireParamsMapper(block: BlockConfig) {
11-
const mapper = block.tools.config?.params
12-
if (!mapper) throw new Error(`${block.type} has no params mapper`)
13-
return mapper
14-
}
9+
/** The block's `tools.config.params` mapper; `context` is set on a tool call, absent on the canvas. */
10+
const buildParams = (
11+
inputs: Record<string, unknown>,
12+
context?: BlockToolParamsContext
13+
): Record<string, unknown> => GoogleDriveBlock.tools.config?.params?.(inputs, context) ?? {}
1514

1615
const paginationCases = [
1716
{ operation: 'list', subBlockId: 'pageToken' },
@@ -22,26 +21,22 @@ const paginationCases = [
2221
] as const
2322

2423
describe('GoogleDriveBlock pagination', () => {
25-
const buildParams = requireParamsMapper(GoogleDriveBlock)
26-
2724
describe.each(paginationCases)('$operation', ({ operation, subBlockId }) => {
2825
/**
2926
* `pageToken` is the canonical tool param, so the `list` case would forward
3027
* through `...rest` even without the mapper. The per-operation ids are the
3128
* ones the mapper has to translate, and none of them may survive as-is.
3229
*/
3330
it('forwards the page token to the tool under its own id', () => {
34-
const params = buildParams({ operation, [subBlockId]: 'token-abc' }, undefined as never)
31+
const params = buildParams({ operation, [subBlockId]: 'token-abc' })
3532

3633
expect(params).toMatchObject({ pageToken: 'token-abc' })
3734
if (subBlockId !== 'pageToken') expect(params[subBlockId]).toBeUndefined()
3835
})
3936
})
4037

4138
it('does not leak a page token into operations that do not paginate', () => {
42-
expect(
43-
buildParams({ operation: 'get_file', pageToken: 'token-abc' }, undefined as never).pageToken
44-
).toBeUndefined()
39+
expect(buildParams({ operation: 'get_file', pageToken: 'token-abc' }).pageToken).toBeUndefined()
4540
})
4641

4742
/**
@@ -54,7 +49,7 @@ describe('GoogleDriveBlock pagination', () => {
5449
'$subBlockId left over from a previous operation',
5550
({ subBlockId }) => {
5651
it.each(['upload', 'get_file', 'list'])('is dropped under %s', (operation) => {
57-
const params = buildParams({ operation, [subBlockId]: 'stale' }, undefined as never)
52+
const params = buildParams({ operation, [subBlockId]: 'stale' })
5853

5954
expect(params.pageToken).toBeUndefined()
6055
expect(params[subBlockId]).toBeUndefined()
@@ -63,15 +58,12 @@ describe('GoogleDriveBlock pagination', () => {
6358
)
6459

6560
it('prefers the operation-owned token when a stale sibling is also present', () => {
66-
const params = buildParams(
67-
{
68-
operation: 'search',
69-
searchPageToken: 'search-token',
70-
commentsPageToken: 'stale',
71-
pageToken: 'stale-canonical',
72-
},
73-
undefined as never
74-
)
61+
const params = buildParams({
62+
operation: 'search',
63+
searchPageToken: 'search-token',
64+
commentsPageToken: 'stale',
65+
pageToken: 'stale-canonical',
66+
})
7567

7668
expect(params.pageToken).toBe('search-token')
7769
expect(params.commentsPageToken).toBeUndefined()
@@ -84,7 +76,6 @@ describe('GoogleDriveBlock pagination', () => {
8476
* the per-operation fields (`permissionsPageSize`, `searchPageToken`, `searchQuery`) are never rendered.
8577
*/
8678
describe('GoogleDriveBlock agent tool calls', () => {
87-
const buildParams = requireParamsMapper(GoogleDriveBlock)
8879
const agentCall = (operation: string, args: Record<string, unknown>): Record<string, unknown> => {
8980
const inputs = { ...args, operation }
9081
return { ...inputs, ...buildParams(inputs, { operation }) }
@@ -107,16 +98,9 @@ describe('GoogleDriveBlock agent tool calls', () => {
10798
expect(buildParams({ operation: 'search', pageToken: 'stale' }).pageToken).toBeUndefined()
10899
})
109100

110-
it('drops a list page token left over on the canvas under a non-paginating operation', () => {
111-
expect(buildParams({ operation: 'get_file', pageToken: 'stale' }).pageToken).toBeUndefined()
112-
})
113-
114101
/** A stored agent tool keeps its operation on the tool, not in the args the mapper receives. */
115-
it.each(['list', 'search', 'list_permissions', 'list_revisions', 'list_comments'])(
116-
'keeps the model pageToken on %s when the args carry no operation',
117-
(operation) => {
118-
const args = { fileId: 'f-1', pageToken: 'next' }
119-
expect({ ...args, ...buildParams(args, { operation }) }.pageToken).toBe('next')
120-
}
121-
)
102+
it('keeps the model pageToken when the args carry no operation', () => {
103+
const args = { fileId: 'f-1', pageToken: 'next' }
104+
expect({ ...args, ...buildParams(args, { operation: 'list_comments' }) }.pageToken).toBe('next')
105+
})
122106
})

‎apps/sim/blocks/blocks/google_drive.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { GoogleDriveIcon } from '@/components/icons'
33
import { getScopesForService } from '@/lib/oauth/utils'
44
import type { BlockConfig, BlockMeta } from '@/blocks/types'
55
import { AuthMode, IntegrationType } from '@/blocks/types'
6-
import { normalizeFileInput, SERVICE_ACCOUNT_SUBBLOCKS } from '@/blocks/utils'
6+
import {
7+
isToolCall,
8+
normalizeFileInput,
9+
SERVICE_ACCOUNT_SUBBLOCKS,
10+
toolCallArg,
11+
} from '@/blocks/utils'
712
import { getTrigger } from '@/triggers'
813

914
/**
@@ -1685,8 +1690,8 @@ Return ONLY the comment text - no explanations, no quotes, no extra formatting.`
16851690
includeDeleted === 'true' ? true : includeDeleted === 'false' ? false : undefined
16861691

16871692
let effectivePageSize: string | undefined = pageSize
1688-
// Basic `pageSize`/`query` and advanced `pageToken` belong to List; only an agent call's value is trusted for other operations
1689-
const agentPageSize = context?.operation ? pageSize : undefined
1693+
// `pageSize`, `pageToken` and `query` are List's fields; other operations trust them only from a tool call
1694+
const agentPageSize = toolCallArg(context, pageSize)
16901695
if (params.operation === 'search') effectivePageSize = searchPageSize || agentPageSize
16911696
else if (params.operation === 'list_revisions')
16921697
effectivePageSize = revisionsPageSize || agentPageSize
@@ -1695,7 +1700,7 @@ Return ONLY the comment text - no explanations, no quotes, no extra formatting.`
16951700
else if (params.operation === 'list_permissions')
16961701
effectivePageSize = permissionsPageSize || agentPageSize
16971702

1698-
const agentPageToken = context?.operation ? pageToken : undefined
1703+
const agentPageToken = toolCallArg(context, pageToken)
16991704
let effectivePageToken: string | undefined = pageToken
17001705
if (params.operation === 'search') effectivePageToken = searchPageToken || agentPageToken
17011706
else if (params.operation === 'list_permissions')
@@ -1704,12 +1709,11 @@ Return ONLY the comment text - no explanations, no quotes, no extra formatting.`
17041709
effectivePageToken = revisionsPageToken || agentPageToken
17051710
else if (params.operation === 'list_comments')
17061711
effectivePageToken = commentsPageToken || agentPageToken
1707-
// The canvas list token is stale for every other operation; an agent's own token is not
17081712
else if (params.operation !== 'list') effectivePageToken = agentPageToken
17091713

17101714
const effectiveQuery =
17111715
params.operation === 'search'
1712-
? context?.operation && !searchQuery
1716+
? isToolCall(context) && !searchQuery
17131717
? query
17141718
: searchQuery
17151719
: query

‎apps/sim/blocks/blocks/google_groups.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { GoogleGroupsIcon } from '@/components/icons'
22
import { getScopesForService } from '@/lib/oauth/utils'
33
import type { BlockConfig, BlockMeta } from '@/blocks/types'
44
import { AuthMode, IntegrationType } from '@/blocks/types'
5-
import { SERVICE_ACCOUNT_SUBBLOCKS } from '@/blocks/utils'
5+
import { SERVICE_ACCOUNT_SUBBLOCKS, toolCallArg } from '@/blocks/utils'
66

77
/** Settings the Update Settings operation forwards when set; empty means unchanged. */
88
const UPDATE_SETTINGS_FIELDS = [
@@ -739,9 +739,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
739739
maxResults: rest.maxResults ? Number(rest.maxResults) : undefined,
740740
pageToken: rest.pageToken || undefined,
741741
userKey: rest.userKey || undefined,
742-
orderBy:
743-
(context?.operation ? rest.orderBy : undefined) ||
744-
(rest.sortOrder ? 'email' : undefined),
742+
orderBy: toolCallArg(context, rest.orderBy) || (rest.sortOrder ? 'email' : undefined),
745743
sortOrder: rest.sortOrder || undefined,
746744
}
747745
case 'get_group':

0 commit comments

Comments
 (0)