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
450 lines
13 KiB
TypeScript
450 lines
13 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
|
|
import type { Edge } from 'reactflow'
|
|
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
|
import type { BlockState } from '@/stores/workflows/workflow/types'
|
|
|
|
vi.mock('@/stores/workflows/utils', () => ({
|
|
mergeSubblockState: vi.fn(),
|
|
}))
|
|
|
|
import { UNDO_REDO_OPERATIONS } from '@sim/realtime-protocol/constants'
|
|
import { mergeSubblockState } from '@/stores/workflows/utils'
|
|
import { captureLatestEdges, captureLatestSubBlockValues, createInverseOperation } from './utils'
|
|
|
|
const mockMergeSubblockState = mergeSubblockState as Mock
|
|
|
|
describe('captureLatestEdges', () => {
|
|
const createEdge = (id: string, source: string, target: string): Edge => ({
|
|
id,
|
|
source,
|
|
target,
|
|
})
|
|
|
|
it('should return edges where blockId is the source', () => {
|
|
const edges = [
|
|
createEdge('edge-1', 'block-1', 'block-2'),
|
|
createEdge('edge-2', 'block-3', 'block-4'),
|
|
]
|
|
|
|
const result = captureLatestEdges(edges, ['block-1'])
|
|
|
|
expect(result).toEqual([createEdge('edge-1', 'block-1', 'block-2')])
|
|
})
|
|
|
|
it('should return edges where blockId is the target', () => {
|
|
const edges = [
|
|
createEdge('edge-1', 'block-1', 'block-2'),
|
|
createEdge('edge-2', 'block-3', 'block-4'),
|
|
]
|
|
|
|
const result = captureLatestEdges(edges, ['block-2'])
|
|
|
|
expect(result).toEqual([createEdge('edge-1', 'block-1', 'block-2')])
|
|
})
|
|
|
|
it('should return edges for multiple blocks', () => {
|
|
const edges = [
|
|
createEdge('edge-1', 'block-1', 'block-2'),
|
|
createEdge('edge-2', 'block-3', 'block-4'),
|
|
createEdge('edge-3', 'block-2', 'block-5'),
|
|
]
|
|
|
|
const result = captureLatestEdges(edges, ['block-1', 'block-2'])
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result).toContainEqual(createEdge('edge-1', 'block-1', 'block-2'))
|
|
expect(result).toContainEqual(createEdge('edge-3', 'block-2', 'block-5'))
|
|
})
|
|
|
|
it('should return empty array when no edges match', () => {
|
|
const edges = [
|
|
createEdge('edge-1', 'block-1', 'block-2'),
|
|
createEdge('edge-2', 'block-3', 'block-4'),
|
|
]
|
|
|
|
const result = captureLatestEdges(edges, ['block-99'])
|
|
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('should return empty array when blockIds is empty', () => {
|
|
const edges = [
|
|
createEdge('edge-1', 'block-1', 'block-2'),
|
|
createEdge('edge-2', 'block-3', 'block-4'),
|
|
]
|
|
|
|
const result = captureLatestEdges(edges, [])
|
|
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('should return edge when block has both source and target edges', () => {
|
|
const edges = [
|
|
createEdge('edge-1', 'block-1', 'block-2'),
|
|
createEdge('edge-2', 'block-2', 'block-3'),
|
|
createEdge('edge-3', 'block-4', 'block-2'),
|
|
]
|
|
|
|
const result = captureLatestEdges(edges, ['block-2'])
|
|
|
|
expect(result).toHaveLength(3)
|
|
expect(result).toContainEqual(createEdge('edge-1', 'block-1', 'block-2'))
|
|
expect(result).toContainEqual(createEdge('edge-2', 'block-2', 'block-3'))
|
|
expect(result).toContainEqual(createEdge('edge-3', 'block-4', 'block-2'))
|
|
})
|
|
|
|
it('should handle empty edges array', () => {
|
|
const result = captureLatestEdges([], ['block-1'])
|
|
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('should not duplicate edges when block appears in multiple blockIds', () => {
|
|
const edges = [createEdge('edge-1', 'block-1', 'block-2')]
|
|
|
|
const result = captureLatestEdges(edges, ['block-1', 'block-2'])
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result).toContainEqual(createEdge('edge-1', 'block-1', 'block-2'))
|
|
})
|
|
})
|
|
|
|
describe('captureLatestSubBlockValues', () => {
|
|
const workflowId = 'wf-test'
|
|
|
|
const createBlockState = (
|
|
id: string,
|
|
subBlocks: Record<string, { id: string; type: string; value: unknown }>
|
|
): BlockState =>
|
|
({
|
|
id,
|
|
type: 'function',
|
|
name: 'Test Block',
|
|
position: { x: 0, y: 0 },
|
|
subBlocks: Object.fromEntries(
|
|
Object.entries(subBlocks).map(([subId, sb]) => [
|
|
subId,
|
|
{ id: sb.id, type: sb.type, value: sb.value },
|
|
])
|
|
),
|
|
outputs: {},
|
|
enabled: true,
|
|
}) as BlockState
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should capture single block with single subblock value', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
code: { id: 'code', type: 'code', value: 'console.log("hello")' },
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue(blocks)
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1'])
|
|
|
|
expect(result).toEqual({
|
|
'block-1': { code: 'console.log("hello")' },
|
|
})
|
|
})
|
|
|
|
it('should capture single block with multiple subblock values', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
code: { id: 'code', type: 'code', value: 'test code' },
|
|
model: { id: 'model', type: 'dropdown', value: 'gpt-4' },
|
|
temperature: { id: 'temperature', type: 'slider', value: 0.7 },
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue(blocks)
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1'])
|
|
|
|
expect(result).toEqual({
|
|
'block-1': {
|
|
code: 'test code',
|
|
model: 'gpt-4',
|
|
temperature: 0.7,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should capture multiple blocks with values', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
code: { id: 'code', type: 'code', value: 'code 1' },
|
|
}),
|
|
'block-2': createBlockState('block-2', {
|
|
prompt: { id: 'prompt', type: 'long-input', value: 'hello world' },
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockImplementation((_blocks, _wfId, blockId) => {
|
|
if (blockId === 'block-1') return { 'block-1': blocks['block-1'] }
|
|
if (blockId === 'block-2') return { 'block-2': blocks['block-2'] }
|
|
return {}
|
|
})
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1', 'block-2'])
|
|
|
|
expect(result).toEqual({
|
|
'block-1': { code: 'code 1' },
|
|
'block-2': { prompt: 'hello world' },
|
|
})
|
|
})
|
|
|
|
it('should skip null values', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
code: { id: 'code', type: 'code', value: 'valid code' },
|
|
empty: { id: 'empty', type: 'short-input', value: null },
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue(blocks)
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1'])
|
|
|
|
expect(result).toEqual({
|
|
'block-1': { code: 'valid code' },
|
|
})
|
|
expect(result['block-1']).not.toHaveProperty('empty')
|
|
})
|
|
|
|
it('should skip undefined values', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
code: { id: 'code', type: 'code', value: 'valid code' },
|
|
empty: { id: 'empty', type: 'short-input', value: undefined },
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue(blocks)
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1'])
|
|
|
|
expect(result).toEqual({
|
|
'block-1': { code: 'valid code' },
|
|
})
|
|
})
|
|
|
|
it('should return empty object for block with no subBlocks', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': {
|
|
id: 'block-1',
|
|
type: 'function',
|
|
name: 'Test Block',
|
|
position: { x: 0, y: 0 },
|
|
subBlocks: {},
|
|
outputs: {},
|
|
enabled: true,
|
|
} as BlockState,
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue(blocks)
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1'])
|
|
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
it('should return empty object for non-existent blockId', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
code: { id: 'code', type: 'code', value: 'test' },
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue({})
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['non-existent'])
|
|
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
it('should return empty object when blockIds is empty', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
code: { id: 'code', type: 'code', value: 'test' },
|
|
}),
|
|
}
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, [])
|
|
|
|
expect(result).toEqual({})
|
|
expect(mockMergeSubblockState).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle various value types (string, number, array)', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
text: { id: 'text', type: 'short-input', value: 'string value' },
|
|
number: { id: 'number', type: 'slider', value: 42 },
|
|
array: {
|
|
id: 'array',
|
|
type: 'table',
|
|
value: [
|
|
['a', 'b'],
|
|
['c', 'd'],
|
|
],
|
|
},
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue(blocks)
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1'])
|
|
|
|
expect(result).toEqual({
|
|
'block-1': {
|
|
text: 'string value',
|
|
number: 42,
|
|
array: [
|
|
['a', 'b'],
|
|
['c', 'd'],
|
|
],
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should only capture values for blockIds in the list', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
code: { id: 'code', type: 'code', value: 'code 1' },
|
|
}),
|
|
'block-2': createBlockState('block-2', {
|
|
code: { id: 'code', type: 'code', value: 'code 2' },
|
|
}),
|
|
'block-3': createBlockState('block-3', {
|
|
code: { id: 'code', type: 'code', value: 'code 3' },
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockImplementation((_blocks, _wfId, blockId) => {
|
|
if (blockId === 'block-1') return { 'block-1': blocks['block-1'] }
|
|
if (blockId === 'block-3') return { 'block-3': blocks['block-3'] }
|
|
return {}
|
|
})
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1', 'block-3'])
|
|
|
|
expect(result).toEqual({
|
|
'block-1': { code: 'code 1' },
|
|
'block-3': { code: 'code 3' },
|
|
})
|
|
expect(result).not.toHaveProperty('block-2')
|
|
})
|
|
|
|
it('should handle block without subBlocks property', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': {
|
|
id: 'block-1',
|
|
type: 'function',
|
|
name: 'Test Block',
|
|
position: { x: 0, y: 0 },
|
|
outputs: {},
|
|
enabled: true,
|
|
} as BlockState,
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue(blocks)
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1'])
|
|
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
it('should handle empty string values', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
code: { id: 'code', type: 'code', value: '' },
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue(blocks)
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1'])
|
|
|
|
expect(result).toEqual({
|
|
'block-1': { code: '' },
|
|
})
|
|
})
|
|
|
|
it('should handle zero numeric values', () => {
|
|
const blocks: Record<string, BlockState> = {
|
|
'block-1': createBlockState('block-1', {
|
|
temperature: { id: 'temperature', type: 'slider', value: 0 },
|
|
}),
|
|
}
|
|
|
|
mockMergeSubblockState.mockReturnValue(blocks)
|
|
|
|
const result = captureLatestSubBlockValues(blocks, workflowId, ['block-1'])
|
|
|
|
expect(result).toEqual({
|
|
'block-1': { temperature: 0 },
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('createInverseOperation', () => {
|
|
it('inverts batch subblock updates', () => {
|
|
const operation = {
|
|
id: 'op-1',
|
|
type: UNDO_REDO_OPERATIONS.BATCH_UPDATE_SUBBLOCKS,
|
|
timestamp: 1,
|
|
workflowId: 'workflow-1',
|
|
userId: 'user-1',
|
|
data: {
|
|
updates: [
|
|
{
|
|
blockId: 'block-1',
|
|
subBlockId: 'prompt',
|
|
before: 'old',
|
|
after: 'new',
|
|
},
|
|
],
|
|
subflowUpdates: [
|
|
{
|
|
blockId: 'loop-1',
|
|
blockType: 'loop',
|
|
fieldId: 'subflowIterations',
|
|
before: 2,
|
|
after: 3,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
expect(createInverseOperation(operation)).toEqual({
|
|
...operation,
|
|
data: {
|
|
updates: [
|
|
{
|
|
blockId: 'block-1',
|
|
subBlockId: 'prompt',
|
|
before: 'new',
|
|
after: 'old',
|
|
},
|
|
],
|
|
subflowUpdates: [
|
|
{
|
|
blockId: 'loop-1',
|
|
blockType: 'loop',
|
|
fieldId: 'subflowIterations',
|
|
before: 3,
|
|
after: 2,
|
|
},
|
|
],
|
|
},
|
|
})
|
|
})
|
|
})
|