{/* Organization & Name */}
vi.fn())
const mockUseNodesInteractions = vi.hoisted(() => vi.fn())
@@ -38,6 +40,9 @@ vi.mock('reactflow', () => ({
Right: 'right',
Left: 'left',
},
+ useStoreApi: () => ({
+ getState: () => ({ getNodes: () => [] }),
+ }),
}))
vi.mock('../hooks/use-available-blocks', async (importOriginal) => {
@@ -81,8 +86,10 @@ describe('CustomEdge', () => {
})
})
- it('should render a gradient edge and its real insert-node trigger', () => {
- render(
+ it('should render a gradient edge and hide the start tab from its insert-node selector', async () => {
+ const user = userEvent.setup()
+
+ renderWorkflowComponent(
{
opacity: '0.7',
zIndex: '1001',
})
+
+ await user.click(addBlockTrigger)
+
+ expect(screen.queryByRole('tab', { name: 'workflow.tabs.start' })).not.toBeInTheDocument()
})
it('should prefer the running stroke color when the edge is selected', () => {
- render(
+ renderWorkflowComponent(
{
})
it('should use the fail-branch running color while the connected node is hovering', () => {
- render(
+ renderWorkflowComponent(
{
})
it('should fall back to the default edge color when no highlight state is active', () => {
- render(
+ renderWorkflowComponent(
{
})
describe('inContainer filtering', () => {
- it('should exclude Iteration, Loop, End, DataSource, KnowledgeBase, HumanInput when inContainer=true', () => {
+ it('should allow HumanInput while excluding unsupported blocks when inContainer=true', () => {
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM, true), {
hooksStoreProps,
})
@@ -155,7 +155,7 @@ describe('useAvailableBlocks', () => {
expect(result.current.availableNextBlocks).not.toContain(BlockEnum.End)
expect(result.current.availableNextBlocks).not.toContain(BlockEnum.DataSource)
expect(result.current.availableNextBlocks).not.toContain(BlockEnum.KnowledgeBase)
- expect(result.current.availableNextBlocks).not.toContain(BlockEnum.HumanInput)
+ expect(result.current.availableNextBlocks).toContain(BlockEnum.HumanInput)
})
it('should exclude LoopEnd when not in container', () => {
diff --git a/web/app/components/workflow/hooks/__tests__/use-nodes-interactions.spec.ts b/web/app/components/workflow/hooks/__tests__/use-nodes-interactions.spec.ts
index 0eab5ad8af2..2d2353b6971 100644
--- a/web/app/components/workflow/hooks/__tests__/use-nodes-interactions.spec.ts
+++ b/web/app/components/workflow/hooks/__tests__/use-nodes-interactions.spec.ts
@@ -1183,15 +1183,14 @@ describe('useNodesInteractions', () => {
)
})
- // Nested container paste restrictions should stay aligned with available block filtering.
- describe('nested container paste restrictions', () => {
+ // Nested container paste behavior should stay aligned with available block filtering.
+ describe('nested container paste behavior', () => {
const disallowedNestedPasteNodeTypes = [
BlockEnum.End,
BlockEnum.Iteration,
BlockEnum.Loop,
BlockEnum.DataSource,
BlockEnum.KnowledgeBase,
- BlockEnum.HumanInput,
]
const createNodeMeta = (type: BlockEnum) => ({
@@ -1205,7 +1204,7 @@ describe('useNodesInteractions', () => {
},
})
- const runDisallowedPasteScenario = async (
+ const pasteNodeIntoContainer = async (
containerType: BlockEnum.Iteration | BlockEnum.Loop,
nodeType: BlockEnum,
) => {
@@ -1263,23 +1262,48 @@ describe('useNodesInteractions', () => {
const pastedNodes = rfState.setNodes.mock.calls.at(-1)?.[0] as Node[]
- expect(pastedNodes).toHaveLength(1)
- expect(pastedNodes[0]?.id).toBe(containerId)
- expect(pastedNodes[0]?.data._children).toEqual([])
- expect(
- pastedNodes.some((node) => node.data.type === nodeType && node.parentId === containerId),
- ).toBe(false)
+ return { containerId, pastedNodes }
}
it.each(disallowedNestedPasteNodeTypes)(
'should not paste %s into an iteration container',
async (nodeType) => {
- await runDisallowedPasteScenario(BlockEnum.Iteration, nodeType)
+ const { containerId, pastedNodes } = await pasteNodeIntoContainer(
+ BlockEnum.Iteration,
+ nodeType,
+ )
+
+ expect(pastedNodes).toHaveLength(1)
+ expect(pastedNodes[0]?.id).toBe(containerId)
+ expect(pastedNodes[0]?.data._children).toEqual([])
},
)
- it('should not paste human-input into a loop container', async () => {
- await runDisallowedPasteScenario(BlockEnum.Loop, BlockEnum.HumanInput)
- })
+ it.each([BlockEnum.Iteration, BlockEnum.Loop] as const)(
+ 'should paste human-input into a %s container',
+ async (containerType) => {
+ const { containerId, pastedNodes } = await pasteNodeIntoContainer(
+ containerType,
+ BlockEnum.HumanInput,
+ )
+ const container = pastedNodes.find((node) => node.id === containerId)
+ const pastedHumanInput = pastedNodes.find(
+ (node) => node.data.type === BlockEnum.HumanInput && node.parentId === containerId,
+ )
+ const isIteration = containerType === BlockEnum.Iteration
+
+ expect(pastedHumanInput).toBeDefined()
+ expect(pastedHumanInput?.data).toMatchObject({
+ isInIteration: isIteration,
+ iteration_id: isIteration ? containerId : undefined,
+ isInLoop: !isIteration,
+ loop_id: isIteration ? undefined : containerId,
+ })
+ expect(container?.data._children).toContainEqual({
+ nodeId: pastedHumanInput?.id,
+ nodeType: BlockEnum.HumanInput,
+ })
+ },
+ )
})
})
diff --git a/web/app/components/workflow/hooks/use-available-blocks.ts b/web/app/components/workflow/hooks/use-available-blocks.ts
index 675a36be49a..6ebb1933f28 100644
--- a/web/app/components/workflow/hooks/use-available-blocks.ts
+++ b/web/app/components/workflow/hooks/use-available-blocks.ts
@@ -11,8 +11,7 @@ const availableBlocksFilter = (nodeType: BlockEnum, inContainer?: boolean) => {
nodeType === BlockEnum.Loop ||
nodeType === BlockEnum.End ||
nodeType === BlockEnum.DataSource ||
- nodeType === BlockEnum.KnowledgeBase ||
- nodeType === BlockEnum.HumanInput)
+ nodeType === BlockEnum.KnowledgeBase)
)
return false
diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts
index 4b2ea7bf6a8..6d38ddf853f 100644
--- a/web/app/components/workflow/hooks/use-nodes-interactions.ts
+++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts
@@ -1786,7 +1786,6 @@ export const useNodesInteractions = () => {
BlockEnum.Loop,
BlockEnum.DataSource,
BlockEnum.KnowledgeBase,
- BlockEnum.HumanInput,
]
// Same-canvas copy keeps the source container selected, so only treat a
// selected container as the paste target when it is not part of the clipboard.
diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx
index 490f656f284..b659664ddfb 100644
--- a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx
+++ b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react'
import type { CommonNodeType } from '@/app/components/workflow/types'
import { fireEvent, render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
import { BlockEnum } from '@/app/components/workflow/types'
import { NodeSourceHandle, NodeTargetHandle } from '../node-handle'
@@ -210,6 +211,16 @@ describe('node-handle', () => {
// Target-side tests cover selector visibility, connection locking, and status rendering.
describe('NodeTargetHandle', () => {
+ it('should show the start tab when adding a node before the target node', async () => {
+ const user = userEvent.setup()
+
+ renderTargetHandle()
+
+ await user.click(screen.getByTestId('handle-target-handle'))
+
+ expect(screen.getByRole('tab', { name: 'workflow.tabs.start' })).toBeInTheDocument()
+ })
+
it('should toggle the target add trigger', () => {
renderTargetHandle()
@@ -260,6 +271,16 @@ describe('node-handle', () => {
// Source-side tests cover selector opening paths, previous-node selection, and status styling.
describe('NodeSourceHandle', () => {
+ it('should show the start tab when adding a node after the source node', async () => {
+ const user = userEvent.setup()
+
+ renderSourceHandle()
+
+ await user.click(screen.getByTestId('handle-source-handle'))
+
+ expect(screen.getByRole('tab', { name: 'workflow.tabs.start' })).toBeInTheDocument()
+ })
+
it('should toggle the source add trigger', () => {
renderSourceHandle()
diff --git a/web/app/components/workflow/nodes/_base/components/node-handle.tsx b/web/app/components/workflow/nodes/_base/components/node-handle.tsx
index 955e5eb2f87..30a22ab512c 100644
--- a/web/app/components/workflow/nodes/_base/components/node-handle.tsx
+++ b/web/app/components/workflow/nodes/_base/components/node-handle.tsx
@@ -79,6 +79,7 @@ export const NodeTargetHandle = memo(
'z-1 size-4! rounded-none! border-none! bg-transparent! outline-hidden!',
'after:absolute after:top-1 after:left-1.5 after:h-2 after:w-0.5 after:bg-workflow-link-line-handle',
'transition-all hover:scale-125',
+ open && 'scale-125',
data._runningStatus === NodeRunningStatus.Succeeded &&
'after:bg-workflow-link-line-success-handle',
data._runningStatus === NodeRunningStatus.Failed &&
@@ -106,6 +107,7 @@ export const NodeTargetHandle = memo(
nextNodeTargetHandle: handleId,
}}
placement="left"
+ showStartTab
triggerClassName={`
absolute left-0 top-0 opacity-0 pointer-events-none transition-opacity duration-150
${nodeSelectorClassName}
@@ -206,6 +208,7 @@ export const NodeSourceHandle = memo(
'group/handle z-1 size-4! rounded-none! border-none! bg-transparent! outline-hidden!',
'after:absolute after:top-1 after:right-1.5 after:h-2 after:w-0.5 after:bg-workflow-link-line-handle',
'transition-all hover:scale-125',
+ open && 'scale-125',
data._runningStatus === NodeRunningStatus.Succeeded &&
'after:bg-workflow-link-line-success-handle',
data._runningStatus === NodeRunningStatus.Failed &&
@@ -252,6 +255,7 @@ export const NodeSourceHandle = memo(
data-popup-open:opacity-100
`}
availableBlocksTypes={availableNextBlocks}
+ showStartTab
/>
)}
diff --git a/web/context/__tests__/console-bootstrap.spec.tsx b/web/context/__tests__/console-bootstrap.spec.tsx
index 9e0e66bbbb1..3287a77cd5a 100644
--- a/web/context/__tests__/console-bootstrap.spec.tsx
+++ b/web/context/__tests__/console-bootstrap.spec.tsx
@@ -186,6 +186,8 @@ vi.mock('@/app/components/base/amplitude/use-amplitude-initialized', () => ({
vi.mock('@/app/components/base/amplitude/registration-tracking', () => ({
flushRegistrationSuccess: vi.fn(),
+ subscribeRegistrationSuccess: () => () => {},
+ getRegistrationSuccessSnapshot: () => 0,
}))
vi.mock('@/app/components/base/zendesk/utils', () => ({
diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx
index d1fcfb76586..a04a731b2e8 100644
--- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx
+++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx
@@ -471,7 +471,7 @@ function AgentVersionRestoreBar({