Files
windmill/frontend/src/lib/components/flows/agentToolTree.test.ts
T
GuilhemandClaude Opus 5 449b1a6933 fix: ground the chat's AI agent provider in the workspace's models (#10774)
* fix: ground the chat's AI agent provider in the workspace's models

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

* fix: never reject an AI agent model the catalog could not confirm

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

* test: benchmark AI agent provider grounding in ai_evals

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

* fix: only reject an AI agent model an exhaustive listing rules out

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

* fix: keep instance-level AI settings out of the workspace provider catalog

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

* fix: keep untrusted model ids out of the chat's context

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

* refactor: carry completeness on the model listing itself

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

* docs: correct two comments left behind by the catalog rework

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

* fix: bound the model listing and verify the default against it

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

* fix: honour a workspace default a filtered listing names

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

* fix: recognise a workspace default past the prompt's model cap

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

* fix: keep an aliasing provider's unlisted model ids permissive

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:42:02 +02:00

155 lines
4.5 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
vi.mock('../aiProviderStorage', () => ({
loadStoredConfig: () => undefined
}))
vi.mock('./flowInfers', () => ({
AI_AGENT_SCHEMA: { properties: {} }
}))
import type { FlowModule } from '$lib/gen'
import {
collectFlowNodeIds,
collectProviderlessAgentIds,
findAgentToolOwner,
removeAgentToolOwner
} from './agentToolTree'
function makeRawModule(id: string): FlowModule {
return {
id,
summary: id,
value: { type: 'rawscript', content: '', language: 'python3', input_transforms: {} } as any
} as FlowModule
}
function makeAiAgent(id: string, tools: any[]): FlowModule {
return {
id,
summary: id,
value: {
type: 'aiagent',
tools,
input_transforms: {}
} as any
} as FlowModule
}
function makeFlowModuleTool(module: FlowModule) {
return {
id: module.id,
summary: module.summary,
value: {
tool_type: 'flowmodule',
...module.value
}
}
}
describe('findAgentToolOwner', () => {
it('finds a direct tool owner in an ai agent', () => {
const rootAgent = makeAiAgent('root_agent', [makeFlowModuleTool(makeRawModule('lookup_user'))])
expect(findAgentToolOwner([rootAgent], 'lookup_user')).toMatchObject({
agentId: 'root_agent',
toolIndex: 0,
depth: 1
})
})
it('finds a nested tool owner inside a nested ai agent tool', () => {
const nestedAgent = makeAiAgent('support_agent', [makeFlowModuleTool(makeRawModule('create_ticket'))])
const rootAgent = makeAiAgent('root_agent', [makeFlowModuleTool(nestedAgent)])
expect(findAgentToolOwner([rootAgent], 'create_ticket')).toMatchObject({
agentId: 'support_agent',
toolIndex: 0,
depth: 2
})
})
})
describe('removeAgentToolOwner', () => {
it('removes the matched tool and returns its subtree ids', () => {
const nestedAgent = makeAiAgent('support_agent', [makeFlowModuleTool(makeRawModule('create_ticket'))])
const rootAgent = makeAiAgent('root_agent', [
makeFlowModuleTool(makeRawModule('lookup_user')),
makeFlowModuleTool(nestedAgent)
])
const owner = findAgentToolOwner([rootAgent], 'support_agent')
expect(owner).toBeDefined()
expect(removeAgentToolOwner(owner!)).toEqual({
tool: expect.objectContaining({ id: 'support_agent' }),
removedIds: ['support_agent', 'create_ticket']
})
expect((rootAgent.value as any).tools).toHaveLength(1)
expect(((rootAgent.value as any).tools as any[]).map((tool) => tool.id)).toEqual(['lookup_user'])
})
})
describe('collectFlowNodeIds', () => {
it('includes ai agent tool ids when deleting an ai agent flow module', () => {
const agent = makeAiAgent('root_agent', [
makeFlowModuleTool(makeRawModule('lookup_user')),
makeFlowModuleTool(makeAiAgent('support_agent', [makeFlowModuleTool(makeRawModule('create_ticket'))]))
])
expect(collectFlowNodeIds(agent)).toEqual([
'root_agent',
'lookup_user',
'support_agent',
'create_ticket'
])
})
})
describe('malformed module trees', () => {
it('leaves a non-array branches to the schema check instead of throwing', () => {
// The traversal now runs on model-produced JSON before it is validated, and every flow
// write path depends on reaching the schema error rather than "branches is not iterable".
expect(() =>
collectProviderlessAgentIds([
{ id: 'a', value: { type: 'branchall', branches: {} } },
{ id: 'b', value: { type: 'branchone', default: null, branches: 'nope' } },
{ id: 'c', value: { type: 'branchall', branches: [null, { modules: undefined }] } }
])
).not.toThrow()
})
})
describe('collectProviderlessAgentIds', () => {
const provider = { type: 'static', value: { kind: 'openai', model: 'gpt-4o' } }
const agent = (id: string, value: Record<string, unknown>) => ({
id,
value: { type: 'aiagent', input_transforms: {}, ...value }
})
it('a linked agent needs no provider of its own', () => {
expect(collectProviderlessAgentIds([agent('a', { agent: 'f/team/my_agent' })])).toEqual([])
})
it('a standalone agent with a provider is fine', () => {
expect(
collectProviderlessAgentIds([agent('a', { input_transforms: { provider }, tools: [] })])
).toEqual([])
})
it('a standalone agent without a provider is reported', () => {
expect(collectProviderlessAgentIds([agent('a', { tools: [] })])).toEqual(['a'])
})
it('nested agent tools and branches are traversed', () => {
const nested = agent('parent', {
input_transforms: { provider },
tools: [agent('nested', { tools: [] })]
})
expect(
collectProviderlessAgentIds([
{ id: 'b', value: { type: 'branchall', branches: [{ modules: [nested] }] } }
])
).toEqual(['nested'])
})
})