diff --git a/apps/desktop/src/main/channel-identity.test.ts b/apps/desktop/src/main/channel-identity.test.ts index 7a7d06613e1..7cc01ac8412 100644 --- a/apps/desktop/src/main/channel-identity.test.ts +++ b/apps/desktop/src/main/channel-identity.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('electron', () => import('@/test/electron-mock')) + import { APP_NAME_FOR_CHANNEL, channelForOrigin, DEFAULT_ORIGIN } from '@/main/config' import { classifyNavigation } from '@/main/navigation' import { DEV, identityForOrigin, LOCAL, PROD, STAGING } from '../../scripts/channels' diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts index e523a0ffbc1..939798f6d18 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts @@ -38,6 +38,29 @@ const oauthBlockConfig = { tools: { access: ['slack_message'] }, } +const tableBlockConfig = { + type: 'table', + name: 'Table', + outputs: {}, + subBlocks: [ + { + id: 'operation', + type: 'dropdown', + options: [ + { label: 'Query Rows', id: 'query_rows' }, + { label: 'Insert Row', id: 'insert_row' }, + ], + }, + ], + tools: { + access: ['table_query_rows', 'table_insert_row'], + config: { + tool: (params: Record) => + params.operation === 'insert_row' ? 'table_insert_row' : 'table_query_rows', + }, + }, +} + const routerBlockConfig = { type: 'router_v2', name: 'Router', @@ -205,6 +228,7 @@ const toolsByIdMock: Record = { const blockConfigsByType: Record = { condition: conditionBlockConfig, slack: oauthBlockConfig, + table: tableBlockConfig, router_v2: routerBlockConfig, agent: agentBlockConfig, pi: piBlockConfig, @@ -1239,6 +1263,42 @@ describe('validateInputsForBlock - agent tools (tool-input)', () => { expect(result.validInputs.tools).toBeDefined() }) + it('accepts a declared integration block operation', () => { + const result = validateInputsForBlock( + 'agent', + { tools: [{ type: 'table', operation: 'insert_row', usageControl: 'auto' }] }, + 'agent-1' + ) + + expect(result.errors).toHaveLength(0) + expect(result.validInputs.tools).toBeDefined() + }) + + it('rejects a prefixed tool id used as an integration block operation', () => { + const result = validateInputsForBlock( + 'agent', + { tools: [{ type: 'table', operation: 'table_insert_row', usageControl: 'auto' }] }, + 'agent-1' + ) + + expect(result.validInputs.tools).toBeUndefined() + expect(result.errors).toHaveLength(1) + expect(result.errors[0]?.error).toContain('invalid operation "table_insert_row"') + expect(result.errors[0]?.error).toContain('query_rows, insert_row') + expect(result.errors[0]?.error).toContain('may differ from the underlying tool id') + }) + + it('rejects a missing operation for a multi-operation integration block', () => { + const result = validateInputsForBlock( + 'agent', + { tools: [{ type: 'table', usageControl: 'auto' }] }, + 'agent-1' + ) + + expect(result.validInputs.tools).toBeUndefined() + expect(result.errors[0]?.error).toContain('requires an operation') + }) + it('rejects an integration tool unavailable in this deployment', () => { mockIsIntegrationDeploymentAvailable.mockReturnValue(false) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts index bbada28edbb..2780a902496 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts @@ -248,6 +248,34 @@ function validateAgentToolEntry(item: any, index: number): string | null { if (!isIntegrationDeploymentAvailableForVisibility(type, overlayVisibility())) { return `${where} block type "${type}" is unavailable in this deployment` } + + const operationConfig = block.subBlocks?.find((subBlock) => subBlock.id === 'operation') + if (operationConfig?.options) { + let validOperations: string[] + try { + const options = + typeof operationConfig.options === 'function' + ? operationConfig.options() + : operationConfig.options + validOperations = options.map((option) => option.id) + } catch (error) { + return `${where} could not validate operations for block type "${type}": ${toError(error).message}` + } + + const operation = item.operation + if ( + validOperations.length > 1 && + (typeof operation !== 'string' || operation.trim() === '') + ) { + return `${where} block type "${type}" requires an operation. Valid operations: ${validOperations.join(', ')}` + } + if ( + operation !== undefined && + (typeof operation !== 'string' || !validOperations.includes(operation)) + ) { + return `${where} block type "${type}" has invalid operation "${String(operation)}". Valid operations: ${validOperations.join(', ')}. Use one of the block operation ids above; it may differ from the underlying tool id.` + } + } } return null diff --git a/bun.lock b/bun.lock index 0132ebcfa12..f3070e48480 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio",