feat(cli): flow-debug run-method|run-value-change|replay
New top-level command group for the Flow Debugger (design 2026-09-05 §7.1). run-method / run-value-change post the FlowDebugRun* management commands with a 2-minute client wait; replay posts FlowDebugReplayCommand. A table projection unwraps the FlowDebugRunResult envelope to one row per step (occurred | kind | lane | target | durationMs | status | error) using the Commons FlowStepProjector's kind/lane resolution, with a trailing note row for an incomplete run (error code verbatim, e.g. STANDBY_NODE) or a truncated event list. --params must be a JSON object (INVALID_PARAMS otherwise). README section added; command-tree test bumped to 18 groups. Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Compact table projection for the <c>flow-debug</c> commands.
|
||||
///
|
||||
/// <para>
|
||||
/// The handler returns a <c>FlowDebugRunResult</c> — <c>{ rootExecutionId, completed,
|
||||
/// error, errorCode, events: [ … ], truncated, … }</c> whose <c>events</c> are canonical
|
||||
/// audit events (each carrying its domain fields inside a <c>detailsJson</c> string). Handed
|
||||
/// to the generic renderer unprojected, the object takes the Property/Value branch and the
|
||||
/// whole event array lands in one cell as raw JSON. This unwraps it to one row per step —
|
||||
/// <c>occurred | kind | lane | target | durationMs | status | error</c> — in the same
|
||||
/// chronological order the Flow Debugger page draws them, so a terminal user can read the
|
||||
/// swimlane top-to-bottom. A trailing <c>note</c> row reports a failed / incomplete run
|
||||
/// (with the error code verbatim, e.g. <c>STANDBY_NODE</c>) or a truncated event list.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>JSON output is never touched — <c>--format json</c> gets the full raw result.</para>
|
||||
/// </summary>
|
||||
internal static class FlowDebugTableProjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Projects a <c>FlowDebugRunResult</c> body to the row array the table renderer
|
||||
/// consumes. Returns the input unchanged when it is not JSON or not that shape, so the
|
||||
/// renderer's own fallbacks still apply.
|
||||
/// </summary>
|
||||
/// <param name="json">The raw success JSON body from the management API.</param>
|
||||
/// <returns>Compact JSON suitable for table rendering.</returns>
|
||||
internal static string ProjectEvents(string json)
|
||||
{
|
||||
JsonDocument doc;
|
||||
try
|
||||
{
|
||||
doc = JsonDocument.Parse(json);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return json;
|
||||
}
|
||||
|
||||
using (doc)
|
||||
{
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object
|
||||
|| !TryGetPropertyCI(root, "events", out var events)
|
||||
|| events.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return json;
|
||||
}
|
||||
|
||||
var rows = new JsonArray();
|
||||
foreach (var evt in events.EnumerateArray())
|
||||
rows.Add(ProjectEvent(evt));
|
||||
|
||||
var completed = TryGetPropertyCI(root, "completed", out var c) && c.ValueKind == JsonValueKind.True;
|
||||
var errorCode = StrValue(root, "errorCode");
|
||||
var error = StrValue(root, "error");
|
||||
var truncated = TryGetPropertyCI(root, "truncated", out var t) && t.ValueKind == JsonValueKind.True;
|
||||
|
||||
var notes = new List<string>();
|
||||
if (!completed || errorCode != null || error != null)
|
||||
{
|
||||
notes.Add(errorCode != null
|
||||
? $"{errorCode}: {error ?? "run did not complete"}"
|
||||
: error ?? "run did not complete");
|
||||
}
|
||||
|
||||
if (truncated)
|
||||
notes.Add("event list truncated — the audit rows remain complete (audit tree --execution-id <root>)");
|
||||
|
||||
if (notes.Count > 0)
|
||||
rows.Add(new JsonObject { ["note"] = JsonValue.Create(string.Join("; ", notes)) });
|
||||
|
||||
return rows.ToJsonString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Projects one canonical audit event (camelCase JSON) to its compact row.</summary>
|
||||
private static JsonNode ProjectEvent(JsonElement evt)
|
||||
{
|
||||
if (evt.ValueKind != JsonValueKind.Object)
|
||||
return JsonValue.Create(evt.ToString())!;
|
||||
|
||||
var details = AuditDetailsCodec.Deserialize(StrValue(evt, "detailsJson"));
|
||||
var kind = FlowStepProjector.ResolveKind(details.Channel, details.Kind);
|
||||
var lane = FlowStepProjector.ResolveLane(kind, details.SourceSiteId);
|
||||
|
||||
string? occurred = null;
|
||||
if (TryGetPropertyCI(evt, "occurredAtUtc", out var o) && o.ValueKind == JsonValueKind.String
|
||||
&& o.TryGetDateTimeOffset(out var when))
|
||||
{
|
||||
occurred = when.ToUniversalTime().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var target = StrValue(evt, "target");
|
||||
var instance = details.SourceInstanceId;
|
||||
var shownTarget = target ?? details.SourceScript;
|
||||
if (instance != null && shownTarget != null && !shownTarget.StartsWith(instance, StringComparison.Ordinal))
|
||||
shownTarget = $"{instance}.{shownTarget}";
|
||||
|
||||
return new JsonObject
|
||||
{
|
||||
["occurred"] = occurred,
|
||||
["kind"] = kind.ToString(),
|
||||
["lane"] = lane.ToString(),
|
||||
["target"] = shownTarget ?? instance,
|
||||
["durationMs"] = details.DurationMs,
|
||||
["status"] = details.Status,
|
||||
["error"] = details.ErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetPropertyCI(JsonElement obj, string name, out JsonElement value)
|
||||
{
|
||||
foreach (var prop in obj.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = prop.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string? StrValue(JsonElement obj, string name)
|
||||
=> TryGetPropertyCI(obj, name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
|
||||
}
|
||||
Reference in New Issue
Block a user