diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 1fd608b842..d074e681b8 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -1015,7 +1015,8 @@ async fn test_deno_flow(db: Pool) { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, }, FlowModule { id: "b".to_string(), @@ -1044,7 +1045,8 @@ async fn test_deno_flow(db: Pool) { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, }], }, stop_after_if: Default::default(), @@ -1052,7 +1054,8 @@ async fn test_deno_flow(db: Pool) { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, }, ], same_worker: false, @@ -1147,7 +1150,8 @@ async fn test_deno_flow_same_worker(db: Pool) { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, }, FlowModule { id: "b".to_string(), @@ -1187,7 +1191,8 @@ async fn test_deno_flow_same_worker(db: Pool) { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, }, FlowModule { id: "e".to_string(), @@ -1214,7 +1219,8 @@ async fn test_deno_flow_same_worker(db: Pool) { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None }, ], }, @@ -1224,6 +1230,7 @@ async fn test_deno_flow_same_worker(db: Pool) { retry: None, sleep: None, cache_ttl: None, + mock: None, }, FlowModule { id: "c".to_string(), @@ -1258,7 +1265,9 @@ async fn test_deno_flow_same_worker(db: Pool) { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, + }, ], same_worker: true, diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index 388c19aa48..0089ef2a65 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -652,7 +652,8 @@ mod tests { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, }, FlowModule { id: "b".to_string(), @@ -672,7 +673,8 @@ mod tests { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, }, FlowModule { id: "c".to_string(), @@ -690,7 +692,8 @@ mod tests { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, }, ], failure_module: Some(FlowModule { @@ -708,7 +711,8 @@ mod tests { suspend: Default::default(), retry: None, sleep: None, - cache_ttl: None + cache_ttl: None, + mock: None, }), same_worker: false, }; diff --git a/backend/windmill-common/src/flows.rs b/backend/windmill-common/src/flows.rs index 5507adf5de..5bfb425663 100644 --- a/backend/windmill-common/src/flows.rs +++ b/backend/windmill-common/src/flows.rs @@ -156,6 +156,13 @@ pub struct Suspend { pub resume_form: Option, } +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Mock { + pub enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub return_value: Option, +} + #[derive(Deserialize, Serialize, Debug, Clone)] pub struct FlowModule { #[serde(default = "default_id")] @@ -168,6 +175,8 @@ pub struct FlowModule { #[serde(skip_serializing_if = "Option::is_none")] pub suspend: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub mock: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub retry: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sleep: Option, diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 9121b153ed..8cd228bbe7 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -589,6 +589,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>( summary: Some( "Virtual module needed for suspend/sleep when last module".to_string(), ), + mock: None, retry: None, sleep: None, suspend: None, diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 4418cc3e95..cd90394388 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -1225,43 +1225,64 @@ async fn push_next_flow_job let mut transform_context: Option = None; - let args: windmill_common::error::Result<_> = match &module.value { - FlowModuleValue::Script { input_transforms, .. } - | FlowModuleValue::RawScript { input_transforms, .. } - | FlowModuleValue::Flow { input_transforms, .. } => { - let ctx = get_transform_context(&flow_job, previous_id.clone(), &status).await?; - transform_context = Some(ctx); - let by_id = transform_context.as_ref().unwrap(); - transform_input( - &flow_job.args, - last_result.clone(), - input_transforms, - resume_messages.as_slice(), - approvers, - by_id, - client, - ) - .await - } - FlowModuleValue::Identity => match last_result.clone() { - Value::Object(m) => Ok(m), - v @ _ => { - let mut m = Map::new(); - m.insert("previous_result".to_string(), v); - Ok(m) + let args: windmill_common::error::Result<_> = if module.mock.is_some() + && module.mock.as_ref().unwrap().enabled + { + let mut m = Map::new(); + let v = module + .mock + .as_ref() + .unwrap() + .return_value + .clone() + .ok_or_else(|| { + Error::BadRequest(format!( + "mock enabled but no return_value specified for module {}", + module.id + )) + })?; + m.insert("previous_result".to_string(), v); + Ok(m) + } else { + match &module.value { + FlowModuleValue::Script { input_transforms, .. } + | FlowModuleValue::RawScript { input_transforms, .. } + | FlowModuleValue::Flow { input_transforms, .. } => { + let ctx = get_transform_context(&flow_job, previous_id.clone(), &status).await?; + transform_context = Some(ctx); + let by_id = transform_context.as_ref().unwrap(); + transform_input( + &flow_job.args, + last_result.clone(), + input_transforms, + resume_messages.as_slice(), + approvers, + by_id, + client, + ) + .await } - }, - _ => { - /* embedded flow input is augmented with embedding flow input */ - if let Some(value) = &flow_job.args { - Ok(value - .as_object() - .ok_or_else(|| { - Error::BadRequest(format!("Expected an object value, found: {value:?}")) - })? - .clone()) - } else { - Ok(Map::new()) + FlowModuleValue::Identity => match last_result.clone() { + Value::Object(m) => Ok(m), + v @ _ => { + let mut m = Map::new(); + m.insert("previous_result".to_string(), v); + Ok(m) + } + }, + + _ => { + /* embedded flow input is augmented with embedding flow input */ + if let Some(value) = &flow_job.args { + Ok(value + .as_object() + .ok_or_else(|| { + Error::BadRequest(format!("Expected an object value, found: {value:?}")) + })? + .clone()) + } else { + Ok(Map::new()) + } } } }; @@ -1648,6 +1669,18 @@ async fn compute_next_flow_transform<'c>( previous_id: String, client: &AuthedClient, ) -> error::Result<(sqlx::Transaction<'c, sqlx::Postgres>, NextFlowTransform)> { + if module.mock.is_some() && module.mock.as_ref().unwrap().enabled { + return Ok(( + tx, + NextFlowTransform::Continue( + ContinuePayload::SingleJob(JobPayloadWithTag { + payload: JobPayload::Identity, + tag: None, + }), + NextStatus::NextStep, + ), + )); + } match &module.value { FlowModuleValue::Identity => Ok(( tx, diff --git a/frontend/src/lib/components/flows/content/FlowBranchesAllWrapper.svelte b/frontend/src/lib/components/flows/content/FlowBranchesAllWrapper.svelte index 95a2d57d75..f0ee3a14c1 100644 --- a/frontend/src/lib/components/flows/content/FlowBranchesAllWrapper.svelte +++ b/frontend/src/lib/components/flows/content/FlowBranchesAllWrapper.svelte @@ -11,6 +11,7 @@ import FlowModuleEarlyStop from './FlowModuleEarlyStop.svelte' import FlowModuleSleep from './FlowModuleSleep.svelte' import FlowModuleSuspend from './FlowModuleSuspend.svelte' + import FlowModuleMock from './FlowModuleMock.svelte' export let flowModule: FlowModule export let previousModule: FlowModule | undefined @@ -67,6 +68,7 @@ Early Stop/Break Suspend/Approval Sleep + Mock
@@ -84,6 +86,11 @@
+ +
+ +
+
diff --git a/frontend/src/lib/components/flows/content/FlowBranchesOneWrapper.svelte b/frontend/src/lib/components/flows/content/FlowBranchesOneWrapper.svelte index 111cc969d1..d6f8367ef4 100644 --- a/frontend/src/lib/components/flows/content/FlowBranchesOneWrapper.svelte +++ b/frontend/src/lib/components/flows/content/FlowBranchesOneWrapper.svelte @@ -11,6 +11,7 @@ import FlowModuleSleep from './FlowModuleSleep.svelte' import FlowModuleSuspend from './FlowModuleSuspend.svelte' import SplitPanesWrapper from '../../splitPanes/SplitPanesWrapper.svelte' + import FlowModuleMock from './FlowModuleMock.svelte' // import FlowRetries from './FlowRetries.svelte' export let flowModule: FlowModule @@ -67,6 +68,7 @@ Early Stop/Break Suspend/Approval Sleep + Mock
@@ -84,6 +86,11 @@
+ +
+ +
+
diff --git a/frontend/src/lib/components/flows/content/FlowLoop.svelte b/frontend/src/lib/components/flows/content/FlowLoop.svelte index ce5b2de3a0..b3b4b855ae 100644 --- a/frontend/src/lib/components/flows/content/FlowLoop.svelte +++ b/frontend/src/lib/components/flows/content/FlowLoop.svelte @@ -15,6 +15,7 @@ import { getStepPropPicker } from '../previousResults' import FlowModuleSleep from './FlowModuleSleep.svelte' + import FlowModuleMock from './FlowModuleMock.svelte' const { previewArgs, flowStateStore, flowStore } = getContext('FlowEditorContext') @@ -113,6 +114,7 @@ Early Stop/Break Suspend/Approval Sleep + Mock
@@ -138,6 +140,11 @@
+ +
+ +
+
diff --git a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte index ad26ed67ad..f6da99808f 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte @@ -29,6 +29,7 @@ import FlowPathViewer from './FlowPathViewer.svelte' import InputTransformSchemaForm from '$lib/components/InputTransformSchemaForm.svelte' import { schemaToObject } from '$lib/schema' + import FlowModuleMock from './FlowModuleMock.svelte' const { selectedId, previewArgs, flowStateStore, flowStore, saveDraft } = getContext('FlowEditorContext') @@ -133,6 +134,7 @@ bind:module={flowModule} on:toggleSuspend={() => selectAdvanced('suspend')} on:toggleSleep={() => selectAdvanced('sleep')} + on:toggleMock={() => selectAdvanced('mock')} on:toggleRetry={() => selectAdvanced('retries')} on:toggleCache={() => selectAdvanced('cache')} on:toggleStopAfterIf={() => selectAdvanced('early-stop')} @@ -265,6 +267,7 @@ Early Stop/Break Suspend/Approval Sleep + Mock Shared Directory {/if} @@ -285,6 +288,10 @@
+ {:else if advancedSelected === 'mock'} +
+ +
{:else if advancedSelected === 'same_worker'}
diff --git a/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte b/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte index 0164142061..581ff9235c 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte @@ -3,7 +3,7 @@ import { WorkerService, type FlowModule } from '$lib/gen' import { faCodeBranch, faPen, faSave } from '@fortawesome/free-solid-svg-icons' import { createEventDispatcher, getContext } from 'svelte' - import { Bed, Database, PhoneIncoming, Repeat, Square } from 'lucide-svelte' + import { Bed, Database, PhoneIncoming, Repeat, Square, Voicemail } from 'lucide-svelte' import Popover from '../../Popover.svelte' import type { FlowEditorContext } from '../types' import { sendUserToast } from '$lib/utils' @@ -83,6 +83,17 @@ Sleep + dispatch('toggleMock')} + > + + Mock + {/if} {#if module.value.type === 'script'}
diff --git a/frontend/src/lib/components/flows/content/FlowModuleMock.svelte b/frontend/src/lib/components/flows/content/FlowModuleMock.svelte new file mode 100644 index 0000000000..6fa0dd7258 --- /dev/null +++ b/frontend/src/lib/components/flows/content/FlowModuleMock.svelte @@ -0,0 +1,57 @@ + + +

+ Mock + + If defined and enabled, the step will immediately return the mock value instead of being + executed. + +

+ { + if (isMockEnabled) { + flowModule.mock = { + enabled: false, + return_value: flowModule.mock?.return_value + } + } else { + flowModule.mock = { + enabled: true, + return_value: flowModule.mock?.return_value ?? { example: 'value' } + } + code = JSON.stringify(flowModule.mock?.return_value, null, 2) + } + console.log(isMockEnabled, flowModule.mock) + }} + options={{ + right: 'Enable step mocking' + }} +/> +
+ Mocked Return value + {#if flowModule?.mock?.return_value != undefined} + + {:else} + + {/if} +
diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte index e061e35324..0a2fcaad02 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte @@ -2,7 +2,7 @@ import Badge from '$lib/components/common/badge/Badge.svelte' import Popover from '$lib/components/Popover.svelte' import { classNames } from '$lib/utils' - import { Bed, Database, Move, PhoneIncoming, Repeat, Square, X } from 'lucide-svelte' + import { Bed, Database, Move, PhoneIncoming, Repeat, Square, Voicemail, X } from 'lucide-svelte' import { createEventDispatcher } from 'svelte' import { fade } from 'svelte/transition' @@ -13,6 +13,7 @@ export let earlyStop: boolean = false export let suspend: boolean = false export let sleep: boolean = false + export let mock: boolean = false export let bold: boolean = false export let id: string | undefined = undefined export let label: string @@ -88,6 +89,17 @@ Sleep {/if} + {#if mock} + +
+ +
+ Mocked +
+ {/if}