d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
352 lines
10 KiB
TypeScript
352 lines
10 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*
|
|
* 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 { blocksMock } from '@sim/testing/mocks'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { Serializer } from '@/serializer/index'
|
|
|
|
vi.mock('@/blocks', () => blocksMock)
|
|
|
|
/**
|
|
* Validates required parameters after user and LLM parameter merge.
|
|
* This checks user-or-llm visibility fields that should have been provided by either source.
|
|
*/
|
|
function validateRequiredParametersAfterMerge(
|
|
toolId: string,
|
|
tool: any,
|
|
params: Record<string, any>
|
|
): void {
|
|
if (!tool?.params) return
|
|
|
|
Object.entries(tool.params).forEach(([paramId, paramConfig]: [string, any]) => {
|
|
// Only validate user-or-llm visibility fields (user-only are validated earlier)
|
|
if (paramConfig.required && paramConfig.visibility === 'user-or-llm') {
|
|
const value = params[paramId]
|
|
if (value === undefined || value === null || value === '') {
|
|
// Capitalize first letter of paramId for display
|
|
const displayName = paramId.charAt(0).toUpperCase() + paramId.slice(1)
|
|
throw new Error(`${displayName} is required for ${tool.name}`)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
vi.mock('@/tools/utils', () => ({
|
|
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
|
|
},
|
|
validateRequiredParametersAfterMerge,
|
|
}))
|
|
|
|
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()
|
|
})
|
|
})
|