mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
fix: harden global flow test fallback
This commit is contained in:
@@ -431,6 +431,16 @@ export class AIChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
private getGlobalHelpers = (): GlobalToolHelpers => {
|
||||
const flowHelpers = this.flowAiChatHelpers
|
||||
return {
|
||||
...(this.isSessionChat ? { sessionId: this.sessionId } : {}),
|
||||
...(flowHelpers
|
||||
? { testActiveFlow: (args?: Record<string, any>) => flowHelpers.testFlow(args) }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
changeMode(
|
||||
mode: AIMode,
|
||||
pendingPrompt?: string,
|
||||
@@ -517,11 +527,7 @@ export class AIChatManager {
|
||||
previewTools: this.isSessionChat
|
||||
})
|
||||
this.tools = globalToolsFor({ sessionPreview: this.isSessionChat })
|
||||
this.helpers = {
|
||||
...(this.isSessionChat ? { sessionId: this.sessionId } : {}),
|
||||
testActiveFlow: async (args?: Record<string, any>) =>
|
||||
this.flowAiChatHelpers?.testFlow(args)
|
||||
} satisfies GlobalToolHelpers
|
||||
this.helpers = this.getGlobalHelpers()
|
||||
} else if (mode === AIMode.APP) {
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
this.systemMessage = prepareAppSystemMessage(customPrompt)
|
||||
@@ -1330,6 +1336,9 @@ export class AIChatManager {
|
||||
|
||||
setFlowHelpers = (flowHelpers: FlowAIChatHelpers) => {
|
||||
this.flowAiChatHelpers = flowHelpers
|
||||
if (this.mode === AIMode.GLOBAL) {
|
||||
this.helpers = this.getGlobalHelpers()
|
||||
}
|
||||
untrack(() => {
|
||||
if (this.autoAcceptEditsActive) {
|
||||
this.acceptPendingFlowEdits(flowHelpers)
|
||||
@@ -1338,6 +1347,9 @@ export class AIChatManager {
|
||||
|
||||
return () => {
|
||||
this.flowAiChatHelpers = undefined
|
||||
if (this.mode === AIMode.GLOBAL) {
|
||||
this.helpers = this.getGlobalHelpers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,37 @@ describe('AIChatManager autonomy mode', () => {
|
||||
expect(jobId).toBe('job-flow-preview')
|
||||
expect(testFlow).toHaveBeenCalledWith({ name: 'Ada' })
|
||||
})
|
||||
|
||||
it('does not expose an active flow test helper in global mode before flow helpers mount', () => {
|
||||
const manager = new AIChatManager()
|
||||
|
||||
manager.changeMode(AIMode.GLOBAL)
|
||||
|
||||
expect(manager.helpers.testActiveFlow).toBeUndefined()
|
||||
})
|
||||
|
||||
it('refreshes the active flow test helper when flow helpers mount in global mode', async () => {
|
||||
const manager = new AIChatManager()
|
||||
const testFlow = vi.fn(async () => 'job-flow-preview')
|
||||
|
||||
manager.changeMode(AIMode.GLOBAL)
|
||||
const cleanup = manager.setFlowHelpers(
|
||||
createFlowHelpers({
|
||||
hasPendingChanges: () => false,
|
||||
acceptAllModuleActions: vi.fn(),
|
||||
testFlow
|
||||
})
|
||||
)
|
||||
|
||||
await expect(manager.helpers.testActiveFlow({ name: 'Ada' })).resolves.toBe(
|
||||
'job-flow-preview'
|
||||
)
|
||||
expect(testFlow).toHaveBeenCalledWith({ name: 'Ada' })
|
||||
|
||||
cleanup()
|
||||
|
||||
expect(manager.helpers.testActiveFlow).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('AIChatManager persisted autonomy default', () => {
|
||||
|
||||
@@ -1490,6 +1490,54 @@ describe('global AI tools', () => {
|
||||
expect(result).toContain('Result (SUCCESS)')
|
||||
})
|
||||
|
||||
it('test_run_flow falls back to preview when the live flow editor test hook returns undefined', async () => {
|
||||
UserDraft.save(
|
||||
'flow',
|
||||
'',
|
||||
{
|
||||
path: 'u/admin/live_flow_fallback',
|
||||
summary: 'Live flow fallback',
|
||||
value: { modules: [{ id: 'fallback_step', value: { type: 'identity' } }] },
|
||||
schema: {},
|
||||
edited_by: '',
|
||||
edited_at: '',
|
||||
archived: false,
|
||||
extra_perms: {}
|
||||
},
|
||||
{ workspace: WORKSPACE }
|
||||
)
|
||||
UserDraft.setLiveEditorDraft({
|
||||
workspace: WORKSPACE,
|
||||
itemKind: 'flow',
|
||||
storagePath: '',
|
||||
effectivePath: 'u/admin/live_flow_fallback'
|
||||
})
|
||||
const testActiveFlow = vi.fn(async () => undefined)
|
||||
|
||||
await withCompletedTestJob(() =>
|
||||
callGlobalTool(
|
||||
'test_run_flow',
|
||||
{
|
||||
path: 'u/admin/live_flow_fallback',
|
||||
args: { name: 'Ada' }
|
||||
},
|
||||
toolCallbacks,
|
||||
{ testActiveFlow }
|
||||
)
|
||||
)
|
||||
|
||||
expect(testActiveFlow).toHaveBeenCalledWith({ name: 'Ada' })
|
||||
expect(FlowService.getFlowByPath).not.toHaveBeenCalled()
|
||||
expect(JobService.runFlowPreview).toHaveBeenCalledWith({
|
||||
workspace: WORKSPACE,
|
||||
requestBody: {
|
||||
path: 'u/admin/live_flow_fallback',
|
||||
value: { modules: [{ id: 'fallback_step', value: { type: 'identity' } }] },
|
||||
args: { name: 'Ada' }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('test_run_step previews rawscript steps from the local draft flow', async () => {
|
||||
const content = 'export async function main(name: string) {\n\treturn name.toUpperCase()\n}'
|
||||
await callGlobalTool('write_flow', {
|
||||
@@ -1610,6 +1658,42 @@ describe('global AI tools', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('test_run_step lists nested step ids when a step is not found', async () => {
|
||||
await callGlobalTool('write_flow', {
|
||||
path: 'f/flows/nested-step-error',
|
||||
summary: 'Flow with nested step',
|
||||
modules: JSON.stringify([
|
||||
{
|
||||
id: 'loop_step',
|
||||
value: {
|
||||
type: 'forloopflow',
|
||||
iterator: { type: 'static', value: [1] },
|
||||
skip_failures: false,
|
||||
modules: [
|
||||
{
|
||||
id: 'nested_script_step',
|
||||
value: {
|
||||
type: 'rawscript',
|
||||
language: 'bun',
|
||||
content: 'export async function main() { return 1 }',
|
||||
input_transforms: {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
await expect(
|
||||
callGlobalTool('test_run_step', {
|
||||
path: 'f/flows/nested-step-error',
|
||||
stepId: 'missing_nested_step',
|
||||
args: {}
|
||||
})
|
||||
).rejects.toThrow(/Available steps: loop_step, nested_script_step/)
|
||||
})
|
||||
|
||||
it('asks the user a question and returns the selected answer', async () => {
|
||||
const callbacks: ToolCallbacks = {
|
||||
setToolStatus: vi.fn(),
|
||||
|
||||
@@ -2640,15 +2640,24 @@ async function testRunFlowByPath(
|
||||
return executeTestRun({
|
||||
jobStarter: async () => {
|
||||
const jobId = await testActiveFlow(testArgs)
|
||||
if (!jobId) {
|
||||
throw new Error('Failed to start test run - active flow editor returned undefined')
|
||||
if (jobId) {
|
||||
return jobId
|
||||
}
|
||||
return jobId
|
||||
|
||||
const flow = await loadFlowDraftValue(args.path, workspace)
|
||||
return JobService.runFlowPreview({
|
||||
workspace,
|
||||
requestBody: {
|
||||
path: args.path,
|
||||
value: flowDraftValueForPreview(flow.flow),
|
||||
args: testArgs
|
||||
}
|
||||
})
|
||||
},
|
||||
workspace,
|
||||
toolCallbacks,
|
||||
toolId,
|
||||
startMessage: `Starting live editor flow test run for "${args.path}"...`,
|
||||
startMessage: `Starting flow test run for "${args.path}"...`,
|
||||
contextName: 'flow'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1113,11 +1113,13 @@ function flowStepArgsForModule(moduleId: string, args: Record<string, any>): Rec
|
||||
}
|
||||
|
||||
function getAvailableFlowStepIds(flowValue: FlowValue): string {
|
||||
return [
|
||||
...(flowValue.modules ?? []).map((module: FlowModule) => module.id),
|
||||
...(flowValue.preprocessor_module ? [flowValue.preprocessor_module.id] : []),
|
||||
...(flowValue.failure_module ? [flowValue.failure_module.id] : [])
|
||||
].join(', ')
|
||||
return Array.from(
|
||||
new Set([
|
||||
...extractAllModules(flowValue.modules ?? []).map((module: FlowModule) => module.id),
|
||||
...(flowValue.preprocessor_module ? [flowValue.preprocessor_module.id] : []),
|
||||
...(flowValue.failure_module ? [flowValue.failure_module.id] : [])
|
||||
])
|
||||
).join(', ')
|
||||
}
|
||||
|
||||
async function loadDeployedScriptForFlowStep(
|
||||
|
||||
Reference in New Issue
Block a user