mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-18 03:18:01 -05:00
Compare commits
2 Commits
main
...
fix/block-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
484eb365db | ||
|
|
73c029ffc7 |
@@ -6,10 +6,14 @@ import type { ResolutionContext } from './reference'
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
/**
|
||||
* Creates a minimal workflow for testing.
|
||||
*/
|
||||
function createTestWorkflow(blocks: Array<{ id: string; name?: string; type?: string }> = []) {
|
||||
function createTestWorkflow(
|
||||
blocks: Array<{
|
||||
id: string
|
||||
name?: string
|
||||
type?: string
|
||||
outputs?: Record<string, any>
|
||||
}> = []
|
||||
) {
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks: blocks.map((b) => ({
|
||||
@@ -17,7 +21,7 @@ function createTestWorkflow(blocks: Array<{ id: string; name?: string; type?: st
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: b.type ?? 'function', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
outputs: b.outputs ?? {},
|
||||
metadata: { id: b.type ?? 'function', name: b.name ?? b.id },
|
||||
enabled: true,
|
||||
})),
|
||||
@@ -126,7 +130,7 @@ describe('BlockResolver', () => {
|
||||
expect(resolver.resolve('<source.items.1.id>', ctx)).toBe(2)
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for non-existent path', () => {
|
||||
it.concurrent('should return undefined for non-existent path when no schema defined', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'source' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
@@ -136,6 +140,48 @@ describe('BlockResolver', () => {
|
||||
expect(resolver.resolve('<source.nonexistent>', ctx)).toBeUndefined()
|
||||
})
|
||||
|
||||
it.concurrent('should throw error for path not in output schema', () => {
|
||||
const workflow = createTestWorkflow([
|
||||
{
|
||||
id: 'source',
|
||||
outputs: {
|
||||
validField: { type: 'string', description: 'A valid field' },
|
||||
nested: {
|
||||
child: { type: 'number', description: 'Nested child' },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { validField: 'value', nested: { child: 42 } },
|
||||
})
|
||||
|
||||
expect(() => resolver.resolve('<source.invalidField>', ctx)).toThrow(
|
||||
/"invalidField" doesn't exist on block "source"/
|
||||
)
|
||||
expect(() => resolver.resolve('<source.invalidField>', ctx)).toThrow(/Available fields:/)
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for path in schema but missing in data', () => {
|
||||
const workflow = createTestWorkflow([
|
||||
{
|
||||
id: 'source',
|
||||
outputs: {
|
||||
requiredField: { type: 'string', description: 'Always present' },
|
||||
optionalField: { type: 'string', description: 'Sometimes missing' },
|
||||
},
|
||||
},
|
||||
])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { requiredField: 'value' },
|
||||
})
|
||||
|
||||
expect(resolver.resolve('<source.requiredField>', ctx)).toBe('value')
|
||||
expect(resolver.resolve('<source.optionalField>', ctx)).toBeUndefined()
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for non-existent block', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'existing' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
|
||||
@@ -9,14 +9,75 @@ import {
|
||||
type ResolutionContext,
|
||||
type Resolver,
|
||||
} from '@/executor/variables/resolvers/reference'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
function isPathInOutputSchema(
|
||||
outputs: Record<string, any> | undefined,
|
||||
pathParts: string[]
|
||||
): boolean {
|
||||
if (!outputs || pathParts.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
let current: any = outputs
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const part = pathParts[i]
|
||||
|
||||
if (/^\d+$/.test(part)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (current === null || current === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (part in current) {
|
||||
current = current[part]
|
||||
continue
|
||||
}
|
||||
|
||||
if (current.properties && part in current.properties) {
|
||||
current = current.properties[part]
|
||||
continue
|
||||
}
|
||||
|
||||
if (current.type === 'array' && current.items) {
|
||||
if (current.items.properties && part in current.items.properties) {
|
||||
current = current.items.properties[part]
|
||||
continue
|
||||
}
|
||||
if (part in current.items) {
|
||||
current = current.items[part]
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if ('type' in current && typeof current.type === 'string') {
|
||||
if (!current.properties && !current.items) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function getSchemaFieldNames(outputs: Record<string, any> | undefined): string[] {
|
||||
if (!outputs) return []
|
||||
return Object.keys(outputs)
|
||||
}
|
||||
|
||||
export class BlockResolver implements Resolver {
|
||||
private nameToBlockId: Map<string, string>
|
||||
private blockById: Map<string, SerializedBlock>
|
||||
|
||||
constructor(private workflow: SerializedWorkflow) {
|
||||
this.nameToBlockId = new Map()
|
||||
this.blockById = new Map()
|
||||
for (const block of workflow.blocks) {
|
||||
this.blockById.set(block.id, block)
|
||||
if (block.metadata?.name) {
|
||||
this.nameToBlockId.set(normalizeName(block.metadata.name), block.id)
|
||||
}
|
||||
@@ -47,7 +108,9 @@ export class BlockResolver implements Resolver {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const block = this.blockById.get(blockId)
|
||||
const output = this.getBlockOutput(blockId, context)
|
||||
|
||||
if (output === undefined) {
|
||||
return undefined
|
||||
}
|
||||
@@ -63,9 +126,6 @@ export class BlockResolver implements Resolver {
|
||||
return result
|
||||
}
|
||||
|
||||
// If failed, check if we should try backwards compatibility fallback
|
||||
const block = this.workflow.blocks.find((b) => b.id === blockId)
|
||||
|
||||
// Response block backwards compatibility:
|
||||
// Old: <responseBlock.response.data> -> New: <responseBlock.data>
|
||||
// Only apply fallback if:
|
||||
@@ -108,6 +168,14 @@ export class BlockResolver implements Resolver {
|
||||
}
|
||||
}
|
||||
|
||||
const schemaFields = getSchemaFieldNames(block?.outputs)
|
||||
if (schemaFields.length > 0 && !isPathInOutputSchema(block?.outputs, pathParts)) {
|
||||
throw new Error(
|
||||
`"${pathParts.join('.')}" doesn't exist on block "${blockName}". ` +
|
||||
`Available fields: ${schemaFields.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user