Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/docs/content/docs/integrations/mailchimp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Create a new audience (list) in Mailchimp
| `contact` | string | Yes | JSON object of contact information \(e.g., \{"company": "Acme", "address1": "123 Main St", "city": "NYC", "state": "NY", "zip": "10001", "country": "US"\}\) |
| `permissionReminder` | string | Yes | Permission reminder text shown to subscribers \(e.g., "You signed up for updates on our website"\) |
| `campaignDefaults` | string | Yes | JSON object of default campaign settings \(e.g., \{"from_name": "Acme", "from_email": "news@acme.com", "subject": "", "language": "en"\}\) |
| `emailTypeOption` | string | Yes | Support multiple email formats: "true" or "false" |
| `emailTypeOption` | string | No | Support multiple email formats: "true" or "false" \(default "true"\) |

#### Output

Expand Down Expand Up @@ -115,7 +115,7 @@ Update an existing audience (list) in Mailchimp
| `audienceName` | string | No | The name of the audience/list \(e.g., "Newsletter Subscribers"\) |
| `permissionReminder` | string | No | Permission reminder text shown to subscribers \(e.g., "You signed up for updates on our website"\) |
| `campaignDefaults` | string | No | JSON object of default campaign settings \(e.g., \{"from_name": "Acme", "from_email": "news@acme.com"\}\) |
| `emailTypeOption` | string | No | Support multiple email formats: "true" or "false". Omit to leave it unchanged |
| `emailTypeOption` | string | No | Whether the audience supports multiple email formats \("true" or "false"\). Leave unset to keep the current setting |

#### Output

Expand Down Expand Up @@ -1789,7 +1789,7 @@ Create a new batch operation in Mailchimp

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `operations` | string | Yes | JSON array of batch operations \(e.g., \[\{"method": "POST", "path": "/lists/\{list_id\}/members", "body": \{"email_address": "user@example.com", "status": "subscribed"\}\}\]\); an object body is sent as the JSON string Mailchimp expects |
| `operations` | json | Yes | JSON array of batch operations \(e.g., \[\{"method": "POST", "path": "/lists/\{list_id\}/members", "body": \{"email_address": "user@example.com", "status": "subscribed"\}\}\]\); an object body is sent as the JSON string Mailchimp expects |

#### Output

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,7 @@ function SubBlockComponent({
subBlockId={config.id}
content={
typeof config.value === 'function'
? config.value(subBlockValues || {})
? (config.value(subBlockValues || {}) ?? '')
: (config.defaultValue as string) || ''
}
/>
Expand Down
17 changes: 17 additions & 0 deletions apps/sim/blocks/blocks/asana.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,20 @@ describe('AsanaBlock agent tool calls with a configured field', () => {
expect(mapped.workspace).toBe('ws-1')
})
})

describe('AsanaBlock canvas limits', () => {
it('passes a numeric 0 limit through so validation can reject it', () => {
expect(
buildParams({ operation: 'get_task', workspace: 'ws-1', assignee: 'me', getTasks_limit: 0 })
).toMatchObject({ limit: 0 })
})

it('ignores a whitespace-only limit field so the model limit applies on a tool call', () => {
expect(
buildParams(
{ workspace: 'ws-1', assignee: 'me', getTasks_limit: ' ', limit: 25 },
{ operation: 'get_task' }
)
).toMatchObject({ limit: 25 })
})
})
13 changes: 10 additions & 3 deletions apps/sim/blocks/blocks/asana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
.filter((gid) => gid.length > 0)
}
const toLimit = (value: unknown): number | undefined =>
value === undefined || value === null || value === '' ? undefined : Number(value)
value === undefined || value === null || String(value).trim() === ''
? undefined
: Number(value)
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
/** A dedicated limit wins when present, so a reference resolving to 0 still reaches validation. */
const pickLimit = (dedicated: unknown, toolArg: unknown): unknown =>
dedicated === undefined || dedicated === null || String(dedicated).trim() === ''
? toolCallArg(context, toolArg)
: dedicated
const projectsArray = splitGids(params.projects)

// Only send a completion value when the user actually set the toggle; an
Expand All @@ -553,7 +560,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
workspace: params.getTasks_workspace || toolCallArg(context, params.workspace),
project: params.getTasks_project || toolCallArg(context, params.project),
assignee: params.assignee,
limit: toLimit(params.getTasks_limit || toolCallArg(context, params.limit)),
limit: toLimit(pickLimit(params.getTasks_limit, params.limit)),
offset: params.getTasks_offset || toolCallArg(context, params.offset),
}
case 'create_task':
Expand Down Expand Up @@ -588,7 +595,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
assignee: params.assignee,
projects: projectsArray,
completed: completedValue,
limit: toLimit(params.searchTasks_limit || toolCallArg(context, params.limit)),
limit: toLimit(pickLimit(params.searchTasks_limit, params.limit)),
}
case 'add_comment':
return {
Expand Down
20 changes: 20 additions & 0 deletions apps/sim/blocks/blocks/attio.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { triggersMock } from '@sim/testing/mocks/triggers.mock'
import { describe, expect, it, vi } from 'vitest'

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

import { AttioBlock } from '@/blocks/blocks/attio'

describe('Attio agent operation resolution', () => {
it('maps the selected list when the operation arrives only in the tool-call context', () => {
const mapper = AttioBlock.tools.config?.params
if (!mapper) throw new Error('Attio block has no params mapper')

const mapped = mapper(
{ oauthCredential: 'cred', listIdOrSlug: 'list-1' },
{ operation: 'get_list' }
)

expect(mapped.list).toBe('list-1')
})
})
4 changes: 2 additions & 2 deletions apps/sim/blocks/blocks/attio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1609,11 +1609,11 @@ Record reference: {"allowed_objects": ["people", "companies"]}`,
],
config: {
tool: (params) => `attio_${params.operation}`,
params: (params) => {
params: (params, context) => {
const cleanParams: Record<string, unknown> = {
oauthCredential: params.oauthCredential,
}
const op = params.operation
const op = params.operation ?? context?.operation

// Record params — each field is only sent for the operations whose subBlock condition
// actually exposes it, so a stale value left over from switching operations never leaks
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/blocks/blocks/box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,8 @@ export const BoxBlock = {
if (normalizedFile) {
params.file = normalizedFile
}
const { credential, operation, ...rest } = params
const { credential, operation: storedOperation, ...rest } = params
const operation = storedOperation ?? context?.operation

const baseParams: Record<string, unknown> = {
accessToken: credential,
Expand Down
38 changes: 38 additions & 0 deletions apps/sim/blocks/blocks/dropbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { triggersMock } from '@sim/testing/mocks/triggers.mock'
import { describe, expect, it, vi } from 'vitest'

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

import { DropboxBlock } from '@/blocks/blocks/dropbox'

function requiredFor(subBlockId: string, values: Record<string, unknown>) {
const subBlock = DropboxBlock.subBlocks.find(
(candidate) => candidate.id === subBlockId && typeof candidate.required === 'function'
)
if (!subBlock || typeof subBlock.required !== 'function') {
throw new Error(`${subBlockId} has no dynamic required condition`)
}
return subBlock.required(values)
}

describe('Dropbox cursor continuation', () => {
it('requires a folder path only until a list folder cursor is set', () => {
expect(requiredFor('path', {})).toEqual({ field: 'operation', value: 'dropbox_list_folder' })
expect(requiredFor('path', { listFolderCursor: ' ' })).toEqual({
field: 'operation',
value: 'dropbox_list_folder',
})
expect(requiredFor('path', { listFolderCursor: 'cursor-1' })).toEqual({
field: 'operation',
value: [],
})
})

it('requires a search query only until a search cursor is set', () => {
expect(requiredFor('query', {})).toEqual({ field: 'operation', value: 'dropbox_search' })
expect(requiredFor('query', { searchCursor: 'cursor-1' })).toEqual({
field: 'operation',
value: [],
})
})
})
10 changes: 8 additions & 2 deletions apps/sim/blocks/blocks/dropbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ export const DropboxBlock = {
type: 'short-input',
placeholder: '/ (root) or /folder',
condition: { field: 'operation', value: 'dropbox_list_folder' },
required: true,
required: (values) =>
String(values?.listFolderCursor ?? '').trim()
? { field: 'operation', value: [] }
: { field: 'operation', value: 'dropbox_list_folder' },
},
{
id: 'recursive',
Expand Down Expand Up @@ -332,7 +335,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
type: 'short-input',
placeholder: 'Enter search term...',
condition: { field: 'operation', value: 'dropbox_search' },
required: true,
required: (values) =>
String(values?.searchCursor ?? '').trim()
? { field: 'operation', value: [] }
: { field: 'operation', value: 'dropbox_search' },
},
{
id: 'path',
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/blocks/blocks/hubspot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1635,7 +1635,7 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
params: (params, context) => {
const {
oauthCredential,
operation,
operation: storedOperation,
propertiesToSet,
properties,
searchProperties,
Expand All @@ -1647,6 +1647,7 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
archived,
...rest
} = params
const operation = storedOperation ?? context?.operation

const cleanParams: Record<string, any> = {
oauthCredential,
Expand Down
76 changes: 76 additions & 0 deletions apps/sim/blocks/blocks/intercom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, it } from 'vitest'
import { IntercomV2Block } from '@/blocks/blocks/intercom'
import type { BlockToolParamsContext } from '@/blocks/types'

function mapParams(params: Record<string, unknown>, context?: BlockToolParamsContext) {
const mapper = IntercomV2Block.tools.config?.params
if (!mapper) throw new Error('Intercom block has no params mapper')
return mapper(params, context)
}

describe('Intercom snooze timestamps', () => {
it('sends a positive whole Unix timestamp as a number', () => {
expect(
mapParams({
operation: 'snooze_conversation',
conversationId: 'c1',
admin_id: 'a1',
snoozed_until: '1767225600',
}).snoozed_until
).toBe(1767225600)
})

it.each(['0', '-5', ' ', '1.5', 0])('rejects %j as a snooze time', (snoozedUntil) => {
expect(() =>
mapParams({
operation: 'snooze_conversation',
conversationId: 'c1',
admin_id: 'a1',
snoozed_until: snoozedUntil,
})
).toThrow('positive whole Unix timestamp')
})
})

describe('Intercom agent operation resolution', () => {
it('keeps a boolean disable_notifications when the operation arrives only in the context', () => {
const mapped = mapParams(
{ ticket_type_id: 't1', disable_notifications: true },
{ operation: 'create_ticket' }
)

expect(mapped.disable_notifications).toBe(true)
})
})

describe('Intercom ticket snooze', () => {
it('validates the model snoozed_until on an update_ticket tool call', () => {
const mapped = mapParams(
{ ticket_id: 't1', snoozed_until: '1767225600' },
{ operation: 'update_ticket' }
)
expect(mapped.snoozed_until).toBe(1767225600)
expect(() =>
mapParams({ ticket_id: 't1', snoozed_until: '-1' }, { operation: 'update_ticket' })
).toThrow('positive whole Unix timestamp')
})

it('does not send a stale conversation snooze value with a canvas ticket update', () => {
const inputs = { operation: 'update_ticket', ticket_id: 't1', snoozed_until: '5' }
const sent = { ...inputs, ...mapParams(inputs) }
expect(sent.snoozed_until).toBeUndefined()
})
})

describe('Intercom role defaults', () => {
const role = IntercomV2Block.subBlocks.find((subBlock) => subBlock.id === 'role')

it('leads with No change everywhere except a known Create Contact', () => {
const options = role?.options
if (typeof options !== 'function') throw new Error('role options are static')
expect(options({ values: {} })[0]?.id).toBe('')
expect(options({ values: { operation: 'update_contact' } })[0]?.id).toBe('')
expect(options({ values: { operation: 'create_contact' } })[0]?.id).toBe('lead')
expect(options()[0]?.id).toBe('lead')
})
})
Loading
Loading