chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockIsHosted, mockIsAzureConfigured, mockIsOllamaConfigured } = vi.hoisted(() => ({
|
||||
mockIsHosted: { value: false },
|
||||
mockIsAzureConfigured: { value: false },
|
||||
mockIsOllamaConfigured: { value: false },
|
||||
}))
|
||||
|
||||
const {
|
||||
mockGetHostedModels,
|
||||
mockGetProviderModels,
|
||||
mockGetProviderIcon,
|
||||
mockGetBaseModelProviders,
|
||||
} = vi.hoisted(() => ({
|
||||
mockGetHostedModels: vi.fn(() => []),
|
||||
mockGetProviderModels: vi.fn(() => []),
|
||||
mockGetProviderIcon: vi.fn(() => null),
|
||||
mockGetBaseModelProviders: vi.fn(() => ({})),
|
||||
}))
|
||||
|
||||
const { mockProviders } = vi.hoisted(() => ({
|
||||
mockProviders: {
|
||||
value: {
|
||||
base: { models: [] as string[], isLoading: false },
|
||||
ollama: { models: [] as string[], isLoading: false },
|
||||
vllm: { models: [] as string[], isLoading: false },
|
||||
litellm: { models: [] as string[], isLoading: false },
|
||||
openrouter: { models: [] as string[], isLoading: false },
|
||||
fireworks: { models: [] as string[], isLoading: false },
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env-flags', () => ({
|
||||
get isHosted() {
|
||||
return mockIsHosted.value
|
||||
},
|
||||
get isAzureConfigured() {
|
||||
return mockIsAzureConfigured.value
|
||||
},
|
||||
get isOllamaConfigured() {
|
||||
return mockIsOllamaConfigured.value
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/providers/models', () => ({
|
||||
getProviderFileAttachment: vi
|
||||
.fn()
|
||||
.mockReturnValue({ maxBytes: 10 * 1024 * 1024, strategy: 'inline' }),
|
||||
INLINE_ATTACHMENT_MAX_BYTES: 10 * 1024 * 1024,
|
||||
getHostedModels: mockGetHostedModels,
|
||||
getProviderModels: mockGetProviderModels,
|
||||
getProviderIcon: mockGetProviderIcon,
|
||||
getBaseModelProviders: mockGetBaseModelProviders,
|
||||
}))
|
||||
|
||||
vi.mock('@/providers/utils', () => ({
|
||||
getProviderFromModel: vi.fn(() => 'openai'),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/providers/store', () => ({
|
||||
useProvidersStore: {
|
||||
getState: () => ({
|
||||
get providers() {
|
||||
return mockProviders.value
|
||||
},
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/oauth/utils', () => ({
|
||||
getScopesForService: vi.fn(() => []),
|
||||
}))
|
||||
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import {
|
||||
getApiKeyCondition,
|
||||
getDependsOnFields,
|
||||
getSubBlocksDependingOnChange,
|
||||
parseOptionalBooleanInput,
|
||||
parseOptionalJsonInput,
|
||||
parseOptionalNumberInput,
|
||||
} from '@/blocks/utils'
|
||||
|
||||
const BASE_CLOUD_MODELS: Record<string, string> = {
|
||||
'gpt-4o': 'openai',
|
||||
'claude-sonnet-4-5': 'anthropic',
|
||||
'gemini-2.5-pro': 'google',
|
||||
'mistral-large-latest': 'mistral',
|
||||
}
|
||||
|
||||
describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => {
|
||||
const evaluateCondition = (model: string): boolean => {
|
||||
const conditionFn = getApiKeyCondition()
|
||||
const condition = conditionFn({ model })
|
||||
if ('not' in condition && condition.not) return false
|
||||
if (condition.value === '__no_model_selected__') return false
|
||||
return true
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsHosted.value = false
|
||||
mockIsAzureConfigured.value = false
|
||||
mockIsOllamaConfigured.value = false
|
||||
mockProviders.value = {
|
||||
base: { models: [], isLoading: false },
|
||||
ollama: { models: [], isLoading: false },
|
||||
vllm: { models: [], isLoading: false },
|
||||
litellm: { models: [], isLoading: false },
|
||||
openrouter: { models: [], isLoading: false },
|
||||
fireworks: { models: [], isLoading: false },
|
||||
}
|
||||
mockGetHostedModels.mockReturnValue([])
|
||||
mockGetProviderModels.mockReturnValue([])
|
||||
mockGetBaseModelProviders.mockReturnValue({})
|
||||
})
|
||||
|
||||
describe('empty or missing model', () => {
|
||||
it('does not require API key when model is empty', () => {
|
||||
expect(evaluateCondition('')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not require API key when model is whitespace', () => {
|
||||
expect(evaluateCondition(' ')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hosted models', () => {
|
||||
it('does not require API key for hosted models on hosted platform', () => {
|
||||
mockIsHosted.value = true
|
||||
mockGetHostedModels.mockReturnValue(['gpt-4o', 'claude-sonnet-4-5'])
|
||||
expect(evaluateCondition('gpt-4o')).toBe(false)
|
||||
expect(evaluateCondition('claude-sonnet-4-5')).toBe(false)
|
||||
})
|
||||
|
||||
it('requires API key for non-hosted models on hosted platform', () => {
|
||||
mockIsHosted.value = true
|
||||
mockGetHostedModels.mockReturnValue(['gpt-4o'])
|
||||
expect(evaluateCondition('claude-sonnet-4-5')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Vertex AI models', () => {
|
||||
it('does not require API key for vertex/ prefixed models', () => {
|
||||
expect(evaluateCondition('vertex/gemini-2.5-pro')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Bedrock models', () => {
|
||||
it('does not require API key for bedrock/ prefixed models', () => {
|
||||
expect(evaluateCondition('bedrock/anthropic.claude-v2')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Azure models', () => {
|
||||
it('does not require API key for azure/ models when Azure is configured', () => {
|
||||
mockIsAzureConfigured.value = true
|
||||
expect(evaluateCondition('azure/gpt-4o')).toBe(false)
|
||||
expect(evaluateCondition('azure-openai/gpt-4o')).toBe(false)
|
||||
expect(evaluateCondition('azure-anthropic/claude-sonnet-4-5')).toBe(false)
|
||||
})
|
||||
|
||||
it('requires API key for azure/ models when Azure is not configured', () => {
|
||||
mockIsAzureConfigured.value = false
|
||||
expect(evaluateCondition('azure/gpt-4o')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('vLLM models', () => {
|
||||
it('does not require API key for vllm/ prefixed models', () => {
|
||||
expect(evaluateCondition('vllm/my-model')).toBe(false)
|
||||
expect(evaluateCondition('vllm/llama-3-70b')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('provider store lookup (client-side)', () => {
|
||||
it('does not require API key when model is in the Ollama store bucket', () => {
|
||||
mockProviders.value.ollama.models = ['llama3:latest', 'mistral:latest']
|
||||
expect(evaluateCondition('llama3:latest')).toBe(false)
|
||||
expect(evaluateCondition('mistral:latest')).toBe(false)
|
||||
})
|
||||
|
||||
it('requires API key when model is in the base store bucket', () => {
|
||||
mockProviders.value.base.models = ['gpt-4o', 'claude-sonnet-4-5']
|
||||
expect(evaluateCondition('gpt-4o')).toBe(true)
|
||||
expect(evaluateCondition('claude-sonnet-4-5')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not require API key when model is in the vLLM store bucket', () => {
|
||||
mockProviders.value.vllm.models = ['my-custom-model']
|
||||
expect(evaluateCondition('my-custom-model')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not require API key when model is in the LiteLLM store bucket', () => {
|
||||
mockProviders.value.litellm.models = ['litellm/anthropic/claude-sonnet-4-6']
|
||||
expect(evaluateCondition('litellm/anthropic/claude-sonnet-4-6')).toBe(false)
|
||||
})
|
||||
|
||||
it('requires API key when model is in the fireworks store bucket', () => {
|
||||
mockProviders.value.fireworks.models = ['fireworks/llama-3']
|
||||
expect(evaluateCondition('fireworks/llama-3')).toBe(true)
|
||||
})
|
||||
|
||||
it('requires API key when model is in the openrouter store bucket', () => {
|
||||
mockProviders.value.openrouter.models = ['openrouter/anthropic/claude']
|
||||
expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true)
|
||||
})
|
||||
|
||||
it('is case-insensitive for store lookup', () => {
|
||||
mockProviders.value.ollama.models = ['Llama3:Latest']
|
||||
expect(evaluateCondition('llama3:latest')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Ollama — OLLAMA_URL env var (server-safe)', () => {
|
||||
it('does not require API key for unknown models when OLLAMA_URL is set', () => {
|
||||
mockIsOllamaConfigured.value = true
|
||||
expect(evaluateCondition('llama3:latest')).toBe(false)
|
||||
expect(evaluateCondition('phi3:latest')).toBe(false)
|
||||
expect(evaluateCondition('gemma2:latest')).toBe(false)
|
||||
expect(evaluateCondition('deepseek-coder:latest')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not require API key for Ollama models that match cloud provider regex patterns', () => {
|
||||
mockIsOllamaConfigured.value = true
|
||||
expect(evaluateCondition('mistral:latest')).toBe(false)
|
||||
expect(evaluateCondition('mistral')).toBe(false)
|
||||
expect(evaluateCondition('mistral-nemo')).toBe(false)
|
||||
expect(evaluateCondition('gpt2')).toBe(false)
|
||||
})
|
||||
|
||||
it('requires API key for known cloud models even when OLLAMA_URL is set', () => {
|
||||
mockIsOllamaConfigured.value = true
|
||||
mockGetBaseModelProviders.mockReturnValue(BASE_CLOUD_MODELS)
|
||||
expect(evaluateCondition('gpt-4o')).toBe(true)
|
||||
expect(evaluateCondition('claude-sonnet-4-5')).toBe(true)
|
||||
expect(evaluateCondition('gemini-2.5-pro')).toBe(true)
|
||||
expect(evaluateCondition('mistral-large-latest')).toBe(true)
|
||||
})
|
||||
|
||||
it('requires API key for slash-prefixed cloud models when OLLAMA_URL is set', () => {
|
||||
mockIsOllamaConfigured.value = true
|
||||
expect(evaluateCondition('azure/gpt-4o')).toBe(true)
|
||||
expect(evaluateCondition('fireworks/llama-3')).toBe(true)
|
||||
expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true)
|
||||
expect(evaluateCondition('groq/llama-3')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cloud provider models that need API key', () => {
|
||||
it('requires API key for standard cloud models on hosted platform', () => {
|
||||
mockIsHosted.value = true
|
||||
mockGetHostedModels.mockReturnValue([])
|
||||
expect(evaluateCondition('gpt-4o')).toBe(true)
|
||||
expect(evaluateCondition('claude-sonnet-4-5')).toBe(true)
|
||||
expect(evaluateCondition('gemini-2.5-pro')).toBe(true)
|
||||
expect(evaluateCondition('mistral-large-latest')).toBe(true)
|
||||
})
|
||||
|
||||
it('requires API key for prefixed cloud models on hosted platform', () => {
|
||||
mockIsHosted.value = true
|
||||
expect(evaluateCondition('fireworks/llama-3')).toBe(true)
|
||||
expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true)
|
||||
expect(evaluateCondition('groq/llama-3')).toBe(true)
|
||||
expect(evaluateCondition('cerebras/gpt-oss-120b')).toBe(true)
|
||||
})
|
||||
|
||||
it('requires API key for prefixed cloud models on self-hosted', () => {
|
||||
mockIsHosted.value = false
|
||||
expect(evaluateCondition('fireworks/llama-3')).toBe(true)
|
||||
expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true)
|
||||
expect(evaluateCondition('groq/llama-3')).toBe(true)
|
||||
expect(evaluateCondition('cerebras/gpt-oss-120b')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('self-hosted without OLLAMA_URL', () => {
|
||||
it('requires API key for any model (Ollama models cannot appear without OLLAMA_URL)', () => {
|
||||
mockIsHosted.value = false
|
||||
mockIsOllamaConfigured.value = false
|
||||
expect(evaluateCondition('llama3:latest')).toBe(true)
|
||||
expect(evaluateCondition('mistral:latest')).toBe(true)
|
||||
expect(evaluateCondition('gpt-4o')).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseOptionalJsonInput', () => {
|
||||
it('returns undefined for empty values', () => {
|
||||
expect(parseOptionalJsonInput('', 'payload')).toBeUndefined()
|
||||
expect(parseOptionalJsonInput(' ', 'payload')).toBeUndefined()
|
||||
expect(parseOptionalJsonInput(undefined, 'payload')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('parses JSON strings', () => {
|
||||
expect(parseOptionalJsonInput('{"a":1}', 'payload')).toEqual({ a: 1 })
|
||||
expect(parseOptionalJsonInput('["a","b"]', 'payload')).toEqual(['a', 'b'])
|
||||
})
|
||||
|
||||
it('returns non-string values as-is', () => {
|
||||
const value = { a: 1 }
|
||||
expect(parseOptionalJsonInput(value, 'payload')).toBe(value)
|
||||
})
|
||||
|
||||
it('throws a helpful error for invalid JSON', () => {
|
||||
expect(() => parseOptionalJsonInput('{', 'payload')).toThrow(/Invalid JSON for payload/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseOptionalNumberInput', () => {
|
||||
it('returns undefined for empty values', () => {
|
||||
expect(parseOptionalNumberInput('', 'limit')).toBeUndefined()
|
||||
expect(parseOptionalNumberInput(' ', 'limit')).toBeUndefined()
|
||||
expect(parseOptionalNumberInput(undefined, 'limit')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('parses number strings and number values', () => {
|
||||
expect(parseOptionalNumberInput('42', 'limit')).toBe(42)
|
||||
expect(parseOptionalNumberInput(7, 'limit')).toBe(7)
|
||||
})
|
||||
|
||||
it('validates integer-only values', () => {
|
||||
expect(parseOptionalNumberInput('42', 'limit', { integer: true })).toBe(42)
|
||||
expect(() => parseOptionalNumberInput('1.5', 'limit', { integer: true })).toThrow(
|
||||
/expected an integer/i
|
||||
)
|
||||
})
|
||||
|
||||
it('validates min and max bounds', () => {
|
||||
expect(parseOptionalNumberInput('10', 'limit', { min: 1, max: 20 })).toBe(10)
|
||||
expect(() => parseOptionalNumberInput('0', 'limit', { min: 1 })).toThrow(
|
||||
/limit must be at least 1/i
|
||||
)
|
||||
expect(() => parseOptionalNumberInput('21', 'limit', { max: 20 })).toThrow(
|
||||
/limit must be at most 20/i
|
||||
)
|
||||
})
|
||||
|
||||
it('throws a helpful error for invalid numbers', () => {
|
||||
expect(() => parseOptionalNumberInput('abc', 'limit')).toThrow(/Invalid number for limit/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseOptionalBooleanInput', () => {
|
||||
it('returns undefined for empty values', () => {
|
||||
expect(parseOptionalBooleanInput('')).toBeUndefined()
|
||||
expect(parseOptionalBooleanInput(' ')).toBeUndefined()
|
||||
expect(parseOptionalBooleanInput(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('passes through boolean values', () => {
|
||||
expect(parseOptionalBooleanInput(true)).toBe(true)
|
||||
expect(parseOptionalBooleanInput(false)).toBe(false)
|
||||
})
|
||||
|
||||
it('supports numeric boolean values', () => {
|
||||
expect(parseOptionalBooleanInput(1)).toBe(true)
|
||||
expect(parseOptionalBooleanInput(0)).toBe(false)
|
||||
expect(parseOptionalBooleanInput(5)).toBe(true)
|
||||
})
|
||||
|
||||
it('supports trimmed and case-insensitive string values', () => {
|
||||
expect(parseOptionalBooleanInput('true')).toBe(true)
|
||||
expect(parseOptionalBooleanInput(' TRUE ')).toBe(true)
|
||||
expect(parseOptionalBooleanInput('1')).toBe(true)
|
||||
expect(parseOptionalBooleanInput('false')).toBe(false)
|
||||
expect(parseOptionalBooleanInput(' False ')).toBe(false)
|
||||
expect(parseOptionalBooleanInput('0')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns undefined for unrecognized string values', () => {
|
||||
expect(parseOptionalBooleanInput('yes')).toBeUndefined()
|
||||
expect(parseOptionalBooleanInput('no')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDependsOnFields', () => {
|
||||
it('returns an empty array when dependsOn is unset', () => {
|
||||
expect(getDependsOnFields(undefined)).toEqual([])
|
||||
})
|
||||
|
||||
it('returns array dependencies unchanged', () => {
|
||||
expect(getDependsOnFields(['credential', 'projectId'])).toEqual(['credential', 'projectId'])
|
||||
})
|
||||
|
||||
it('flattens all and any dependencies', () => {
|
||||
expect(getDependsOnFields({ all: ['credential'], any: ['teamId', 'manualTeamId'] })).toEqual([
|
||||
'credential',
|
||||
'teamId',
|
||||
'manualTeamId',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSubBlocksDependingOnChange', () => {
|
||||
it('finds direct dependents of a changed subblock', () => {
|
||||
const subBlocks: SubBlockConfig[] = [
|
||||
{ id: 'provider', title: 'Provider', type: 'dropdown' },
|
||||
{ id: 'model', title: 'Model', type: 'dropdown', dependsOn: ['provider'] },
|
||||
{ id: 'prompt', title: 'Prompt', type: 'long-input' },
|
||||
]
|
||||
|
||||
expect(
|
||||
getSubBlocksDependingOnChange(subBlocks, 'provider').map((subBlock) => subBlock.id)
|
||||
).toEqual(['model'])
|
||||
})
|
||||
|
||||
it('matches dependents through canonical basic and advanced siblings', () => {
|
||||
const subBlocks: SubBlockConfig[] = [
|
||||
{
|
||||
id: 'channel',
|
||||
title: 'Channel',
|
||||
type: 'channel-selector',
|
||||
canonicalParamId: 'channelId',
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'manualChannel',
|
||||
title: 'Channel ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'channelId',
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'messageId',
|
||||
title: 'Message ID',
|
||||
type: 'short-input',
|
||||
dependsOn: ['channelId'],
|
||||
},
|
||||
{
|
||||
id: 'threadTs',
|
||||
title: 'Thread Timestamp',
|
||||
type: 'short-input',
|
||||
dependsOn: ['otherField'],
|
||||
},
|
||||
]
|
||||
|
||||
expect(
|
||||
getSubBlocksDependingOnChange(subBlocks, 'manualChannel').map((subBlock) => subBlock.id)
|
||||
).toEqual(['messageId'])
|
||||
expect(
|
||||
getSubBlocksDependingOnChange(subBlocks, 'channel').map((subBlock) => subBlock.id)
|
||||
).toEqual(['messageId'])
|
||||
})
|
||||
|
||||
it('matches object-form dependencies when any listed dependency changes', () => {
|
||||
const subBlocks: SubBlockConfig[] = [
|
||||
{ id: 'credential', title: 'Credential', type: 'oauth-input' },
|
||||
{ id: 'teamId', title: 'Team', type: 'short-input' },
|
||||
{
|
||||
id: 'projectId',
|
||||
title: 'Project',
|
||||
type: 'short-input',
|
||||
dependsOn: { all: ['credential'], any: ['teamId'] },
|
||||
},
|
||||
]
|
||||
|
||||
expect(
|
||||
getSubBlocksDependingOnChange(subBlocks, 'credential').map((subBlock) => subBlock.id)
|
||||
).toEqual(['projectId'])
|
||||
expect(
|
||||
getSubBlocksDependingOnChange(subBlocks, 'teamId').map((subBlock) => subBlock.id)
|
||||
).toEqual(['projectId'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user