c52d8d0171
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>
132 lines
5.9 KiB
C#
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") + "\"";
|
|
}
|