diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor
index 1809db49..67469b3c 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor
@@ -235,7 +235,12 @@
else
{
-
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs
index ccacf1a1..b61cf3d0 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs
@@ -330,6 +330,11 @@ public partial class FlowDebugger : ComponentBase, IDisposable
ResetResult();
_predicted = null;
_state = FlowState.Idle;
+ // Inputs are per-entry-point, as in OnScriptChangedAsync: never carry a script's value or
+ // parameter tree (or its validity) across a switch of entry kind.
+ _scriptParams = new();
+ _scriptParamsValid = false;
+ _valueChangeValue = string.Empty;
await PersistAsync(StorageEntry, entry.ToString());
if (_entry == EntryKind.Method && _selectedMethodId > 0)
await OnMethodChangedAsync();
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/FlowDebuggerPageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/FlowDebuggerPageTests.cs
index 56569c73..ccc22fea 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/FlowDebuggerPageTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/FlowDebuggerPageTests.cs
@@ -691,4 +691,64 @@ public class FlowDebuggerPageTests : BunitContext
cut.WaitForAssertion(() => Assert.Equal("Manual", cut.Find("select[data-test='flow-script-select']").GetAttribute("value")));
_ = _flow.Received().PredictScriptAsync(7, "Manual", Arg.Any
());
}
+
+ ///
+ /// Two call-kind scripts declaring BYTE-IDENTICAL parameter definitions — the case that makes
+ /// ParameterValueForm skip its reparse (and its ValidChanged) and keeps the previous
+ /// script's raw text and parse errors. The page must remount the form per script instead.
+ ///
+ private static readonly FlowScriptOption Manual2 = new("Manual2", "Call", null,
+ """{"type":"object","properties":{"n":{"type":"integer"}},"required":["n"]}""", 20);
+
+ private void WireInstanceWithTwinScripts()
+ {
+ _templates.GetInstancesBySiteIdAsync(1, Arg.Any())
+ .Returns(Task.FromResult>([new Instance("Sim01") { Id = 7, SiteId = 1, TemplateId = 10 }]));
+ _flow.GetInstanceScriptsAsync(7, Arg.Any())
+ .Returns(Task.FromResult>([Manual, Manual2, Tick]));
+ _flow.PredictScriptAsync(7, Arg.Any(), Arg.Any()).Returns(ThreeNodePrediction());
+ }
+
+ private IRenderedComponent ChooseScriptWithAnInvalidParameter()
+ {
+ var cut = Render();
+ cut.Find("button[data-test='flow-entry-script']").Click();
+ cut.Find("select[data-test='flow-site-select']").Change("1");
+ cut.Find("select[data-test='flow-instance-select']").Change("7");
+ cut.Find("select[data-test='flow-script-select']").Change("Manual");
+ cut.Find("#param-n").Input("abc"); // not an integer: the form parks a parse error
+ Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
+ return cut;
+ }
+
+ [Fact]
+ public void ScriptEntry_SwitchingBetweenCallKindScripts_ResetsTheFormAndValidity()
+ {
+ RegisterServices();
+ WireInstanceWithTwinScripts();
+ var cut = ChooseScriptWithAnInvalidParameter();
+
+ cut.Find("select[data-test='flow-script-select']").Change("Manual2");
+
+ // A fresh form: no echo of the previous script's text, and no stale parse error keeping
+ // Run disabled invisibly — Run is disabled only because the required field is unset.
+ Assert.Equal(string.Empty, cut.Find("#param-n").GetAttribute("value"));
+ Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
+
+ cut.Find("#param-n").Input("5");
+ Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
+ }
+
+ [Fact]
+ public void ScriptEntry_SwitchingFromAnInvalidFormToANoParameterScript_EnablesRun()
+ {
+ RegisterServices();
+ WireInstanceWithTwinScripts();
+ var cut = ChooseScriptWithAnInvalidParameter();
+
+ cut.Find("select[data-test='flow-script-select']").Change("Tick");
+
+ Assert.Contains("takes no parameters", cut.Find("[data-test='flow-inputs']").TextContent);
+ Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
+ }
}