Compare commits

..

4 Commits

Author SHA1 Message Date
Waleed Latif
1e55a0e044 v0.3.3: fix + improvement
v0.3.3: fix + improvement
2025-07-16 16:34:08 -07:00
Waleed Latif
e142753d64 improvement(permissions): remove the unused workspace_member table in favor of permissions (#710) 2025-07-16 16:28:20 -07:00
Waleed Latif
61deb02959 fix(subflows): fixed subflows not executing (#711) 2025-07-16 16:27:43 -07:00
Emir Karabeg
e52862166d fix: sidebar scroll going over sidebar height (#709) 2025-07-16 16:16:09 -07:00
10 changed files with 6296 additions and 42 deletions

View File

@@ -514,7 +514,7 @@ export function VoiceInterface({
const getButtonContent = () => {
if (state === 'agent_speaking') {
return (
<svg className='w-6 h-6' viewBox='0 0 24 24' fill='currentColor'>
<svg className='h-6 w-6' viewBox='0 0 24 24' fill='currentColor'>
<rect x='6' y='6' width='12' height='12' rx='2' />
</svg>
)
@@ -534,15 +534,15 @@ export function VoiceInterface({
isPlayingAudio={state === 'agent_speaking'}
isStreaming={isStreaming}
isMuted={isMuted}
className='w-80 h-80 md:w-96 md:h-96'
className='h-80 w-80 md:h-96 md:w-96'
/>
</div>
{/* Live transcript - subtitle style */}
<div className='mb-16 h-24 flex items-center justify-center'>
<div className='mb-16 flex h-24 items-center justify-center'>
{currentTranscript && (
<div className='max-w-2xl px-8'>
<p className='text-xl text-gray-700 text-center leading-relaxed overflow-hidden'>
<p className='overflow-hidden text-center text-gray-700 text-xl leading-relaxed'>
{currentTranscript}
</p>
</div>
@@ -550,7 +550,7 @@ export function VoiceInterface({
</div>
{/* Status */}
<p className='text-lg text-gray-600 mb-8 text-center'>
<p className='mb-8 text-center text-gray-600 text-lg'>
{getStatusText()}
{isMuted && <span className='ml-2 text-gray-400 text-sm'>(Muted)</span>}
</p>

View File

@@ -720,7 +720,7 @@ export function Sidebar() {
}`}
>
<div className='px-2'>
<ScrollArea ref={workflowScrollAreaRef} className='h-[212px]' hideScrollbar={true}>
<ScrollArea ref={workflowScrollAreaRef} className='h-[210px]' hideScrollbar={true}>
<FolderTree
regularWorkflows={regularWorkflows}
marketplaceWorkflows={tempWorkflows}

View File

@@ -0,0 +1,47 @@
CREATE TABLE "template_stars" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"template_id" text NOT NULL,
"starred_at" timestamp DEFAULT now() NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "templates" (
"id" text PRIMARY KEY NOT NULL,
"workflow_id" text NOT NULL,
"user_id" text NOT NULL,
"name" text NOT NULL,
"description" text,
"author" text NOT NULL,
"views" integer DEFAULT 0 NOT NULL,
"stars" integer DEFAULT 0 NOT NULL,
"color" text DEFAULT '#3972F6' NOT NULL,
"icon" text DEFAULT 'FileText' NOT NULL,
"category" text NOT NULL,
"state" jsonb NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
DROP TABLE "workspace_member" CASCADE;--> statement-breakpoint
ALTER TABLE "template_stars" ADD CONSTRAINT "template_stars_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "template_stars" ADD CONSTRAINT "template_stars_template_id_templates_id_fk" FOREIGN KEY ("template_id") REFERENCES "public"."templates"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "templates" ADD CONSTRAINT "templates_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "templates" ADD CONSTRAINT "templates_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "template_stars_user_id_idx" ON "template_stars" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "template_stars_template_id_idx" ON "template_stars" USING btree ("template_id");--> statement-breakpoint
CREATE INDEX "template_stars_user_template_idx" ON "template_stars" USING btree ("user_id","template_id");--> statement-breakpoint
CREATE INDEX "template_stars_template_user_idx" ON "template_stars" USING btree ("template_id","user_id");--> statement-breakpoint
CREATE INDEX "template_stars_starred_at_idx" ON "template_stars" USING btree ("starred_at");--> statement-breakpoint
CREATE INDEX "template_stars_template_starred_at_idx" ON "template_stars" USING btree ("template_id","starred_at");--> statement-breakpoint
CREATE UNIQUE INDEX "template_stars_user_template_unique" ON "template_stars" USING btree ("user_id","template_id");--> statement-breakpoint
CREATE INDEX "templates_workflow_id_idx" ON "templates" USING btree ("workflow_id");--> statement-breakpoint
CREATE INDEX "templates_user_id_idx" ON "templates" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "templates_category_idx" ON "templates" USING btree ("category");--> statement-breakpoint
CREATE INDEX "templates_views_idx" ON "templates" USING btree ("views");--> statement-breakpoint
CREATE INDEX "templates_stars_idx" ON "templates" USING btree ("stars");--> statement-breakpoint
CREATE INDEX "templates_category_views_idx" ON "templates" USING btree ("category","views");--> statement-breakpoint
CREATE INDEX "templates_category_stars_idx" ON "templates" USING btree ("category","stars");--> statement-breakpoint
CREATE INDEX "templates_user_category_idx" ON "templates" USING btree ("user_id","category");--> statement-breakpoint
CREATE INDEX "templates_created_at_idx" ON "templates" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "templates_updated_at_idx" ON "templates" USING btree ("updated_at");

File diff suppressed because it is too large Load Diff

View File

@@ -372,6 +372,13 @@
"when": 1752093722331,
"tag": "0053_gigantic_gabe_jones",
"breakpoints": true
},
{
"idx": 54,
"version": "7",
"when": 1752708227343,
"tag": "0054_naive_raider",
"breakpoints": true
}
]
}

View File

@@ -628,29 +628,6 @@ export const workspace = pgTable('workspace', {
updatedAt: timestamp('updated_at').notNull().defaultNow(),
})
// @deprecated - Use permissions table instead. This table is kept for backward compatibility during migration.
export const workspaceMember = pgTable(
'workspace_member',
{
id: text('id').primaryKey(),
workspaceId: text('workspace_id')
.notNull()
.references(() => workspace.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
role: text('role').notNull().default('member'), // e.g., 'owner', 'admin', 'member'
joinedAt: timestamp('joined_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => {
return {
// Create index on userId for fast lookups of workspaces by user
userIdIdx: uniqueIndex('user_workspace_idx').on(table.userId, table.workspaceId),
}
}
)
// Define the permission enum
export const permissionTypeEnum = pgEnum('permission_type', ['admin', 'write', 'read'])

View File

@@ -89,9 +89,9 @@ describe('Routing', () => {
})
describe('shouldSkipConnection', () => {
it.concurrent('should skip flow control blocks', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(true)
expect(Routing.shouldSkipConnection('source', BlockType.LOOP)).toBe(true)
it.concurrent('should allow regular connections to flow control blocks', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
expect(Routing.shouldSkipConnection('source', BlockType.LOOP)).toBe(false)
})
it.concurrent('should skip flow control specific connections', () => {
@@ -107,9 +107,9 @@ describe('Routing', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.API)).toBe(false)
})
it.concurrent('should not skip routing connections', () => {
expect(Routing.shouldSkipConnection('condition-test-if', BlockType.FUNCTION)).toBe(false)
expect(Routing.shouldSkipConnection('condition-test-else', BlockType.AGENT)).toBe(false)
it.concurrent('should skip condition-specific connections during selective activation', () => {
expect(Routing.shouldSkipConnection('condition-test-if', BlockType.FUNCTION)).toBe(true)
expect(Routing.shouldSkipConnection('condition-test-else', BlockType.AGENT)).toBe(true)
})
it.concurrent('should handle empty/undefined types', () => {

View File

@@ -79,12 +79,7 @@ export class Routing {
* Checks if a connection should be skipped during selective activation
*/
static shouldSkipConnection(sourceHandle: string | undefined, targetBlockType: string): boolean {
// Skip flow control blocks
if (Routing.shouldSkipInSelectiveActivation(targetBlockType)) {
return true
}
// Skip flow control specific connections
// Skip flow control specific connections (internal flow control handles)
const flowControlHandles = [
'parallel-start-source',
'parallel-end-source',
@@ -92,6 +87,19 @@ export class Routing {
'loop-end-source',
]
return flowControlHandles.includes(sourceHandle || '')
if (flowControlHandles.includes(sourceHandle || '')) {
return true
}
// Skip condition-specific connections during selective activation
// These should only be activated when the condition makes a specific decision
if (sourceHandle?.startsWith('condition-')) {
return true
}
// For regular connections (no special source handle), allow activation of flow control blocks
// This enables regular blocks (like agents) to activate parallel/loop blocks
// The flow control blocks themselves will handle active path checking
return false
}
}

View File

@@ -0,0 +1,131 @@
import { describe, expect, it } from 'vitest'
import { BlockType } from '@/executor/consts'
import { Routing } from '@/executor/routing/routing'
describe('Parallel Activation Integration - shouldSkipConnection behavior', () => {
describe('Regular blocks can activate parallel/loop blocks', () => {
it('should allow Agent → Parallel connections', () => {
// This was the original bug - agent couldn't activate parallel
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
expect(Routing.shouldSkipConnection('source', BlockType.PARALLEL)).toBe(false)
})
it('should allow Function → Parallel connections', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
expect(Routing.shouldSkipConnection('source', BlockType.PARALLEL)).toBe(false)
})
it('should allow API → Loop connections', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.LOOP)).toBe(false)
expect(Routing.shouldSkipConnection('source', BlockType.LOOP)).toBe(false)
})
it('should allow all regular blocks to activate parallel/loop', () => {
const regularBlocks = [
BlockType.FUNCTION,
BlockType.AGENT,
BlockType.API,
BlockType.EVALUATOR,
BlockType.RESPONSE,
BlockType.WORKFLOW,
]
regularBlocks.forEach((sourceBlockType) => {
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
expect(Routing.shouldSkipConnection(undefined, BlockType.LOOP)).toBe(false)
})
})
})
describe('✅ Still works: Router and Condition blocks can activate parallel/loop', () => {
it('should allow Router → Parallel connections', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
})
it('should allow Condition → Parallel connections', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
})
})
describe('✅ Still blocked: Internal flow control connections', () => {
it('should block parallel-start-source connections during selective activation', () => {
expect(Routing.shouldSkipConnection('parallel-start-source', BlockType.FUNCTION)).toBe(true)
expect(Routing.shouldSkipConnection('parallel-start-source', BlockType.AGENT)).toBe(true)
})
it('should block parallel-end-source connections during selective activation', () => {
expect(Routing.shouldSkipConnection('parallel-end-source', BlockType.FUNCTION)).toBe(true)
expect(Routing.shouldSkipConnection('parallel-end-source', BlockType.AGENT)).toBe(true)
})
it('should block loop-start-source connections during selective activation', () => {
expect(Routing.shouldSkipConnection('loop-start-source', BlockType.FUNCTION)).toBe(true)
expect(Routing.shouldSkipConnection('loop-start-source', BlockType.AGENT)).toBe(true)
})
it('should block loop-end-source connections during selective activation', () => {
expect(Routing.shouldSkipConnection('loop-end-source', BlockType.FUNCTION)).toBe(true)
expect(Routing.shouldSkipConnection('loop-end-source', BlockType.AGENT)).toBe(true)
})
})
describe('✅ Still blocked: Condition-specific connections during selective activation', () => {
it('should block condition-specific connections during selective activation', () => {
expect(Routing.shouldSkipConnection('condition-test-if', BlockType.FUNCTION)).toBe(true)
expect(Routing.shouldSkipConnection('condition-test-else', BlockType.AGENT)).toBe(true)
expect(Routing.shouldSkipConnection('condition-some-id', BlockType.PARALLEL)).toBe(true)
})
})
describe('✅ Still works: Regular connections', () => {
it('should allow regular connections between regular blocks', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.FUNCTION)).toBe(false)
expect(Routing.shouldSkipConnection('source', BlockType.AGENT)).toBe(false)
expect(Routing.shouldSkipConnection('output', BlockType.API)).toBe(false)
})
it('should allow regular connections with any source handle (except blocked ones)', () => {
expect(Routing.shouldSkipConnection('result', BlockType.FUNCTION)).toBe(false)
expect(Routing.shouldSkipConnection('output', BlockType.AGENT)).toBe(false)
expect(Routing.shouldSkipConnection('data', BlockType.PARALLEL)).toBe(false)
})
})
})
describe('Real-world workflow scenarios', () => {
describe('✅ Working: User workflows', () => {
it('should support: Start → Agent → Parallel → Agent pattern', () => {
// This is the user's exact workflow pattern that was broken
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
})
it('should support: Start → Function → Loop → Function pattern', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.LOOP)).toBe(false)
})
it('should support: Start → API → Parallel → Multiple Agents pattern', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
})
it('should support: Start → Evaluator → Parallel → Response pattern', () => {
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
})
})
describe('✅ Working: Complex routing patterns', () => {
it('should support: Start → Router → Parallel → Function (existing working pattern)', () => {
// This already worked before the fix
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
})
it('should support: Start → Condition → Parallel → Agent (existing working pattern)', () => {
// This already worked before the fix
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
})
it('should support: Start → Router → Function → Parallel → Agent (new working pattern)', () => {
// Router selects function, function activates parallel
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(false)
})
})
})

View File

@@ -0,0 +1,545 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { BlockType } from '@/executor/consts'
import { PathTracker } from '@/executor/path/path'
import type { ExecutionContext } from '@/executor/types'
import type { SerializedWorkflow } from '@/serializer/types'
describe('Parallel Block Activation Regression Tests', () => {
let pathTracker: PathTracker
let mockContext: ExecutionContext
const createMockContext = (workflow: SerializedWorkflow): ExecutionContext => ({
workflowId: 'test-workflow',
blockStates: new Map(),
blockLogs: [],
metadata: { duration: 0 },
environmentVariables: {},
decisions: { router: new Map(), condition: new Map() },
loopIterations: new Map(),
loopItems: new Map(),
executedBlocks: new Set(),
activeExecutionPath: new Set(['start']),
completedLoops: new Set(),
workflow,
})
describe('Original Bug: Agent → Parallel should work', () => {
beforeEach(() => {
// The exact scenario from the user's non-working workflow
const workflow: SerializedWorkflow = {
version: '2.0',
blocks: [
{
id: 'start',
metadata: { id: BlockType.STARTER, name: 'Start' },
position: { x: 0, y: 0 },
config: { tool: BlockType.STARTER, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'agent-1',
metadata: { id: BlockType.AGENT, name: 'Agent 1' },
position: { x: 200, y: 0 },
config: { tool: BlockType.AGENT, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'parallel-1',
metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' },
position: { x: 400, y: 0 },
config: { tool: BlockType.PARALLEL, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'agent-2',
metadata: { id: BlockType.AGENT, name: 'Agent 2' },
position: { x: 600, y: 0 },
config: { tool: BlockType.AGENT, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
],
connections: [
{ source: 'start', target: 'agent-1' },
{ source: 'agent-1', target: 'parallel-1' }, // This was broken!
{ source: 'parallel-1', target: 'agent-2', sourceHandle: 'parallel-start-source' },
],
loops: {},
parallels: {
'parallel-1': {
id: 'parallel-1',
nodes: ['agent-2'],
count: 3,
parallelType: 'count',
},
},
}
pathTracker = new PathTracker(workflow)
mockContext = createMockContext(workflow)
})
it('should allow agent to activate parallel block', () => {
// Agent 1 executes successfully
mockContext.blockStates.set('agent-1', {
output: { content: 'Agent response', usage: { tokens: 100 } },
executed: true,
executionTime: 1000,
})
mockContext.executedBlocks.add('agent-1')
mockContext.activeExecutionPath.add('agent-1')
// Update paths after agent execution
pathTracker.updateExecutionPaths(['agent-1'], mockContext)
// ✅ The parallel block should be activated
expect(mockContext.activeExecutionPath.has('parallel-1')).toBe(true)
})
it('should not activate parallel-start-source connections during path updates', () => {
// Set up parallel block as executed
mockContext.blockStates.set('parallel-1', {
output: { parallelId: 'parallel-1', parallelCount: 3, started: true },
executed: true,
executionTime: 100,
})
mockContext.executedBlocks.add('parallel-1')
mockContext.activeExecutionPath.add('parallel-1')
// Update paths after parallel execution
pathTracker.updateExecutionPaths(['parallel-1'], mockContext)
// ✅ The child agent should NOT be activated via PathTracker (parallel handler manages this)
expect(mockContext.activeExecutionPath.has('agent-2')).toBe(false)
})
})
describe('Regression: Router → Parallel should still work', () => {
beforeEach(() => {
// The working scenario that should continue to work
const workflow: SerializedWorkflow = {
version: '2.0',
blocks: [
{
id: 'start',
metadata: { id: BlockType.STARTER, name: 'Start' },
position: { x: 0, y: 0 },
config: { tool: BlockType.STARTER, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'router-1',
metadata: { id: BlockType.ROUTER, name: 'Router 1' },
position: { x: 200, y: 0 },
config: { tool: BlockType.ROUTER, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'parallel-1',
metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' },
position: { x: 400, y: 0 },
config: { tool: BlockType.PARALLEL, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'function-1',
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
position: { x: 600, y: 0 },
config: { tool: BlockType.FUNCTION, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
],
connections: [
{ source: 'start', target: 'router-1' },
{ source: 'router-1', target: 'parallel-1' },
{ source: 'parallel-1', target: 'function-1', sourceHandle: 'parallel-start-source' },
],
loops: {},
parallels: {
'parallel-1': {
id: 'parallel-1',
nodes: ['function-1'],
count: 2,
parallelType: 'count',
},
},
}
pathTracker = new PathTracker(workflow)
mockContext = createMockContext(workflow)
})
it('should allow router to activate parallel block', () => {
// Router executes and selects parallel
mockContext.blockStates.set('router-1', {
output: {
selectedPath: { blockId: 'parallel-1', blockType: BlockType.PARALLEL },
reasoning: 'Going to parallel',
},
executed: true,
executionTime: 500,
})
mockContext.executedBlocks.add('router-1')
mockContext.activeExecutionPath.add('router-1')
// Update paths after router execution
pathTracker.updateExecutionPaths(['router-1'], mockContext)
// ✅ Router should activate parallel block
expect(mockContext.activeExecutionPath.has('parallel-1')).toBe(true)
})
})
describe('Regression: Condition → Parallel should still work', () => {
beforeEach(() => {
const workflow: SerializedWorkflow = {
version: '2.0',
blocks: [
{
id: 'start',
metadata: { id: BlockType.STARTER, name: 'Start' },
position: { x: 0, y: 0 },
config: { tool: BlockType.STARTER, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'condition-1',
metadata: { id: BlockType.CONDITION, name: 'Condition 1' },
position: { x: 200, y: 0 },
config: { tool: BlockType.CONDITION, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'parallel-1',
metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' },
position: { x: 400, y: 0 },
config: { tool: BlockType.PARALLEL, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'function-1',
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
position: { x: 400, y: 200 },
config: { tool: BlockType.FUNCTION, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'agent-1',
metadata: { id: BlockType.AGENT, name: 'Agent 1' },
position: { x: 600, y: 0 },
config: { tool: BlockType.AGENT, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
],
connections: [
{ source: 'start', target: 'condition-1' },
{ source: 'condition-1', target: 'parallel-1', sourceHandle: 'condition-if' },
{ source: 'condition-1', target: 'function-1', sourceHandle: 'condition-else' },
{ source: 'parallel-1', target: 'agent-1', sourceHandle: 'parallel-start-source' },
],
loops: {},
parallels: {
'parallel-1': {
id: 'parallel-1',
nodes: ['agent-1'],
count: 2,
parallelType: 'count',
},
},
}
pathTracker = new PathTracker(workflow)
mockContext = createMockContext(workflow)
})
it('should allow condition to activate parallel block when if condition is met', () => {
// Condition executes and selects if path (parallel)
mockContext.blockStates.set('condition-1', {
output: {
selectedConditionId: 'if',
conditionResult: true,
selectedPath: { blockId: 'parallel-1', blockType: BlockType.PARALLEL },
},
executed: true,
executionTime: 200,
})
mockContext.executedBlocks.add('condition-1')
mockContext.activeExecutionPath.add('condition-1')
// Update paths after condition execution
pathTracker.updateExecutionPaths(['condition-1'], mockContext)
// ✅ Condition should activate parallel block
expect(mockContext.activeExecutionPath.has('parallel-1')).toBe(true)
// ✅ Function should NOT be activated (else path)
expect(mockContext.activeExecutionPath.has('function-1')).toBe(false)
})
it('should allow condition to activate function block when else condition is met', () => {
// Condition executes and selects else path (function)
mockContext.blockStates.set('condition-1', {
output: {
selectedConditionId: 'else',
conditionResult: false,
selectedPath: { blockId: 'function-1', blockType: BlockType.FUNCTION },
},
executed: true,
executionTime: 200,
})
mockContext.executedBlocks.add('condition-1')
mockContext.activeExecutionPath.add('condition-1')
// Update paths after condition execution
pathTracker.updateExecutionPaths(['condition-1'], mockContext)
// ✅ Function should be activated (else path)
expect(mockContext.activeExecutionPath.has('function-1')).toBe(true)
// ✅ Parallel should NOT be activated (if path)
expect(mockContext.activeExecutionPath.has('parallel-1')).toBe(false)
})
})
describe('Regression: All regular blocks should activate parallel/loop', () => {
it.each([
{ blockType: BlockType.FUNCTION, name: 'Function' },
{ blockType: BlockType.AGENT, name: 'Agent' },
{ blockType: BlockType.API, name: 'API' },
{ blockType: BlockType.EVALUATOR, name: 'Evaluator' },
{ blockType: BlockType.RESPONSE, name: 'Response' },
{ blockType: BlockType.WORKFLOW, name: 'Workflow' },
])('should allow $name → Parallel activation', ({ blockType, name }) => {
const workflow: SerializedWorkflow = {
version: '2.0',
blocks: [
{
id: 'start',
metadata: { id: BlockType.STARTER, name: 'Start' },
position: { x: 0, y: 0 },
config: { tool: BlockType.STARTER, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'regular-block',
metadata: { id: blockType, name },
position: { x: 200, y: 0 },
config: { tool: blockType, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'parallel-1',
metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' },
position: { x: 400, y: 0 },
config: { tool: BlockType.PARALLEL, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'target-function',
metadata: { id: BlockType.FUNCTION, name: 'Target Function' },
position: { x: 600, y: 0 },
config: { tool: BlockType.FUNCTION, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
],
connections: [
{ source: 'start', target: 'regular-block' },
{ source: 'regular-block', target: 'parallel-1' },
{
source: 'parallel-1',
target: 'target-function',
sourceHandle: 'parallel-start-source',
},
],
loops: {},
parallels: {
'parallel-1': {
id: 'parallel-1',
nodes: ['target-function'],
count: 2,
parallelType: 'count',
},
},
}
pathTracker = new PathTracker(workflow)
mockContext = createMockContext(workflow)
// Regular block executes
mockContext.blockStates.set('regular-block', {
output: { result: 'Success' },
executed: true,
executionTime: 100,
})
mockContext.executedBlocks.add('regular-block')
mockContext.activeExecutionPath.add('regular-block')
// Update paths after regular block execution
pathTracker.updateExecutionPaths(['regular-block'], mockContext)
// ✅ The parallel block should be activated
expect(mockContext.activeExecutionPath.has('parallel-1')).toBe(true)
})
})
describe('Regression: Internal flow control connections should still be blocked', () => {
it('should prevent activation of parallel-start-source connections during selective activation', () => {
const workflow: SerializedWorkflow = {
version: '2.0',
blocks: [
{
id: 'function-1',
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
position: { x: 0, y: 0 },
config: { tool: BlockType.FUNCTION, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'agent-1',
metadata: { id: BlockType.AGENT, name: 'Agent 1' },
position: { x: 200, y: 0 },
config: { tool: BlockType.AGENT, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
],
connections: [
// This is an internal flow control connection that should be blocked
{ source: 'function-1', target: 'agent-1', sourceHandle: 'parallel-start-source' },
],
loops: {},
parallels: {},
}
pathTracker = new PathTracker(workflow)
mockContext = createMockContext(workflow)
// Function 1 executes
mockContext.blockStates.set('function-1', {
output: { result: 'Success' },
executed: true,
executionTime: 100,
})
mockContext.executedBlocks.add('function-1')
mockContext.activeExecutionPath.add('function-1')
// Update paths after function execution
pathTracker.updateExecutionPaths(['function-1'], mockContext)
// ❌ Agent should NOT be activated via parallel-start-source during selective activation
expect(mockContext.activeExecutionPath.has('agent-1')).toBe(false)
})
})
describe('Edge Cases', () => {
it('should handle loop blocks the same way as parallel blocks', () => {
const workflow: SerializedWorkflow = {
version: '2.0',
blocks: [
{
id: 'start',
metadata: { id: BlockType.STARTER, name: 'Start' },
position: { x: 0, y: 0 },
config: { tool: BlockType.STARTER, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'function-1',
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
position: { x: 200, y: 0 },
config: { tool: BlockType.FUNCTION, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'loop-1',
metadata: { id: BlockType.LOOP, name: 'Loop 1' },
position: { x: 400, y: 0 },
config: { tool: BlockType.LOOP, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
{
id: 'agent-1',
metadata: { id: BlockType.AGENT, name: 'Agent 1' },
position: { x: 600, y: 0 },
config: { tool: BlockType.AGENT, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
],
connections: [
{ source: 'start', target: 'function-1' },
{ source: 'function-1', target: 'loop-1' }, // Function → Loop should work
{ source: 'loop-1', target: 'agent-1', sourceHandle: 'loop-start-source' },
],
loops: {
'loop-1': {
id: 'loop-1',
nodes: ['agent-1'],
iterations: 3,
loopType: 'for',
},
},
parallels: {},
}
pathTracker = new PathTracker(workflow)
mockContext = createMockContext(workflow)
// Function 1 executes
mockContext.blockStates.set('function-1', {
output: { result: 'Success' },
executed: true,
executionTime: 100,
})
mockContext.executedBlocks.add('function-1')
mockContext.activeExecutionPath.add('function-1')
// Update paths after function execution
pathTracker.updateExecutionPaths(['function-1'], mockContext)
// ✅ Function should be able to activate loop block
expect(mockContext.activeExecutionPath.has('loop-1')).toBe(true)
})
})
})