Files
wwtools/graccesscli/analysis/ide-edit-investigation/probe_setvalue/Program.cs
T
Joseph Doherty c52d8d0171 graccesscli: correct script-edit docs to TN-537 truth (writes DO persist)
I was wrong. AVEVA Tech Note 537 ("Creating an Application Object Script
Using GRAccess", April 2008) documents the supported pattern:
ConfigurableAttributes[<script>.<field>].SetValue(MxValue) inside a
CheckOut/Save/CheckIn cycle. graccesscli's existing
FindAttributeForMutation already follows this — writes to MxCategoryPackageOnly_Lockable
script-text fields persist correctly.

The earlier "writeback gap" diagnosis was a phantom caused by a reader-side
issue. `object attribute value get` against a script body returns
"Supported: False / Attribute value is not exposed" because
MxValueDetails uses a case-sensitive `ReadProperty(attr, "Value")` lookup
plus an accessor probe (GetBoolean -> GetInteger -> GetFloat -> GetDouble
-> GetString) that can fall through silently for some MxValue shapes. The
COM-side property is exposed as `value` (lowercase), readable as
`attr.value.GetString()` -- which the live probe at
`analysis/ide-edit-investigation/probe_setvalue/` does and confirms the
post-write content matches the marker exactly.

Live verification on $TestMachine.UpdateTestChangingInt.DeclarationsText
and $DelmiaReceiver.ProcessRecipe.{ExecuteText,DeclarationsText}:

  === verdict ===
    marker landed on same-proxy   ConfigurableAttributes: True
    marker landed on same-proxy   Attributes            : True
    marker landed on fresh-proxy  ConfigurableAttributes: True
    marker landed on fresh-proxy  Attributes            : True

The probe also confirmed that two earlier graccesscli `object scripts set`
invocations (which I had wrongly believed failed) had persisted -- the
marker text I wrote previously was still on disk in
ProcessRecipe.{ExecuteText,DeclarationsText} when read directly via
attr.value.GetString(). The probe restored both fields to their original
values.

This commit:

- Updates the misleading [Command(...)] / [CommandOption(...)]
  descriptions in GRAccessSurfaceCommands.cs back to honest versions
  citing TN-537.
- Restores the --file-using examples for `object scripts set` and
  `object scripts create` across script-editing.md, llm-integration.md,
  usage.md, and zb-testmachine.md.
- Removes the test that asserted the (wrong) EnsureMutableViaSetValue
  guard. Re-aims ScriptCommandDescriptions_… at the corrected wording.
- Removes two leftover EnsureMutableViaSetValue calls in the trigger-period
  / trigger-type write paths (both targeted MxCategoryWriteable_C_Lockable
  attributes; would never have fired even if the helper still existed).
- Adds analysis/ide-edit-investigation/REPORT.md (replacing the earlier
  wrong report) plus the probe sources under probe_setvalue/.

The MxValueDetails reader gap (case-sensitive ReadProperty + accessor
probe) is a real follow-up: `object attribute value get` should
case-insensitively read `value` and try GetString first when the
underlying MxValue.DataType is MxString. Out of scope here -- that's a
separate, smaller fix.

Test count delta: 67 -> 66 (-2 wrong tests, +1 corrected description test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:33:51 -04:00

132 lines
5.9 KiB
C#

using System;
using ArchestrA.GRAccess;
class Program
{
static int Main(string[] args)
{
// Probe: write to $TestMachine.UpdateTestChangingInt.DeclarationsText (a
// package-only ScriptExtension text field) via the TN-537 pattern using
// ConfigurableAttributes. Read back through TWO paths to discriminate
// between (a) write-side no-op vs (b) read-side staleness:
// 1. Same IgObject proxy via obj.ConfigurableAttributes (in-memory edit view)
// 2. Fresh galaxy.QueryObjectsByName proxy via .Attributes (runtime view)
//
// Restores DeclarationsText to empty when done.
const string GalaxyName = "ZB";
const string Tagname = "$TestMachine";
const string ScriptName = "UpdateTestChangingInt";
const string FieldName = "DeclarationsText";
const string Marker = "// graccesscli probe marker — TN537 ConfigurableAttributes path\n";
var user = Environment.GetEnvironmentVariable("MX_TEST_USER") ?? "";
var pass = Environment.GetEnvironmentVariable("MX_TEST_PASSWORD") ?? "";
var node = Environment.MachineName;
Console.WriteLine($"[probe] connecting to {node} galaxy={GalaxyName} as {user}");
var gr = new GRAccessAppClass();
var galaxies = gr.QueryGalaxies(node);
var galaxy = galaxies[GalaxyName];
if (string.IsNullOrEmpty(user))
galaxy.Login();
else
galaxy.Login(user, pass);
var attrPath = ScriptName + "." + FieldName;
// Acquire the target template object.
var names = new string[] { Tagname };
var initial = QueryOne(galaxy, names);
// Read current value via ConfigurableAttributes (configuration view).
var beforeCfg = TryReadString(initial.ConfigurableAttributes, attrPath, "before-cfg");
var beforeRt = TryReadString(initial.Attributes, attrPath, "before-rt");
Console.WriteLine($"[probe] BEFORE cfg={Quote(beforeCfg)} rt={Quote(beforeRt)}");
// Mutate via the canonical TN-537 sequence on the same proxy.
Console.WriteLine("[probe] CheckOut...");
initial.CheckOut();
ThrowIfFailed(initial.CommandResult, "CheckOut");
Console.WriteLine("[probe] ConfigurableAttributes[...].SetValue(MxValue)...");
var cfgAttr = initial.ConfigurableAttributes[attrPath];
var v = new MxValueClass();
v.PutString(Marker);
cfgAttr.SetValue(v);
Console.WriteLine("[probe] Save...");
initial.Save();
ThrowIfFailed(initial.CommandResult, "Save");
Console.WriteLine("[probe] CheckIn...");
initial.CheckIn("graccesscli probe round-trip test");
ThrowIfFailed(initial.CommandResult, "CheckIn");
// Now read back THREE ways:
// A. Same proxy (initial), ConfigurableAttributes — possibly stale.
// B. Same proxy (initial), Attributes — possibly stale.
// C. Fresh proxy from a fresh QueryObjectsByName call.
var afterCfgSame = TryReadString(initial.ConfigurableAttributes, attrPath, "after-cfg-same");
var afterRtSame = TryReadString(initial.Attributes, attrPath, "after-rt-same");
Console.WriteLine($"[probe] AFTER (same proxy) cfg={Quote(afterCfgSame)} rt={Quote(afterRtSame)}");
var refreshed = QueryOne(galaxy, names);
var afterCfgFresh = TryReadString(refreshed.ConfigurableAttributes, attrPath, "after-cfg-fresh");
var afterRtFresh = TryReadString(refreshed.Attributes, attrPath, "after-rt-fresh");
Console.WriteLine($"[probe] AFTER (fresh proxy) cfg={Quote(afterCfgFresh)} rt={Quote(afterRtFresh)}");
// Restore: write the original value back (or empty if there was none).
var restoreTo = beforeCfg ?? "";
Console.WriteLine($"[probe] Restoring DeclarationsText to {Quote(restoreTo)}...");
refreshed.CheckOut();
ThrowIfFailed(refreshed.CommandResult, "CheckOut(restore)");
var restoreAttr = refreshed.ConfigurableAttributes[attrPath];
var rv = new MxValueClass();
rv.PutString(restoreTo);
restoreAttr.SetValue(rv);
refreshed.Save();
ThrowIfFailed(refreshed.CommandResult, "Save(restore)");
refreshed.CheckIn("graccesscli probe restore");
ThrowIfFailed(refreshed.CommandResult, "CheckIn(restore)");
Console.WriteLine("[probe] Restored.");
// Verdict.
Console.WriteLine();
Console.WriteLine("=== verdict ===");
Console.WriteLine($" marker landed on same-proxy ConfigurableAttributes: {(afterCfgSame == Marker)}");
Console.WriteLine($" marker landed on same-proxy Attributes : {(afterRtSame == Marker)}");
Console.WriteLine($" marker landed on fresh-proxy ConfigurableAttributes: {(afterCfgFresh == Marker)}");
Console.WriteLine($" marker landed on fresh-proxy Attributes : {(afterRtFresh == Marker)}");
return 0;
}
static IgObject QueryOne(IGalaxy galaxy, string[] names)
{
var objs = galaxy.QueryObjectsByName(EgObjectIsTemplateOrInstance.gObjectIsTemplate, ref names);
ThrowIfFailed(galaxy.CommandResult, "QueryObjectsByName");
return objs[1];
}
static string TryReadString(IAttributes attrs, string name, string label)
{
try
{
var attr = attrs[name];
if (attr == null) return null;
var val = attr.value;
return val?.GetString();
}
catch (Exception ex) { Console.WriteLine($"[probe] {label} read threw: {ex.Message}"); return null; }
}
static void ThrowIfFailed(ICommandResult r, string what)
{
if (r != null && !r.Successful)
throw new InvalidOperationException($"{what} failed: ID={r.ID} Text='{r.Text}' Custom='{r.CustomMessage}'");
}
static string Quote(string s) => s == null ? "<null>" : "\"" + s.Replace("\n", "\\n").Replace("\r", "\\r") + "\"";
}