Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions ee/codegen/src/__test__/nodes/inline-subworkflow-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
5 changes: 3 additions & 2 deletions ee/codegen/src/generators/nodes/bases/nested-workflow-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -68,7 +67,9 @@ export abstract class BaseNestedWorkflowNode<

protected generateNestedWorkflowContexts(): Map<string, WorkflowContext> {
const nestedWorkflowLabel = `${this.nodeContext.getNodeLabel()} Workflow`;
const nestedWorkflowClassName = createPythonClassName(nestedWorkflowLabel);
const nestedWorkflowClassName =
this.workflowContext.getUniqueClassName(nestedWorkflowLabel);
this.workflowContext.addUsedClassName(nestedWorkflowClassName);

const innerWorkflowData = this.getInnerWorkflowData();

Expand Down