Files
windmill/frontend/src/lib/components/flows/flowTree.test.ts
T
centdixandClaude Opus 4.5 a5363ea4ed refactor: unify flow chat tree operations (#8862)
* refactor: make flow chat code edits explicit

* refactor: centralize flow tree lookups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: simplify flow chat tree mutations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: reuse flow tree lookup in schema map

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: remove flow lookup alias

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: reuse flow tree in previous results

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: reuse canonical flow module lookup

* fix: align rebased flow helpers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: remove flow chat cleanup plan

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: remove flow chat helper wrappers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: preserve non-flowmodule AI agent tools in skeleton and previous_result

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: consolidate flow module ID collectors into flowTree

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: search full flow tree in test_run_step to find special modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: recurse into aiagent tools in collectAllFlowModuleIds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-20 17:50:50 +00:00

290 lines
7.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { FlowModule, FlowValue } from '$lib/gen'
import {
collectAllFlowModuleIds,
collectAllFlowModuleIdsFromModules,
collectFlowNodes,
ensureModuleArrayByLocation,
findFlowNode,
findModuleInFlow,
findModuleInModules,
findModuleParent,
getModuleArrayContainer,
removeFlowModule,
replaceFlowModule
} from './flowTree'
function makeRawModule(id: string): FlowModule {
return {
id,
summary: id,
value: { type: 'rawscript', content: '', language: 'python3', input_transforms: {} } as any
} as FlowModule
}
function makeFlowModuleTool(module: FlowModule): FlowModule {
return {
id: module.id,
summary: module.summary,
value: {
tool_type: 'flowmodule',
...module.value
} as any
} as FlowModule
}
function makeAiAgent(id: string, tools: FlowModule[]): FlowModule {
return {
id,
summary: id,
value: {
type: 'aiagent',
tools,
input_transforms: {}
} as any
} as FlowModule
}
function makeMcpTool(id: string) {
return {
id,
summary: id,
value: {
tool_type: 'mcp',
resource_path: ''
}
}
}
describe('flowTree', () => {
it('returns the root container for top-level modules', () => {
const first = makeRawModule('first')
const second = makeRawModule('second')
const flow: FlowValue = {
modules: [first, second]
}
expect(findModuleParent(flow, 'second')).toEqual({ type: 'root', index: 1 })
expect(findModuleInFlow(flow, 'second')).toBe(second)
expect(getModuleArrayContainer(flow, 'second')).toEqual({
index: 1,
modules: flow.modules
})
})
it('returns nested branch containers from the live flow tree', () => {
const nested = makeRawModule('nested')
const branch = makeRawModule('branch')
const flow: FlowValue = {
modules: [
{
id: 'router',
summary: 'router',
value: {
type: 'branchone',
default: [nested],
branches: [{ expr: 'true', modules: [branch] }],
input_transforms: {}
} as any
}
]
}
expect(findModuleParent(flow, 'nested')).toEqual({
type: 'branchone-default',
parentId: 'router',
index: 0
})
expect(getModuleArrayContainer(flow, 'branch')).toEqual({
index: 0,
modules: ((flow.modules?.[0].value as any).branches[0].modules ?? []) as FlowModule[]
})
})
it('returns ai agent tool containers as mutable flow module arrays', () => {
const lookup = makeFlowModuleTool(makeRawModule('lookup_user'))
const sum = makeFlowModuleTool(makeRawModule('sum'))
const agent = makeAiAgent('agent', [lookup, sum])
const agentTools = ((agent.value as any).tools ?? []) as FlowModule[]
const flow: FlowValue = {
modules: [agent]
}
const match = findFlowNode(flow, 'sum')
const container = getModuleArrayContainer(flow, 'sum')
expect(match).toMatchObject({
location: { type: 'aiagent', parentId: 'agent', index: 1 }
})
expect(findModuleInFlow(flow, 'sum')).toBe(sum)
expect(container).toEqual({
index: 1,
modules: agentTools
})
container?.modules.splice(container.index, 1)
expect(agentTools.map((module) => module.id)).toEqual(['lookup_user'])
})
it('finds ai agent flowmodule tools inside a plain modules array', () => {
const tool = makeFlowModuleTool(makeRawModule('sum'))
const flowModules = [makeAiAgent('agent', [tool])]
expect(findModuleInModules(flowModules, 'sum')).toBe(tool)
})
it('skips non-flowmodule ai agent tools in module traversal', () => {
const lookup = makeFlowModuleTool(makeRawModule('lookup_user'))
const agent = makeAiAgent('agent', [lookup, makeMcpTool('search_docs') as any])
const flow: FlowValue = {
modules: [agent]
}
expect(findModuleInModules(flow.modules ?? [], 'search_docs')).toBeUndefined()
expect(findModuleInFlow(flow, 'search_docs')).toBeNull()
expect(collectFlowNodes(flow).map((entry) => entry.module.id)).toEqual(['agent', 'lookup_user'])
})
it('returns special modules without pretending they belong to an array container', () => {
const failure = makeRawModule('failure')
const preprocessor = makeRawModule('preprocessor')
const flow: FlowValue = {
modules: [],
failure_module: failure,
preprocessor_module: preprocessor
}
expect(findModuleParent(flow, 'failure')).toEqual({ type: 'failure', index: -1 })
expect(findModuleParent(flow, 'preprocessor')).toEqual({
type: 'preprocessor',
index: -1
})
expect(findModuleInFlow(flow, 'failure')).toBe(failure)
expect(findModuleInFlow(flow, 'preprocessor')).toBe(preprocessor)
expect(getModuleArrayContainer(flow, 'failure')).toBeNull()
expect(getModuleArrayContainer(flow, 'preprocessor')).toBeNull()
})
it('removes nested ai agent tools from the real stored tools array', () => {
const lookup = makeFlowModuleTool(makeRawModule('lookup_user'))
const sum = makeFlowModuleTool(makeRawModule('sum'))
const agent = makeAiAgent('agent', [lookup, sum])
const flow: FlowValue = {
modules: [agent]
}
const removed = removeFlowModule(flow, 'sum')
expect(removed?.id).toBe('sum')
expect((((agent.value as any).tools ?? []) as FlowModule[]).map((module) => module.id)).toEqual([
'lookup_user'
])
})
it('replaces module content while preserving the original object reference', () => {
const original = makeRawModule('sum')
const replacement = makeRawModule('sum')
;(replacement.value as any).content = 'updated'
const result = replaceFlowModule(original, replacement)
expect(result).toBe(original)
expect((original.value as any).content).toBe('updated')
})
it('collectAllFlowModuleIds includes all aiagent tool IDs including non-flowmodule', () => {
const lookup = makeFlowModuleTool(makeRawModule('lookup_user'))
const mcp = makeMcpTool('search_docs') as any
const agent = makeAiAgent('agent', [lookup, mcp])
const failure = makeRawModule('failure')
const flow: FlowValue = {
modules: [makeRawModule('step_a'), agent],
failure_module: failure
}
const ids = collectAllFlowModuleIds(flow)
// Should include ALL tool IDs (both flowmodule and MCP)
expect(ids).toContain('step_a')
expect(ids).toContain('agent')
expect(ids).toContain('lookup_user')
expect(ids).toContain('search_docs')
expect(ids).toContain('failure')
expect(ids).toHaveLength(5)
})
it('collectAllFlowModuleIdsFromModules works without special modules', () => {
const agent = makeAiAgent('agent', [
makeFlowModuleTool(makeRawModule('sum')),
makeMcpTool('websearch') as any
])
const ids = collectAllFlowModuleIdsFromModules([agent])
expect(ids).toEqual(['agent', 'sum', 'websearch'])
})
it('collectFlowNodes excludes non-flowmodule tools (MCP)', () => {
// Verify that collectFlowNodes still filters non-flowmodule tools
const lookup = makeFlowModuleTool(makeRawModule('lookup_user'))
const mcp = makeMcpTool('search_docs') as any
const agent = makeAiAgent('agent', [lookup, mcp])
const flow: FlowValue = { modules: [agent] }
const nodeIds = collectFlowNodes(flow).map((n) => n.module.id)
expect(nodeIds).toContain('agent')
expect(nodeIds).toContain('lookup_user')
expect(nodeIds).not.toContain('search_docs')
})
it('recreates a missing branch container from the source flow location', () => {
const nested = makeRawModule('nested')
const beforeFlow: FlowValue = {
modules: [
{
id: 'router',
summary: 'router',
value: {
type: 'branchone',
default: [],
branches: [{ expr: 'true', modules: [nested] }],
input_transforms: {}
} as any
}
]
}
const targetFlow: FlowValue = {
modules: [
{
id: 'router',
summary: 'router',
value: {
type: 'branchone',
default: [],
branches: [],
input_transforms: {}
} as any
}
]
}
const recreatedContainer = ensureModuleArrayByLocation(
targetFlow,
{
type: 'branchone-branch',
parentId: 'router',
branchIndex: 0,
index: 0
},
beforeFlow
)
expect(recreatedContainer).toEqual([])
expect((((targetFlow.modules?.[0].value as any).branches ?? []) as any[])[0]).toMatchObject({
expr: 'true',
modules: []
})
})
})