Files
windmill/frontend/src/lib/components/flows/agentResourceUtils.test.ts
T
2026-09-11 21:26:21 +02:00

191 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import {
agentConfigToInputTransforms,
agentEditorRefusal,
flowLocalInputs,
inputTransformsToAgentConfig,
nonStaticBrainKeys,
summarizeAgentBrain,
toolInputOverrides,
transformValuedBrainKeys
} from './agentResourceUtils'
describe('transformValuedBrainKeys', () => {
// The tag is the whole test: `{type:'ai'}` carries no payload key to look for.
it('flags every transform variant, whatever it carries', () => {
expect(
transformValuedBrainKeys({
system_prompt: { type: 'ai' },
temperature: { type: 'javascript', expr: 'flow_input.t' },
max_iterations: { type: 'static', value: 3 }
})
).toEqual(['system_prompt', 'temperature', 'max_iterations'])
})
it('leaves the brain values that are legitimately objects', () => {
expect(
transformValuedBrainKeys({
output_schema: { type: 'object', properties: {} },
memory: { kind: 'auto', context_length: 20 },
provider: { kind: 'openai', model: 'gpt-4o' },
system_prompt: 'hi'
})
).toEqual([])
})
})
describe('agentEditorRefusal', () => {
it('opens an agent', () => {
expect(agentEditorRefusal('u/admin/agent', 'ai_agent')).toBeUndefined()
})
// A path with no deployed row answers with its own draft, and the generic resource editor writes
// no type: assuming `ai_agent` there would create that path as an agent on deploy.
it('refuses an unknown type rather than assuming one', () => {
expect(agentEditorRefusal('u/admin/db', undefined)).toContain('no deployed agent')
})
})
describe('summarizeAgentBrain', () => {
it('returns only set fields, in brain-key order, formatted', () => {
const rows = summarizeAgentBrain({
provider: { kind: 'openai', model: 'gpt-4o', resource: '$res:f/x/openai' } as any,
system_prompt: 'You are helpful',
temperature: 0.7,
streaming: true,
max_iterations: 10
})
expect(rows).toEqual([
{ label: 'Provider', value: 'openai · gpt-4o' },
{ label: 'System message', value: 'You are helpful' },
{ label: 'Stream the response', value: 'on' },
{ label: 'Temperature', value: '0.7' },
{ label: 'Max iterations', value: '10' }
])
})
it('skips empty/undefined fields', () => {
expect(summarizeAgentBrain({ system_prompt: '', provider: undefined as any })).toEqual([])
expect(summarizeAgentBrain(undefined)).toEqual([])
})
it('summarizes structured fields compactly', () => {
// memory is serialized with a `kind` tag (serde tag = "kind")
const rows = summarizeAgentBrain({
memory: { kind: 'auto', context_length: 20 } as any,
output_schema: { type: 'object' } as any
})
expect(rows).toEqual([
{ label: 'Memory', value: 'auto' },
{ label: 'Output schema', value: 'configured' }
])
})
})
describe('inputTransformsToAgentConfig', () => {
it('captures only static brain values and carries tools', () => {
const config = inputTransformsToAgentConfig(
{
provider: { type: 'static', value: { kind: 'openai' } },
system_prompt: { type: 'static', value: 'hi' },
temperature: { type: 'javascript', expr: 'flow_input.t' }, // non-static → dropped
max_iterations: { type: 'static', value: undefined }, // undefined → dropped
user_message: { type: 'static', value: 'hello' } // not a brain key → dropped
} as any,
[{ id: 't1' }] as any
)
expect(config).toEqual({
tools: [{ id: 't1' }],
provider: { kind: 'openai' },
system_prompt: 'hi'
})
})
it('defaults tools to []', () => {
expect(inputTransformsToAgentConfig({}, undefined)).toEqual({ tools: [] })
})
})
describe('agentConfigToInputTransforms', () => {
it('wraps brain values as static transforms and round-trips through the config', () => {
const config = { provider: { kind: 'openai' }, system_prompt: 'hi', tools: [{ id: 't1' }] }
const its = agentConfigToInputTransforms(config as any)
expect(its).toEqual({
provider: { type: 'static', value: { kind: 'openai' } },
system_prompt: { type: 'static', value: 'hi' }
})
const back = inputTransformsToAgentConfig(its, config.tools as any)
expect(back).toEqual(config)
})
})
describe('nonStaticBrainKeys', () => {
it('lists brain keys with a non-static transform, in brain-key order', () => {
expect(
nonStaticBrainKeys({
provider: { type: 'static', value: {} },
temperature: { type: 'connected' },
system_prompt: { type: 'javascript', expr: 'x' }
} as any)
).toEqual(['system_prompt', 'temperature'])
})
it('flags a non-static provider (the save-blocking case) but not a static one', () => {
// provider is required on the resource; a non-static one gets dropped and must block saving
expect(nonStaticBrainKeys({ provider: { type: 'javascript', expr: 'x' } } as any)).toContain(
'provider'
)
expect(nonStaticBrainKeys({ provider: { type: 'static', value: {} } } as any)).not.toContain(
'provider'
)
})
})
describe('flowLocalInputs', () => {
it('keeps the steps own inputs, dropping brain transforms', () => {
expect(
flowLocalInputs({
provider: { type: 'static', value: {} },
user_message: { type: 'static', value: 'hi' },
user_attachments: { type: 'static', value: [] },
// The roster it narrows belongs to the agent, but which of it one flow may call does
// not: saving this into the resource would impose it on every flow linking the agent.
enabled_tools: { type: 'javascript', expr: 'flow_input.tools' }
} as any)
).toEqual({
user_message: { type: 'static', value: 'hi' },
user_attachments: { type: 'static', value: [] },
enabled_tools: { type: 'javascript', expr: 'flow_input.tools' }
})
})
it('handles undefined', () => {
expect(flowLocalInputs(undefined)).toEqual({})
})
})
describe('toolInputOverrides', () => {
const base = {
tenant: { type: 'javascript', expr: 'flow_input.tenant' },
query: { type: 'static', value: 'x' }
} as any
it('returns nothing when inputs equal the resource base (opening a tool is a no-op)', () => {
expect(toolInputOverrides(base, base)).toEqual({})
})
it('returns only the edited keys, so a revert to base drops back to nothing', () => {
const edited = { ...base, query: { type: 'static', value: 'y' } }
expect(toolInputOverrides(edited, base)).toEqual({ query: { type: 'static', value: 'y' } })
// reverting query back to the base value yields an empty override set again
expect(toolInputOverrides(base, base)).toEqual({})
})
it('includes keys absent from the base', () => {
expect(toolInputOverrides({ extra: { type: 'static', value: 1 } } as any, base)).toEqual({
extra: { type: 'static', value: 1 }
})
})
})