mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 08:02:47 +00:00
* 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>
290 lines
7.9 KiB
TypeScript
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: []
|
|
})
|
|
})
|
|
})
|