feat(flow-debugger): predict the reset write + echo wait of the reset-aware handshake (#36)
Claude-Session: https://claude.ai/code/session_01XPsuy2ZZ5Gbyfpt8PrK5BV
This commit is contained in:
@@ -45,6 +45,8 @@ public sealed record PredictedFlow(IReadOnlyList<PredictedNode> Nodes, IReadOnly
|
||||
/// script API exposes (<c>Route.To(x).Call(y)</c>, <c>Scripts.Call(y)</c>,
|
||||
/// <c>….Attributes.WriteBatchAndWaitAsync(…)</c>, <c>….Attributes[x] = v</c>, …) by text with
|
||||
/// whitespace stripped, exactly as <see cref="ScriptTrustValidator"/>'s hardening walker does.
|
||||
/// The nine-argument reset-aware <c>WriteBatchAndWaitAsync</c> overload (Gitea #36) additionally
|
||||
/// predicts a reset attribute write + echo wait ahead of the batch write + wait pair.
|
||||
/// String-literal arguments resolve to titles; anything else is flagged
|
||||
/// <see cref="PredictedNode.InputDependent"/>. Branching is not modelled: every recognised call
|
||||
/// is emitted once in source order, and an <c>if (…) return …;</c> guard that precedes the next
|
||||
@@ -369,9 +371,18 @@ public static class FlowShapePredictor
|
||||
{
|
||||
case "WriteBatchAndWaitAsync":
|
||||
{
|
||||
// (batch, triggerAttr, triggerValue, waitAttr, waitValue, timeout)
|
||||
// (batch, triggerAttr, triggerValue, waitAttr, waitValue, timeout
|
||||
// [, resetAttr, resetValue, resetTimeout]) — Gitea #36: the reset-aware
|
||||
// overload writes + waits on the reset attribute BEFORE the batch.
|
||||
var (trigger, triggerDependent) = LiteralOrExpr(args.ElementAtOrDefault(1)?.Expression);
|
||||
var (wait, waitDependent) = LiteralOrExpr(args.ElementAtOrDefault(3)?.Expression);
|
||||
var resetArg = ArgAt(args, 6, "resetKey");
|
||||
if (resetArg is not null)
|
||||
{
|
||||
var (reset, resetDependent) = LiteralOrExpr(resetArg.Expression);
|
||||
_b.AddNode(FlowStepKind.AttributeWrite, FlowLane.Tags, Qualify(reset), resetDependent, "reset");
|
||||
_b.AddNode(FlowStepKind.AttributeWait, FlowLane.Site, Qualify(reset), resetDependent, "reset echo");
|
||||
}
|
||||
_b.AddNode(FlowStepKind.AttributeWrite, FlowLane.Tags, Qualify($"batch + {trigger}"), triggerDependent, null);
|
||||
_b.AddNode(FlowStepKind.AttributeWait, FlowLane.Site, Qualify(wait), waitDependent, null);
|
||||
return true;
|
||||
@@ -463,6 +474,16 @@ public static class FlowShapePredictor
|
||||
private static string TrimAsyncSuffix(string member)
|
||||
=> member.Length > 5 && member.EndsWith("Async", StringComparison.Ordinal) ? member[..^5] : member;
|
||||
|
||||
/// <summary>An argument by name (<c>name: expr</c>) or, failing that, by position among the unnamed arguments.</summary>
|
||||
private static ArgumentSyntax? ArgAt(SeparatedSyntaxList<ArgumentSyntax> args, int index, string name)
|
||||
{
|
||||
var named = args.FirstOrDefault(a => a.NameColon?.Name.Identifier.ValueText == name);
|
||||
if (named is not null)
|
||||
return named;
|
||||
var positional = args.ElementAtOrDefault(index);
|
||||
return positional is { NameColon: null } ? positional : null;
|
||||
}
|
||||
|
||||
/// <summary>A string literal resolves to its value; anything else is input-dependent and shown as its source text.</summary>
|
||||
private static (string Text, bool Dependent) LiteralOrExpr(ExpressionSyntax? expr)
|
||||
{
|
||||
|
||||
@@ -313,4 +313,46 @@ public class FlowShapePredictorTests
|
||||
Assert.True(write.InputDependent);
|
||||
Assert.Equal("name", write.Title);
|
||||
}
|
||||
|
||||
/// <summary>Gitea #36: the reset-aware overload predicts the reset write + echo wait ahead of the batch.</summary>
|
||||
[Fact]
|
||||
public void ResetAwareWriteBatchAndWait_PredictsResetWriteAndEchoWaitBeforeTheBatch()
|
||||
{
|
||||
const string body = """
|
||||
var data = new System.Collections.Generic.Dictionary<string, object?> { { "MoveOutBatchID", 1 } };
|
||||
var hs = await Children["RightMESReceiver"].Attributes.WriteBatchAndWaitAsync(
|
||||
data, "MoveOutFlag", true, "MoveOutCompleteFlag", true, System.TimeSpan.FromSeconds(25),
|
||||
"MoveOutCompleteFlag", false, System.TimeSpan.FromSeconds(10));
|
||||
if (hs.ResetFailed) return new { WasSuccessful = false };
|
||||
""";
|
||||
|
||||
var flow = FlowShapePredictor.PredictSiteScript("MesMoveOut", body);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
(FlowStepKind.ScriptRun, FlowLane.Site, "MesMoveOut"),
|
||||
(FlowStepKind.AttributeWrite, FlowLane.Tags, "MoveOutCompleteFlag"),
|
||||
(FlowStepKind.AttributeWait, FlowLane.Site, "MoveOutCompleteFlag"),
|
||||
(FlowStepKind.AttributeWrite, FlowLane.Tags, "batch + MoveOutFlag"),
|
||||
(FlowStepKind.AttributeWait, FlowLane.Site, "MoveOutCompleteFlag"),
|
||||
], flow.Nodes.Select(n => (n.Kind, n.Lane, n.Title)).ToList());
|
||||
Assert.Equal("reset", flow.Nodes[1].Note);
|
||||
Assert.Equal("reset echo", flow.Nodes[2].Note);
|
||||
}
|
||||
|
||||
/// <summary>Named reset arguments resolve by name, not position.</summary>
|
||||
[Fact]
|
||||
public void ResetAwareWriteBatchAndWait_NamedArguments_ResolveTheResetKey()
|
||||
{
|
||||
const string body = """
|
||||
var hs = await Attributes.WriteBatchAndWaitAsync(data, "Trig", true, "Done", true, t,
|
||||
resetKey: "Done", resetValue: false);
|
||||
""";
|
||||
|
||||
var flow = FlowShapePredictor.PredictSiteScript("S", body);
|
||||
|
||||
Assert.Equal("Done", flow.Nodes[1].Title);
|
||||
Assert.Equal(FlowStepKind.AttributeWrite, flow.Nodes[1].Kind);
|
||||
Assert.Equal(5, flow.Nodes.Count);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user