feat(tools): added hunter.io tools/block, added default values of first option in dropdowns to avoid operation selector issue, added descriptions & param validation & updated docs (#825)

* feat(tools): added hunter.io tools/block, added default values of first option in dropdowns to avoid operation selector issue

* fix

* added description for all outputs, fixed param validation for tools

* cleanup

* add dual validation, once during serialization and once during execution

* improvement(docs): add base exec charge info to docs (#826)

* improvement(doc-tags-subblock): use table for doc tags subblock in create_document tool for KB (#827)

* improvement(doc-tags-subblock): use table for doc tags create doc tool in KB block

* enforce max tags

* remove red warning text

* fix(bugs): fixed rb2b csp, fixed overly-verbose logs, fixed x URLs (#828)

Co-authored-by: waleedlatif <waleedlatif@waleedlatifs-MacBook-Pro.local>

* fixed serialization errors to appear like execution errors, also fixed contrast on cmdk modal

* fixed required for tools, added tag dropdown for kb tags

* fix remaining tools with required fields

* update utils

* update docs

* fix kb tags

* fix types for exa

* lint

* updated contributing guide + pr template

* Test pre-commit hook with linting

* Test pre-commit hook again

* remove test files

* fixed wealthbox tool

* update telemetry endpoints

---------

Co-authored-by: waleedlatif <waleedlatif@waleedlatifs-MacBook-Pro.local>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
This commit is contained in:
Waleed Latif
2025-07-30 23:36:44 -07:00
committed by GitHub
parent 03607bbc8b
commit b253454723
180 changed files with 4924 additions and 3653 deletions

View File

@@ -7,7 +7,7 @@
* converting between workflow state (blocks, edges, loops) and serialized format
* used by the executor.
*/
import { describe, expect, test, vi } from 'vitest'
import { describe, expect, vi } from 'vitest'
import { getProviderFromModel } from '@/providers/utils'
import {
createAgentWithToolsWorkflowState,
@@ -22,7 +22,6 @@ import {
import { Serializer } from '@/serializer/index'
import type { SerializedWorkflow } from '@/serializer/types'
// Mock getBlock function
vi.mock('@/blocks', () => ({
getBlock: (type: string) => {
// Mock block configurations for different block types
@@ -120,12 +119,76 @@ vi.mock('@/blocks', () => ({
],
inputs: {},
},
jina: {
name: 'Jina',
description: 'Convert website content into text',
category: 'tools',
bgColor: '#333333',
tools: {
access: ['jina_read_url'],
config: {
tool: () => 'jina_read_url',
},
},
subBlocks: [
{ id: 'url', type: 'short-input', title: 'URL', required: true },
{ id: 'apiKey', type: 'short-input', title: 'API Key', required: true },
],
inputs: {
url: { type: 'string' },
apiKey: { type: 'string' },
},
},
reddit: {
name: 'Reddit',
description: 'Access Reddit data and content',
category: 'tools',
bgColor: '#FF5700',
tools: {
access: ['reddit_get_posts', 'reddit_get_comments'],
config: {
tool: () => 'reddit_get_posts',
},
},
subBlocks: [
{ id: 'operation', type: 'dropdown', title: 'Operation', required: true },
{ id: 'credential', type: 'oauth-input', title: 'Reddit Account', required: true },
{ id: 'subreddit', type: 'short-input', title: 'Subreddit', required: true },
],
inputs: {
operation: { type: 'string' },
credential: { type: 'string' },
subreddit: { type: 'string' },
},
},
}
return mockConfigs[type] || null
},
}))
// Mock getTool function
vi.mock('@/tools/utils', () => ({
getTool: (toolId: string) => {
// Mock tool configurations for testing
const mockTools: Record<string, any> = {
jina_read_url: {
params: {
url: { visibility: 'user-or-llm', required: true },
apiKey: { visibility: 'user-only', required: true },
},
},
reddit_get_posts: {
params: {
subreddit: { visibility: 'user-or-llm', required: true },
credential: { visibility: 'user-only', required: true },
},
},
}
return mockTools[toolId] || null
},
}))
// Mock logger
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: () => ({
@@ -141,7 +204,7 @@ describe('Serializer', () => {
* Serialization tests
*/
describe('serializeWorkflow', () => {
test('should serialize a minimal workflow correctly', () => {
it.concurrent('should serialize a minimal workflow correctly', () => {
const { blocks, edges, loops } = createMinimalWorkflowState()
const serializer = new Serializer()
@@ -170,7 +233,7 @@ describe('Serializer', () => {
expect(serialized.connections[0].target).toBe('agent1')
})
test('should serialize a conditional workflow correctly', () => {
it.concurrent('should serialize a conditional workflow correctly', () => {
const { blocks, edges, loops } = createConditionalWorkflowState()
const serializer = new Serializer()
@@ -202,7 +265,7 @@ describe('Serializer', () => {
expect(falsePathConnection?.target).toBe('agent2')
})
test('should serialize a workflow with loops correctly', () => {
it.concurrent('should serialize a workflow with loops correctly', () => {
const { blocks, edges, loops } = createLoopWorkflowState()
const serializer = new Serializer()
@@ -223,7 +286,7 @@ describe('Serializer', () => {
expect(loopBackConnection?.sourceHandle).toBe('condition-true')
})
test('should serialize a complex workflow with multiple block types', () => {
it.concurrent('should serialize a complex workflow with multiple block types', () => {
const { blocks, edges, loops } = createComplexWorkflowState()
const serializer = new Serializer()
@@ -260,7 +323,7 @@ describe('Serializer', () => {
expect(agentBlock?.outputs.responseFormat).toBeDefined()
})
test('should serialize agent block with custom tools correctly', () => {
it.concurrent('should serialize agent block with custom tools correctly', () => {
const { blocks, edges, loops } = createAgentWithToolsWorkflowState()
const serializer = new Serializer()
@@ -292,7 +355,7 @@ describe('Serializer', () => {
expect(functionTool.name).toBe('calculator')
})
test('should handle invalid block types gracefully', () => {
it.concurrent('should handle invalid block types gracefully', () => {
const { blocks, edges, loops } = createInvalidWorkflowState()
const serializer = new Serializer()
@@ -307,7 +370,7 @@ describe('Serializer', () => {
* Deserialization tests
*/
describe('deserializeWorkflow', () => {
test('should deserialize a serialized workflow correctly', () => {
it.concurrent('should deserialize a serialized workflow correctly', () => {
const { blocks, edges, loops } = createMinimalWorkflowState()
const serializer = new Serializer()
@@ -341,7 +404,7 @@ describe('Serializer', () => {
expect(deserialized.edges[0].target).toBe('agent1')
})
test('should deserialize a complex workflow with all block types', () => {
it.concurrent('should deserialize a complex workflow with all block types', () => {
const { blocks, edges, loops } = createComplexWorkflowState()
const serializer = new Serializer()
@@ -379,7 +442,7 @@ describe('Serializer', () => {
expect(agentBlock.subBlocks.provider.value).toBe('openai')
})
test('should handle serialized workflow with invalid block metadata', () => {
it.concurrent('should handle serialized workflow with invalid block metadata', () => {
const invalidWorkflow = createInvalidSerializedWorkflow() as SerializedWorkflow
const serializer = new Serializer()
@@ -389,7 +452,7 @@ describe('Serializer', () => {
)
})
test('should handle serialized workflow with missing metadata', () => {
it.concurrent('should handle serialized workflow with missing metadata', () => {
const invalidWorkflow = createMissingMetadataWorkflow() as SerializedWorkflow
const serializer = new Serializer()
@@ -402,7 +465,7 @@ describe('Serializer', () => {
* End-to-end serialization/deserialization tests
*/
describe('round-trip serialization', () => {
test('should preserve all data through serialization and deserialization', () => {
it.concurrent('should preserve all data through serialization and deserialization', () => {
const { blocks, edges, loops } = createComplexWorkflowState()
const serializer = new Serializer()
@@ -446,4 +509,211 @@ describe('Serializer', () => {
expect(reserialized.loops).toEqual(serialized.loops)
})
})
describe('validation during serialization', () => {
it.concurrent('should throw error for missing user-only required fields', () => {
const serializer = new Serializer()
// Create a block state with a missing user-only required field (API key)
const blockWithMissingUserOnlyField: any = {
id: 'test-block',
type: 'jina',
name: 'Test Jina Block',
position: { x: 0, y: 0 },
subBlocks: {
url: { value: 'https://example.com' },
apiKey: { value: null }, // Missing user-only required field
},
outputs: {},
enabled: true,
}
expect(() => {
serializer.serializeWorkflow(
{ 'test-block': blockWithMissingUserOnlyField },
[],
{},
undefined,
true
)
}).toThrow('Test Jina Block is missing required fields: API Key')
})
it.concurrent('should not throw error when all user-only required fields are present', () => {
const serializer = new Serializer()
const blockWithAllUserOnlyFields: any = {
id: 'test-block',
type: 'jina',
name: 'Test Jina Block',
position: { x: 0, y: 0 },
subBlocks: {
url: { value: 'https://example.com' },
apiKey: { value: 'test-api-key' },
},
outputs: {},
enabled: true,
}
expect(() => {
serializer.serializeWorkflow(
{ 'test-block': blockWithAllUserOnlyFields },
[],
{},
undefined,
true
)
}).not.toThrow()
})
it.concurrent('should not validate user-or-llm fields during serialization', () => {
const serializer = new Serializer()
// Create a Reddit block with missing subreddit (user-or-llm field)
const blockWithMissingUserOrLlmField: any = {
id: 'test-block',
type: 'reddit',
name: 'Test Reddit Block',
position: { x: 0, y: 0 },
subBlocks: {
operation: { value: 'get_posts' },
credential: { value: 'test-credential' },
subreddit: { value: null }, // Missing user-or-llm field - should NOT be validated here
},
outputs: {},
enabled: true,
}
// Should NOT throw because subreddit is user-or-llm, not user-only
expect(() => {
serializer.serializeWorkflow(
{ 'test-block': blockWithMissingUserOrLlmField },
[],
{},
undefined,
true
)
}).not.toThrow()
})
it.concurrent('should not validate when validateRequired is false', () => {
const serializer = new Serializer()
const blockWithMissingField: any = {
id: 'test-block',
type: 'jina',
name: 'Test Jina Block',
position: { x: 0, y: 0 },
subBlocks: {
url: { value: 'https://example.com' },
apiKey: { value: null }, // Missing required field
},
outputs: {},
enabled: true,
}
// Should NOT throw when validation is disabled (default behavior)
expect(() => {
serializer.serializeWorkflow({ 'test-block': blockWithMissingField }, [], {})
}).not.toThrow()
})
it.concurrent('should validate multiple user-only fields and report all missing', () => {
const serializer = new Serializer()
const blockWithMultipleMissing: any = {
id: 'test-block',
type: 'jina',
name: 'Test Jina Block',
position: { x: 0, y: 0 },
subBlocks: {
url: { value: null }, // Missing user-or-llm field (should NOT be validated)
apiKey: { value: null }, // Missing user-only field (should be validated)
},
outputs: {},
enabled: true,
}
expect(() => {
serializer.serializeWorkflow(
{ 'test-block': blockWithMultipleMissing },
[],
{},
undefined,
true
)
}).toThrow('Test Jina Block is missing required fields: API Key')
})
it.concurrent('should handle blocks with no tool configuration gracefully', () => {
const serializer = new Serializer()
const blockWithNoTools: any = {
id: 'test-block',
type: 'condition', // Condition blocks have different tool setup
name: 'Test Condition Block',
position: { x: 0, y: 0 },
subBlocks: {
condition: { value: null }, // Missing required field but not user-only
},
outputs: {},
enabled: true,
}
// Should NOT throw because condition blocks don't have user-only params
expect(() => {
serializer.serializeWorkflow({ 'test-block': blockWithNoTools }, [], {}, undefined, true)
}).not.toThrow()
})
it.concurrent('should handle empty string values as missing', () => {
const serializer = new Serializer()
const blockWithEmptyString: any = {
id: 'test-block',
type: 'jina',
name: 'Test Jina Block',
position: { x: 0, y: 0 },
subBlocks: {
url: { value: 'https://example.com' },
apiKey: { value: '' }, // Empty string should be treated as missing
},
outputs: {},
enabled: true,
}
expect(() => {
serializer.serializeWorkflow(
{ 'test-block': blockWithEmptyString },
[],
{},
undefined,
true
)
}).toThrow('Test Jina Block is missing required fields: API Key')
})
it.concurrent('should only validate user-only fields, not user-or-llm fields', () => {
const serializer = new Serializer()
// Block with both user-only and user-or-llm missing fields
const mixedBlock: any = {
id: 'test-block',
type: 'reddit',
name: 'Test Reddit Block',
position: { x: 0, y: 0 },
subBlocks: {
operation: { value: 'get_posts' },
credential: { value: null }, // user-only - should be validated
subreddit: { value: null }, // user-or-llm - should NOT be validated
},
outputs: {},
enabled: true,
}
expect(() => {
serializer.serializeWorkflow({ 'test-block': mixedBlock }, [], {}, undefined, true)
}).toThrow('Test Reddit Block is missing required fields: Reddit Account')
})
})
})

View File

@@ -3,6 +3,7 @@ import { createLogger } from '@/lib/logs/console/logger'
import { getBlock } from '@/blocks'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
import { getTool } from '@/tools/utils'
const logger = createLogger('Serializer')
@@ -11,11 +12,12 @@ export class Serializer {
blocks: Record<string, BlockState>,
edges: Edge[],
loops: Record<string, Loop>,
parallels?: Record<string, Parallel>
parallels?: Record<string, Parallel>,
validateRequired = false
): SerializedWorkflow {
return {
version: '1.0',
blocks: Object.values(blocks).map((block) => this.serializeBlock(block)),
blocks: Object.values(blocks).map((block) => this.serializeBlock(block, validateRequired)),
connections: edges.map((edge) => ({
source: edge.source,
target: edge.target,
@@ -27,7 +29,7 @@ export class Serializer {
}
}
private serializeBlock(block: BlockState): SerializedBlock {
private serializeBlock(block: BlockState, validateRequired = false): SerializedBlock {
// Special handling for subflow blocks (loops, parallels, etc.)
if (block.type === 'loop' || block.type === 'parallel') {
return {
@@ -55,8 +57,14 @@ export class Serializer {
throw new Error(`Invalid block type: ${block.type}`)
}
// Check if this is an agent block with custom tools
// Extract parameters from UI state
const params = this.extractParams(block)
// Validate required fields that only users can provide (before execution starts)
if (validateRequired) {
this.validateRequiredFieldsBeforeExecution(block, blockConfig, params)
}
let toolId = ''
if (block.type === 'agent' && params.tools) {
@@ -208,6 +216,62 @@ export class Serializer {
return params
}
private validateRequiredFieldsBeforeExecution(
block: BlockState,
blockConfig: any,
params: Record<string, any>
) {
// Validate user-only required fields before execution starts
// This catches missing API keys, credentials, and other user-provided values early
// Fields that are user-or-llm will be validated later after parameter merging
// Get the tool configuration to check parameter visibility
const toolAccess = blockConfig.tools?.access
if (!toolAccess || toolAccess.length === 0) {
return // No tools to validate against
}
// Determine the current tool ID using the same logic as the serializer
let currentToolId = ''
try {
currentToolId = blockConfig.tools.config?.tool
? blockConfig.tools.config.tool(params)
: blockConfig.tools.access[0]
} catch (error) {
logger.warn('Tool selection failed during validation, using default:', {
error: error instanceof Error ? error.message : String(error),
})
currentToolId = blockConfig.tools.access[0]
}
// Get the specific tool to validate against
const currentTool = getTool(currentToolId)
if (!currentTool) {
return // Tool not found, skip validation
}
// Check required user-only parameters for the current tool
const missingFields: string[] = []
// Iterate through the tool's parameters, not the block's subBlocks
Object.entries(currentTool.params || {}).forEach(([paramId, paramConfig]) => {
if (paramConfig.required && paramConfig.visibility === 'user-only') {
const fieldValue = params[paramId]
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
// Find the corresponding subBlock to get the display title
const subBlockConfig = blockConfig.subBlocks?.find((sb: any) => sb.id === paramId)
const displayName = subBlockConfig?.title || paramId
missingFields.push(displayName)
}
}
})
if (missingFields.length > 0) {
const blockName = block.name || blockConfig.name || 'Block'
throw new Error(`${blockName} is missing required fields: ${missingFields.join(', ')}`)
}
}
deserializeWorkflow(workflow: SerializedWorkflow): {
blocks: Record<string, BlockState>
edges: Edge[]

View File

@@ -0,0 +1,372 @@
/**
* @vitest-environment jsdom
*
* Integration Tests for Validation Architecture
*
* These tests verify the complete validation flow:
* 1. Early validation (serialization) - user-only required fields
* 2. Late validation (tool execution) - user-or-llm required fields
*/
import { describe, expect, vi } from 'vitest'
import { Serializer } from '@/serializer/index'
import { validateRequiredParametersAfterMerge } from '@/tools/utils'
vi.mock('@/blocks', () => ({
getBlock: (type: string) => {
const mockConfigs: Record<string, any> = {
jina: {
name: 'Jina',
description: 'Convert website content into text',
category: 'tools',
bgColor: '#333333',
tools: {
access: ['jina_read_url'],
},
subBlocks: [
{ id: 'url', type: 'short-input', title: 'URL', required: true },
{ id: 'apiKey', type: 'short-input', title: 'API Key', required: true },
],
inputs: {
url: { type: 'string' },
apiKey: { type: 'string' },
},
},
reddit: {
name: 'Reddit',
description: 'Access Reddit data',
category: 'tools',
bgColor: '#FF5700',
tools: {
access: ['reddit_get_posts'],
},
subBlocks: [
{ id: 'operation', type: 'dropdown', title: 'Operation', required: true },
{ id: 'credential', type: 'oauth-input', title: 'Reddit Account', required: true },
{ id: 'subreddit', type: 'short-input', title: 'Subreddit', required: true },
],
inputs: {
operation: { type: 'string' },
credential: { type: 'string' },
subreddit: { type: 'string' },
},
},
}
return mockConfigs[type] || null
},
}))
vi.mock('@/tools/utils', async () => {
const actual = await vi.importActual('@/tools/utils')
return {
...actual,
getTool: (toolId: string) => {
const mockTools: Record<string, any> = {
jina_read_url: {
name: 'Jina Reader',
params: {
url: {
type: 'string',
visibility: 'user-or-llm',
required: true,
description: 'URL to extract content from',
},
apiKey: {
type: 'string',
visibility: 'user-only',
required: true,
description: 'Your Jina API key',
},
},
},
reddit_get_posts: {
name: 'Reddit Posts',
params: {
subreddit: {
type: 'string',
visibility: 'user-or-llm',
required: true,
description: 'Subreddit name',
},
credential: {
type: 'string',
visibility: 'user-only',
required: true,
description: 'Reddit credentials',
},
},
},
}
return mockTools[toolId] || null
},
}
})
describe('Validation Integration Tests', () => {
it.concurrent('early validation should catch missing user-only fields', () => {
const serializer = new Serializer()
// Block missing user-only field (API key)
const blockWithMissingUserOnlyField: any = {
id: 'jina-block',
type: 'jina',
name: 'Jina Content Extractor',
position: { x: 0, y: 0 },
subBlocks: {
url: { value: 'https://example.com' }, // Present
apiKey: { value: null }, // Missing user-only field
},
outputs: {},
enabled: true,
}
// Should fail at serialization (early validation)
expect(() => {
serializer.serializeWorkflow(
{ 'jina-block': blockWithMissingUserOnlyField },
[],
{},
undefined,
true
)
}).toThrow('Jina Content Extractor is missing required fields: API Key')
})
it.concurrent(
'early validation should allow missing user-or-llm fields (LLM can provide later)',
() => {
const serializer = new Serializer()
// Block missing user-or-llm field (URL) but has user-only field (API key)
const blockWithMissingUserOrLlmField: any = {
id: 'jina-block',
type: 'jina',
name: 'Jina Content Extractor',
position: { x: 0, y: 0 },
subBlocks: {
url: { value: null }, // Missing user-or-llm field (LLM can provide)
apiKey: { value: 'test-api-key' }, // Present user-only field
},
outputs: {},
enabled: true,
}
// Should pass serialization (early validation doesn't check user-or-llm fields)
expect(() => {
serializer.serializeWorkflow(
{ 'jina-block': blockWithMissingUserOrLlmField },
[],
{},
undefined,
true
)
}).not.toThrow()
}
)
it.concurrent(
'late validation should catch missing user-or-llm fields after parameter merge',
() => {
// Simulate parameters after user + LLM merge
const mergedParams = {
url: null, // Missing user-or-llm field
apiKey: 'test-api-key', // Present user-only field
}
// Should fail at tool validation (late validation)
expect(() => {
validateRequiredParametersAfterMerge(
'jina_read_url',
{
name: 'Jina Reader',
params: {
url: {
type: 'string',
visibility: 'user-or-llm',
required: true,
description: 'URL to extract content from',
},
apiKey: {
type: 'string',
visibility: 'user-only',
required: true,
description: 'Your Jina API key',
},
},
} as any,
mergedParams
)
}).toThrow('"Url" is required for Jina Reader')
}
)
it.concurrent('late validation should NOT validate user-only fields (validated earlier)', () => {
// Simulate parameters after user + LLM merge - missing user-only field
const mergedParams = {
url: 'https://example.com', // Present user-or-llm field
apiKey: null, // Missing user-only field (but shouldn't be checked here)
}
// Should pass tool validation (late validation doesn't check user-only fields)
expect(() => {
validateRequiredParametersAfterMerge(
'jina_read_url',
{
name: 'Jina Reader',
params: {
url: {
type: 'string',
visibility: 'user-or-llm',
required: true,
description: 'URL to extract content from',
},
apiKey: {
type: 'string',
visibility: 'user-only',
required: true,
description: 'Your Jina API key',
},
},
} as any,
mergedParams
)
}).not.toThrow()
})
it.concurrent('complete validation flow: both layers working together', () => {
const serializer = new Serializer()
// Scenario 1: Missing user-only field - should fail at serialization
const blockMissingUserOnly: any = {
id: 'reddit-block',
type: 'reddit',
name: 'Reddit Posts',
position: { x: 0, y: 0 },
subBlocks: {
operation: { value: 'get_posts' },
credential: { value: null }, // Missing user-only
subreddit: { value: 'programming' }, // Present user-or-llm
},
outputs: {},
enabled: true,
}
expect(() => {
serializer.serializeWorkflow(
{ 'reddit-block': blockMissingUserOnly },
[],
{},
undefined,
true
)
}).toThrow('Reddit Posts is missing required fields: Reddit Account')
// Scenario 2: Has user-only fields but missing user-or-llm - should pass serialization
const blockMissingUserOrLlm: any = {
id: 'reddit-block',
type: 'reddit',
name: 'Reddit Posts',
position: { x: 0, y: 0 },
subBlocks: {
operation: { value: 'get_posts' },
credential: { value: 'reddit-token' }, // Present user-only
subreddit: { value: null }, // Missing user-or-llm
},
outputs: {},
enabled: true,
}
// Should pass serialization
expect(() => {
serializer.serializeWorkflow(
{ 'reddit-block': blockMissingUserOrLlm },
[],
{},
undefined,
true
)
}).not.toThrow()
// But should fail at tool validation
const mergedParams = {
subreddit: null, // Missing user-or-llm field
credential: 'reddit-token', // Present user-only field
}
expect(() => {
validateRequiredParametersAfterMerge(
'reddit_get_posts',
{
name: 'Reddit Posts',
params: {
subreddit: {
type: 'string',
visibility: 'user-or-llm',
required: true,
description: 'Subreddit name',
},
credential: {
type: 'string',
visibility: 'user-only',
required: true,
description: 'Reddit credentials',
},
},
} as any,
mergedParams
)
}).toThrow('"Subreddit" is required for Reddit Posts')
})
it.concurrent('complete success: all required fields provided correctly', () => {
const serializer = new Serializer()
// Block with all required fields present
const completeBlock: any = {
id: 'jina-block',
type: 'jina',
name: 'Jina Content Extractor',
position: { x: 0, y: 0 },
subBlocks: {
url: { value: 'https://example.com' }, // Present user-or-llm
apiKey: { value: 'test-api-key' }, // Present user-only
},
outputs: {},
enabled: true,
}
// Should pass serialization (early validation)
expect(() => {
serializer.serializeWorkflow({ 'jina-block': completeBlock }, [], {}, undefined, true)
}).not.toThrow()
// Should pass tool validation (late validation)
const completeParams = {
url: 'https://example.com',
apiKey: 'test-api-key',
}
expect(() => {
validateRequiredParametersAfterMerge(
'jina_read_url',
{
name: 'Jina Reader',
params: {
url: {
type: 'string',
visibility: 'user-or-llm',
required: true,
description: 'URL to extract content from',
},
apiKey: {
type: 'string',
visibility: 'user-only',
required: true,
description: 'Your Jina API key',
},
},
} as any,
completeParams
)
}).not.toThrow()
})
})