diff --git a/ee/codegen/src/__test__/nodes/__snapshots__/inline-subworkflow-node.test.ts.snap b/ee/codegen/src/__test__/nodes/__snapshots__/inline-subworkflow-node.test.ts.snap index 68198916d..d22c88cb7 100644 --- a/ee/codegen/src/__test__/nodes/__snapshots__/inline-subworkflow-node.test.ts.snap +++ b/ee/codegen/src/__test__/nodes/__snapshots__/inline-subworkflow-node.test.ts.snap @@ -158,6 +158,42 @@ class MyNode1Display(BaseTemplatingNodeDisplay[MyNode1]): " `; +exports[`InlineSubworkflowNode > nested workflow class name collision > should generate unique nested workflow class name when internal node has same name 1`] = ` +"from vellum.workflows.nodes.displayable import InlineSubworkflowNode + +from .workflow import MyNodeWorkflow + + +class MyNode(InlineSubworkflowNode): + subworkflow = MyNodeWorkflow +" +`; + +exports[`InlineSubworkflowNode > nested workflow class name collision > should generate unique nested workflow class name when internal node has same name 2`] = ` +"from vellum.workflows import BaseWorkflow + +from .nodes.my_node_workflow import MyNodeWorkflow1 + + +class MyNodeWorkflow(BaseWorkflow): + graph = MyNodeWorkflow1 + + class Outputs(BaseWorkflow.Outputs): + final_output = MyNodeWorkflow1.Outputs.result +" +`; + +exports[`InlineSubworkflowNode > nested workflow class name collision > should generate unique nested workflow class name when internal node has same name 3`] = ` +"from vellum.workflows.nodes.displayable import TemplatingNode +from vellum.workflows.state import BaseState + + +class MyNodeWorkflow1(TemplatingNode[BaseState, str]): + template = """Hello, World!""" + inputs = {} +" +`; + exports[`InlineSubworkflowNode > with state > inline subworkflow node file with state should include all three generic type parameters 1`] = ` "from vellum.workflows.nodes.displayable import InlineSubworkflowNode as BaseInlineSubworkflowNode from vellum.workflows.state import BaseState diff --git a/ee/codegen/src/__test__/nodes/inline-subworkflow-node.test.ts b/ee/codegen/src/__test__/nodes/inline-subworkflow-node.test.ts index ed08e51d2..2c6229b8f 100644 --- a/ee/codegen/src/__test__/nodes/inline-subworkflow-node.test.ts +++ b/ee/codegen/src/__test__/nodes/inline-subworkflow-node.test.ts @@ -226,6 +226,73 @@ describe("InlineSubworkflowNode", () => { }); }); + describe("nested workflow class name collision", () => { + /** + * Tests that when an internal node has the same name as what the nested workflow + * class would be named, the nested workflow class gets a unique name. + * This is the APO-2207 scenario where: + * - Outer inline subworkflow node labeled "My Node" → nested workflow class "MyNodeWorkflow" + * - Internal node labeled "My Node Workflow" → would also be "MyNodeWorkflow" + * The fix ensures the nested workflow class gets renamed to avoid collision. + */ + beforeEach(async () => { + const workflowContext = workflowContextFactory({ + absolutePathToOutputDirectory: tempDir, + moduleName: "code", + }); + + // GIVEN an inline subworkflow node labeled "My Node" + // AND an internal node labeled "My Node Workflow" (same as what the nested workflow class would be named) + const nodeData = inlineSubworkflowNodeDataFactory({ + label: "My Node", + nodes: [templatingNodeFactory({ label: "My Node Workflow" }).build()], + }).build(); + + const nodeContext = (await createNodeContext({ + workflowContext, + nodeData, + })) as InlineSubworkflowNodeContext; + + const node = new InlineSubworkflowNode({ + workflowContext, + nodeContext, + }); + + // WHEN we generate the code + await node.persist(); + }); + + it(`should generate unique nested workflow class name when internal node has same name`, async () => { + // THEN the outer node file should reference a unique workflow class name + const outerNodeContent = await readFile( + join(tempDir, "code", "nodes", "my_node", "__init__.py"), + "utf-8" + ); + expect(outerNodeContent).toMatchSnapshot(); + + // AND the nested workflow file should have the unique class name + const workflowContent = await readFile( + join(tempDir, "code", "nodes", "my_node", "workflow.py"), + "utf-8" + ); + expect(workflowContent).toMatchSnapshot(); + + // AND the internal node should keep its original class name + const internalNodeContent = await readFile( + join( + tempDir, + "code", + "nodes", + "my_node", + "nodes", + "my_node_workflow.py" + ), + "utf-8" + ); + expect(internalNodeContent).toMatchSnapshot(); + }); + }); + describe("with state", () => { beforeEach(async () => { const workflowContext = workflowContextFactory({ diff --git a/ee/codegen/src/generators/nodes/bases/nested-workflow-base.ts b/ee/codegen/src/generators/nodes/bases/nested-workflow-base.ts index fe5ff2872..2b014ae82 100644 --- a/ee/codegen/src/generators/nodes/bases/nested-workflow-base.ts +++ b/ee/codegen/src/generators/nodes/bases/nested-workflow-base.ts @@ -9,7 +9,6 @@ import { NodeAttributeGenerationError } from "src/generators/errors"; import { StrInstantiation } from "src/generators/extensions/str-instantiation"; import { WorkflowProjectGenerator } from "src/project"; import { WorkflowDataNode, WorkflowRawData } from "src/types/vellum"; -import { createPythonClassName } from "src/utils/casing"; export abstract class BaseNestedWorkflowNode< T extends WorkflowDataNode, @@ -68,7 +67,9 @@ export abstract class BaseNestedWorkflowNode< protected generateNestedWorkflowContexts(): Map { const nestedWorkflowLabel = `${this.nodeContext.getNodeLabel()} Workflow`; - const nestedWorkflowClassName = createPythonClassName(nestedWorkflowLabel); + const nestedWorkflowClassName = + this.workflowContext.getUniqueClassName(nestedWorkflowLabel); + this.workflowContext.addUsedClassName(nestedWorkflowClassName); const innerWorkflowData = this.getInnerWorkflowData();