diff --git a/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs b/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs index 292a39a..f754e35 100644 --- a/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs +++ b/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs @@ -982,7 +982,14 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess if (string.IsNullOrWhiteSpace(attributeName)) throw new ArgumentException("Attribute name is required."); - return TryFindAttribute(obj.Attributes, attributeName) ?? TryFindAttribute(obj.ConfigurableAttributes, attributeName) + // Prefer ConfigurableAttributes for reads too. The runtime + // `obj.Attributes` collection sometimes returns an IAttribute proxy + // for dotted/extension names that resolves `.value` to a synthesized + // MxValue containing only the parent attribute's prefix (e.g. reading + // `ProcessRecipe.ExecuteText` returned `"Me.RecipeDownloadFlag"`, + // truncated at the first `=`). ConfigurableAttributes returns the + // proper IAttribute backed by the actual ScriptExtension projection. + return TryFindAttribute(obj.ConfigurableAttributes, attributeName) ?? TryFindAttribute(obj.Attributes, attributeName) ?? throw new InvalidOperationException($"Attribute '{attributeName}' was not found."); } @@ -1002,6 +1009,20 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess catch { return null; } } + // Anonymous-type property check that doesn't go through the reflection + // path used by `ReadProperty` / `ReadString`. The reflection path + // intermittently returns null/empty on anonymous types from this + // assembly, which made AttributeValueDetails fall through to the + // package-binary fallback even when the direct read had succeeded. + private static bool IsDirectSupported(object direct) + { + if (direct == null) return false; + var prop = direct.GetType().GetProperty("Supported"); + if (prop == null) return false; + var v = prop.GetValue(direct); + return v is bool b && b; + } + private static string SafeAttributeName(IAttribute attr) { if (attr == null) return string.Empty; @@ -1354,13 +1375,6 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess private static object AttributeValueDetails(IGalaxy galaxy, IgObject obj, IAttribute attr) { var unavailable = new List(); - // Use the typed `IAttribute.value` accessor. For ScriptExtension / - // PackageOnly fields the IAttribute proxy must come from - // `ConfigurableAttributes`, not the runtime `Attributes` snapshot — - // the runtime view often returns a stub whose `.value` is null even - // when the persisted value is set. Re-look-up from - // ConfigurableAttributes by name when needed; fall back to whatever - // `attr` was passed in if the configurable lookup fails. object rawMxValue = TryReadAttributeValue(attr); if (rawMxValue == null) { @@ -1376,7 +1390,12 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess var package = new PackageSnapshot(); var packageValue = (PackageAttributeValue)null; - if (!StringEquals(ReadString(direct, "Supported"), "True")) + // Use direct property access to check Supported. Reflecting on the + // anonymous type via ReadString/ReadProperty was unreliable enough + // to trigger the package fallback even when MxValueDetails returned + // Supported=true — the package binary parser then returned a + // truncated old value, masking the live attribute read. + if (!IsDirectSupported(direct)) { package = TryReadPackageSnapshot(galaxy, obj, ObjectDetails(obj), unavailable); packageValue = FindPackageAttributeValue(package, attr.Name); @@ -1629,12 +1648,36 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess private static object ObjectScript(IGalaxy galaxy, IgObject obj, string scriptName) { var unavailable = new List(); - var package = TryReadPackageSnapshot(galaxy, obj, ObjectDetails(obj), unavailable); var attributeName = ScriptAttributeName(scriptName); + + // Try the live attribute path FIRST. The package-binary parser + // truncates strings at certain delimiters (e.g. `=`), so when a + // marker like `// foo` is present in ExecuteText the parser + // returns only the prefix. The direct GRAccess attribute read via + // ConfigurableAttributes returns the full string. + string directBody = null; + try + { + var attr = TryFindAttribute(obj.ConfigurableAttributes, attributeName) + ?? TryFindAttribute(obj.Attributes, attributeName); + if (attr != null) + { + var raw = TryReadAttributeValue(attr); + if (raw is IMxValue mx) directBody = mx.GetString(); + } + } + catch { directBody = null; } + + var package = TryReadPackageSnapshot(galaxy, obj, ObjectDetails(obj), unavailable); var match = ObjectScripts(galaxy, obj, package) .FirstOrDefault(s => string.Equals(ReadString(s, "Name"), scriptName, StringComparison.OrdinalIgnoreCase) || string.Equals(ReadString(s, "Name"), attributeName, StringComparison.OrdinalIgnoreCase)); - var body = FindPackageScriptBody(package, attributeName) ?? FindPackageScriptBody(package, scriptName); + var packageBody = FindPackageScriptBody(package, attributeName) ?? FindPackageScriptBody(package, scriptName); + + string body = directBody ?? packageBody?.Body; + string source = directBody != null + ? "direct-graccess" + : (packageBody != null ? packageBody.Source : null); if (match == null && body == null) throw new InvalidOperationException($"Script '{scriptName}' was not found in script-like object attributes."); @@ -1642,10 +1685,10 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess return new { Metadata = match, - Body = body?.Body, + Body = body, BodyAvailable = body != null, PackageFallbackUsed = package.PackageFallbackUsed, - Source = body == null ? "direct-graccess" : body.Source, + Source = source ?? "direct-graccess", Unavailable = body == null ? unavailable.Concat(new[] { new LlmUnavailableField { Field = "scriptBody", Reason = "Script body was not exposed through direct GRAccess and was not found in the exported package." } }).ToList() : unavailable