mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
v0.3.3: fix + improvement
v0.3.3: fix + improvement
This commit is contained in:
47
apps/sim/db/migrations/0054_naive_raider.sql
Normal file
47
apps/sim/db/migrations/0054_naive_raider.sql
Normal 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");
|
||||
5539
apps/sim/db/migrations/meta/0054_snapshot.json
Normal file
5539
apps/sim/db/migrations/meta/0054_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
131
apps/sim/executor/tests/parallel-activation-integration.test.ts
Normal file
131
apps/sim/executor/tests/parallel-activation-integration.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
545
apps/sim/executor/tests/parallel-activation-regression.test.ts
Normal file
545
apps/sim/executor/tests/parallel-activation-regression.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user