From bd95ace1c5d8e29ab15c15b77e2dd98eb6f5b019 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 5 May 2026 15:45:07 -0400 Subject: [PATCH] graccesscli: fail fast on package-only attribute writes (writeback gap fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigation findings: IAttribute.SetValue silently no-ops for any attribute whose AttributeCategory starts with `MxCategoryPackageOnly`. The COM call returns Successful=true and the dispatcher reports OK, but a verify-read via direct IAttribute.GetValue confirms the value is unchanged. Discovered while validating the new --field flag round-trip on \$DelmiaReceiver.ProcessRecipe.ExecuteText: - BEFORE write: ExecuteText body = `Me.RecipeDownloadFlag = false;` - WRITE: scripts set --field ExecuteText --file marker.txt → OK - AFTER write: ExecuteText body = `Me.RecipeDownloadFlag = false;` (UNCHANGED — the marker prefix from marker.txt is absent) - ConfigurableAttributes shows `ProcessRecipe.ExecuteText` category = `MxCategoryPackageOnly_Lockable`. Same for DeclarationsText, StartupText, ShutdownText, OnScanText, OffScanText, Expression. - Per docs/script-parsing.md:8, "Object-level scripts ... may appear as attributes, extension attributes, or only inside exported object packages" — script-text fields are in the third bucket. Trigger settings (TriggerType, TriggerPeriod) are `MxCategoryWriteable_C_Lockable` and remain effective via SetValue — verified by `scripts settings set --trigger-period-ms 1000` returning OK on a clean checkout state. Add `EnsureMutableViaSetValue(IAttribute, name)` helper that throws InvalidOperationException with a clear remediation hint when called on a package-only attribute. Wired into three write sites: - `case "value-set"` (object attribute value set) - `ObjectScriptSet` (scripts set body writer) - `SetScriptSettings` Expression branch (the one trigger-related field that's package-only; TriggerType/TriggerPeriod stay unguarded) Validation against live ZB galaxy: - Before fix: `scripts set --field ExecuteText` returned `success: true` but didn't mutate the body. - After fix: same call returns `success: false` with `"Attribute 'ProcessRecipe.ExecuteText' has category 'MxCategoryPackageOnly_Lockable'. Package-only attributes silently no-op via IAttribute.SetValue and can only be edited through the package import/export round-trip..."` (exit code 1). - `scripts settings set --trigger-period-ms 1000` against the same script: still OK (TriggerPeriod is Writeable_C_Lockable). The real fix for editing script bodies is the package-rewrite path — export `.aaPKG`, modify the binary script-extension records, re-import via `galaxy.ImportObjects`. graccesscli already has the read half (`PackageSnapshotParser.Parse`); the write half is a separate followup. 61 → 63 → 63 (no test count delta this commit; the new helper is exercised end-to-end by the live validation above; unit tests for the failure path require a mock IAttribute proxy that's out of scope here). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../GRAccess/GRAccessCommandDispatcher.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 d6056f4..34fe325 100644 --- a/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs +++ b/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs @@ -502,6 +502,7 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess return AtomicObjectEdit(obj, editObj => { var editAttr = FindAttributeForMutation(editObj, Arg(args, "attribute")); + EnsureMutableViaSetValue(editAttr, Arg(args, "attribute")); editAttr.SetValue(CreateMxValue(Arg(args, "value"), Arg(args, "data-type", "string"))); return CommandSummary(editAttr, "Set attribute value"); }); @@ -1622,11 +1623,40 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess } var attr = FindAttributeForMutation(obj, attributeName); + EnsureMutableViaSetValue(attr, attributeName); var body = File.ReadAllText(file); attr.SetValue(CreateMxValue(body, "string")); return CommandSummary(attr, $"Set script {attributeName}"); } + // Checks whether `attr.SetValue` will actually persist on this attribute, + // or whether the attribute is package-only (in which case SetValue silently + // no-ops despite returning Successful=true). Discovered via investigation + // documented in the writeback-gap notes: ScriptExtension text fields + // (ExecuteText, DeclarationsText, StartupText, ShutdownText, OnScanText, + // OffScanText, Expression) are all `MxCategoryPackageOnly_Lockable` — + // editable only via the package import/export round-trip, never via the + // generic `IAttribute.SetValue` path. Same for any other attribute whose + // category starts with `MxCategoryPackageOnly`. + private static void EnsureMutableViaSetValue(IAttribute attr, string attributeName) + { + string category; + try { category = attr.AttributeCategory.ToString(); } + catch { return; } // best-effort; if we can't read the category, let the write proceed + + if (category != null && category.StartsWith("MxCategoryPackageOnly", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"Attribute '{attributeName}' has category '{category}'. " + + "Package-only attributes silently no-op via IAttribute.SetValue and can only be " + + "edited through the package import/export round-trip. " + + "For ScriptExtension bodies (ExecuteText / DeclarationsText / StartupText / etc.), " + + "edit the source object in the System Platform IDE and re-import, " + + "or use a future package-rewrite path. " + + "See docs/script-parsing.md:8."); + } + } + private static string ObjectScriptCreate(IGalaxy galaxy, IgObject obj, IDictionary args) { var scriptName = Arg(args, "script"); @@ -1691,6 +1721,7 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess if (!string.IsNullOrWhiteSpace(expression)) { var attr = FindAttributeForMutation(obj, scriptName + ".Expression"); + EnsureMutableViaSetValue(attr, scriptName + ".Expression"); attr.SetValue(CreateMxValue(expression, "string")); yield return CommandSummary(attr, $"Set script {scriptName}.Expression"); }