fix(central-ui): FlowDebugService — re-flatten on each instance selection; safe Ask fallback

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-08 01:41:14 -04:00
parent a2bfc79e3a
commit 7197c3986f
2 changed files with 79 additions and 29 deletions
@@ -623,9 +623,9 @@ public sealed class FlowDebugServiceTests : IDisposable
}
/// <summary>
/// The picker, the prediction and the run of one instance share ONE flatten for the life of
/// the scoped service (design 2026-09-08 §5) — a regression here means every keystroke on the
/// page re-walks the template chain.
/// One picker selection costs ONE flatten: the prediction and the run that follow it read the
/// entry <see cref="IFlowDebugService.GetInstanceScriptsAsync"/> just refreshed (design
/// 2026-09-08 §5). A regression here means every keystroke re-walks the template chain.
/// </summary>
[Fact]
public async Task GetInstanceScripts_ThenPredict_ThenRun_FlattenOnce()
@@ -641,6 +641,35 @@ public sealed class FlowDebugServiceTests : IDisposable
await _pipeline.Received(1).FlattenAndValidateAsync(9, Arg.Any<CancellationToken>(), false, null);
}
/// <summary>
/// A scoped service lives for the whole Blazor circuit, so re-selecting the instance must
/// re-flatten — otherwise a template edit or a redeploy mid-session leaves the picker, the
/// parameter form and the Ask sizing stale for the rest of the session.
/// </summary>
[Fact]
public async Task GetInstanceScripts_CalledAgain_ReflattensAndRefreshes()
{
_templates.GetInstanceByIdAsync(9, Arg.Any<CancellationToken>())
.Returns(new Instance("Sim01") { Id = 9, TemplateId = 10, SiteId = 1 });
static Result<FlatteningPipelineResult> Flattened(params ResolvedScript[] scripts)
=> Result<FlatteningPipelineResult>.Success(new FlatteningPipelineResult(
new FlattenedConfiguration { InstanceUniqueName = "Sim01", TemplateId = 10, SiteId = 1, Scripts = scripts },
"h", new ValidationResult()));
_pipeline.FlattenAndValidateAsync(9, Arg.Any<CancellationToken>(), false, null).Returns(
Flattened(new ResolvedScript { CanonicalName = "Manual", Code = "x", TriggerType = "Call" }),
Flattened(
new ResolvedScript { CanonicalName = "Manual", Code = "x", TriggerType = "Call" },
new ResolvedScript { CanonicalName = "Added", Code = "x", TriggerType = "Call" }));
var svc = CreateService(reply: null);
var first = await svc.GetInstanceScriptsAsync(9);
var second = await svc.GetInstanceScriptsAsync(9);
Assert.Equal(["Manual"], first.Select(o => o.CanonicalName).ToList());
Assert.Equal(["Added", "Manual"], second.Select(o => o.CanonicalName).ToList());
await _pipeline.Received(2).FlattenAndValidateAsync(9, Arg.Any<CancellationToken>(), false, null);
}
private sealed class StubAuth : AuthenticationStateProvider
{
private readonly ClaimsPrincipal _user;