Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
# unrun for exactly the reason this job exists.
- name: Pascal and Python helpers agree
run: |
python -m pytest tests/test_cross_validate.py tests/test_pascal_runner.py -q -rs --durations=10 | Tee-Object -Variable captured
python -m pytest tests/test_cross_validate.py tests/test_pascal_runner.py tests/test_sch_properties_match_their_interface.py -q -rs --durations=10 | Tee-Object -Variable captured
# Read the exit code BEFORE anything else runs. Piping a native
# command into Tee-Object means the step's own success is the
# pipeline's, not pytest's, so without this a failing
Expand Down
72 changes: 70 additions & 2 deletions docs/RELEASE_VERIFICATION.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Release verification: 2026.09.23.4

Everything below is Pascal that FPC and the linter have checked and that
**Altium's DelphiScript engine has never executed**. The two are not the
Unless a section records live verification explicitly, the Pascal below has
been checked by FPC and the linter but **not executed by Altium's DelphiScript engine**. The two are not the
same: each accepts identifiers the other rejects, and an undeclared one
faults at runtime where `Try/Except` cannot catch it, halting the
polling loop.
Expand Down Expand Up @@ -1034,3 +1034,71 @@ it. A library edit is real in memory and absent from disk until then.
Altium's font size is not in mils and the conversion is undocumented,
so the source range is reported rather than guessed. Calibrating it
needs a live measurement.


## Generic property rejection on Altium 21 (live verified)

Recorded on Altium 21.4.1.30: `IsHidden` on `eNetLabel` and `Text` on
`eParameterSet` opened an undeclared-identifier dialog and stopped polling.
The fix guards reads and writes before accessing those members. It does not
claim a complete property capability table for every Altium version.

Unsupported query fields remain empty and are listed in
`properties.unreadable`; other requested fields remain available. Unsupported
writes are listed in `properties.unknown` and are not reported as applied.
Single creation returns `UNSUPPORTED_PROPERTY` with the property and object
type in the error message. Batch creation rejects only that item, without
registering it, and includes `reason: "UNSUPPORTED_PROPERTY"`, `property`,
`object_type`, and the zero-based `index` in `failures`. Valid items continue.
This preflight covers the guarded Text and IsHidden combinations, not every
unknown property. Existing inputs and successful response fields are unchanged.

Use visible net labels with `Text`; do not hide them with `IsHidden`. Query a
parameter-set directive's coordinates without `Text`. The bridge does not
substitute a different field for the rejected property.

Live verification of commit `89c017f` on Altium 21.4.1.30 passed on
2026-09-29 after a full Altium restart. The deployed Generic.pas SHA-256 was
`36f9c0dde95de366a1b0ea60bc653793ed6f915c1c7714d08ee20bb791aef57e`.

- Unsupported reads returned empty fields plus unreadable diagnostics, while
coordinates and the directive Name remained readable.
- Single writes returned success=false with unknown-property diagnostics;
batch writes reported both rejected properties in their diagnostic list.
- Both single creates failed without changing object counts. The mixed batch
returned created=2, failed=2, total=4, with failures at indexes 0 and 2 and
the expected property/type. Queries confirmed only the valid objects existed.
- Ordinary parameter IsHidden read back false, true, then false after restore.
- Pings and valid queries succeeded after every rejecting call. No error modal
or polling restart was needed. The recorded response assertions passed.
- Free Pascal execution remains separately pending: local FPC is absent and
the upstream GitHub run is action_required with no jobs executed.

For future acceptance runs, close unrelated design documents first or retain
byte-for-byte backups. The bridge's save-all operation rewrites open clean
schematics too; it must not be treated as a no-op just because app_context
reports no unsaved documents.

Repeatable acceptance, on a disposable schematic only:

1. Save normal work, install the candidate scripts, restart Altium (scripts
are cached), start the bridge, and confirm `app_ping` succeeds.
2. Create an ordinary visible net label and parameter-set directive. Record
their count and positions. Query valid label Text and directive coordinates.
3. Query `IsHidden` on the label and `Text` on the directive, each alongside
coordinates. Expect empty unsupported fields, unreadable diagnostics, and
unchanged valid coordinates. Neither call may open a modal or stop polling.
4. Attempt those writes with `obj_modify` and `obj_batch_modify`. Expect
failure diagnostics, unchanged objects, and no modal. These operations
retain existing partial-write semantics for other valid assignments.
5. Attempt each unsupported pair through `obj_create` and `obj_batch_create`.
Verify failed objects are absent. In a batch containing invalid, valid,
invalid, and valid items, expect two creations, two indexed failures and
no property diagnostic leaking from one item into another.
6. Exercise IsHidden on an ordinary parameter supporting visibility and
confirm read/write behaviour is unchanged. Restore its original value.
7. After each rejection, run `app_ping` and a valid query. Both must complete
without dismissing an error or restarting the polling loop.

Record exact Altium and bridge versions, responses, and object counts. Offline
source guards and Free Pascal tests do not establish live Altium acceptance.
163 changes: 125 additions & 38 deletions scripts/altium/Generic.pas
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,57 @@
Begin
Result := True;
If Obj = Nil Then Exit;
{ Both of these name themselves with Name, not Text. }
If (Obj.ObjectId = ePort) Or (Obj.ObjectId = eSheetEntry) Then
{ Ports, sheet entries and parameter-set directives do not expose Text. }
If (Obj.ObjectId = ePort) Or (Obj.ObjectId = eSheetEntry)
Or (Obj.ObjectId = eParameterSet) Then
Result := False;
End;

{ AD21 net labels do not expose IsHidden. Test before member access: an
undeclared identifier opens a modal that Try/Except cannot contain. }
Function SchObjectHasIsHidden(Obj : ISch_GraphicalObject) : Boolean;
Begin
Result := False;
If Obj = Nil Then Exit;
Result := Obj.ObjectId <> eNetLabel;
End;

{ Preflight only the known unsupported property/type pairs. Preserve the
existing treatment of other names; do not guess a complete capability map. }
Function UnsupportedSchProperty(Obj : ISch_GraphicalObject; SetStr : String) : String;
Var
Remaining, Assignment, PropName : String;
PipePos, EqPos : Integer;
Begin
Result := '';
Remaining := SetStr;
While Remaining <> '' Do
Begin
PipePos := Pos('|', Remaining);
If PipePos > 0 Then
Begin
Assignment := Copy(Remaining, 1, PipePos - 1);
Remaining := Copy(Remaining, PipePos + 1, Length(Remaining));
End
Else
Begin
Assignment := Remaining;
Remaining := '';
End;
EqPos := Pos('=', Assignment);
If EqPos > 0 Then
Begin
PropName := Copy(Assignment, 1, EqPos - 1);
If ((PropName = 'Text') And (Not SchObjectHasText(Obj)))
Or ((PropName = 'IsHidden') And (Not SchObjectHasIsHidden(Obj))) Then
Begin
Result := PropName;
Exit;
End;
End;
End;
End;

Function SchObjectHasOrientation(Obj : ISch_GraphicalObject) : Boolean;
Begin
Result := True;
Expand Down Expand Up @@ -642,7 +688,13 @@
Result := GetSchVertexProperty(Obj, PropName)

// Boolean properties
Else If PropName = 'IsHidden' Then Result := BoolToJsonStr(Obj.IsHidden)
Else If PropName = 'IsHidden' Then
Begin
If SchObjectHasIsHidden(Obj) Then
Result := BoolToJsonStr(Obj.IsHidden)
Else
NotePropertyDiag('unreadable', PropName);
End
Else If PropName = 'IsSolid' Then Result := BoolToJsonStr(Obj.IsSolid)
Else If PropName = 'IsMirrored' Then Result := BoolToJsonStr(Obj.IsMirrored);
Except
Expand Down Expand Up @@ -759,7 +811,7 @@
If SchObjectHasText(Obj) Then
Obj.Text := Value
Else
NotePropertyDiag('unknown', PropName);
Matched := False;
End
Else If PropName = 'Name' Then Obj.Name := Value
Else If PropName = 'LibReference' Then Obj.LibReference := Value
Expand Down Expand Up @@ -860,7 +912,13 @@
Else If PropName = 'YSize' Then Obj.YSize := MilsToCoord(StrToIntDef(Value, 0))

// Boolean properties
Else If PropName = 'IsHidden' Then Obj.IsHidden := StrToBool(Value)
Else If PropName = 'IsHidden' Then
Begin
If SchObjectHasIsHidden(Obj) Then
Obj.IsHidden := StrToBool(Value)
Else
Matched := False;
End
Else If PropName = 'IsSolid' Then Obj.IsSolid := StrToBool(Value)
{ MIRROR IS NOT A PLAIN PROPERTY WRITE. }
{ }
Expand Down Expand Up @@ -1974,13 +2032,15 @@

Function Gen_CreateObject(Params : String; RequestId : String) : String;
Var
ObjTypeStr, PropsStr, Container : String;
ObjTypeStr, PropsStr, Container, UnsupportedProp : String;
ObjTypeInt : Integer;
SchDoc : ISch_Document;
SchLib : ISch_Lib;
Component : ISch_Component;
NewObj : ISch_GraphicalObject;
Begin
ResetPropertyDiag(0);
SchDoc := Nil;
ObjTypeStr := ExtractJsonValue(Params, 'object_type');
PropsStr := ExtractJsonValue(Params, 'properties');
Container := ExtractJsonValue(Params, 'container');
Expand All @@ -2001,6 +2061,15 @@
Exit;
End;

UnsupportedProp := UnsupportedSchProperty(NewObj, PropsStr);
If UnsupportedProp <> '' Then
Begin
SchServer.DestroySchObject(NewObj);
Result := BuildErrorResponse(RequestId, 'UNSUPPORTED_PROPERTY',
'Property ' + UnsupportedProp + ' is not supported on ' + ObjTypeStr);
Exit;
End;

// Set properties
ApplySetProperties(NewObj, PropsStr);

Expand Down Expand Up @@ -7722,9 +7791,11 @@
NewObj : ISch_GraphicalObject;
ActiveDoc : ISch_Document;
ContainerStr : String;
FailuresJson, ItemReason : String;
FailuresJson, ItemReason, UnsupportedProp : String;
FirstFailure : Boolean;
Begin
ResetPropertyDiag(0);
SchDoc := Nil;
Operations := ExtractJsonValue(Params, 'operations');
If Operations = '' Then
Begin
Expand All @@ -7749,6 +7820,8 @@
If Op = '' Then Break;
OpCount := OpCount + 1;
ItemReason := '';
UnsupportedProp := '';
ResetPropertyDiag(0);
Scope := GetBatchField(Op, 'scope');
If Scope = '' Then Scope := 'active_doc';
ObjTypeStr := GetBatchField(Op, 'object_type');
Expand All @@ -7772,48 +7845,58 @@
End
Else
Begin
ApplySetProperties(NewObj, PropsStr);

If ContainerStr = 'component' Then
UnsupportedProp := UnsupportedSchProperty(NewObj, PropsStr);
If UnsupportedProp <> '' Then
Begin
SchServer.DestroySchObject(NewObj);
Inc(Failed);
ItemReason := 'UNSUPPORTED_PROPERTY';
End
Else
Begin
SchLib := SchServer.GetCurrentSchDocument;
If (SchLib <> Nil) And (SchLib.ObjectId = eSchLib) Then
ApplySetProperties(NewObj, PropsStr);

If ContainerStr = 'component' Then
Begin
Component := SchLib.CurrentSchComponent;
If Component <> Nil Then
SchLib := SchServer.GetCurrentSchDocument;
If (SchLib <> Nil) And (SchLib.ObjectId = eSchLib) Then
Begin
Component.AddSchObject(NewObj);
SchRegisterObject(Component, NewObj);
Inc(Created);
Component := SchLib.CurrentSchComponent;
If Component <> Nil Then
Begin
Component.AddSchObject(NewObj);
SchRegisterObject(Component, NewObj);
Inc(Created);
End
Else
Begin
SchServer.DestroySchObject(NewObj);
Inc(Failed);
ItemReason := 'NO_COMPONENT';
End;
End
Else
Begin
SchServer.DestroySchObject(NewObj);
Inc(Failed);
ItemReason := 'NO_COMPONENT';
ItemReason := 'NO_SCHLIB';
End;
End
Else
Begin
SchServer.DestroySchObject(NewObj);
Inc(Failed);
ItemReason := 'NO_SCHLIB';
End;
End
Else
Begin
SchDoc := ActiveDoc;
If SchDoc = Nil Then
Begin
SchServer.DestroySchObject(NewObj);
Inc(Failed);
ItemReason := 'NO_SCHEMATIC';
End
Else
Begin
SchDoc.RegisterSchObjectInContainer(NewObj);
SchRegisterObject(SchDoc, NewObj);
Inc(Created);
SchDoc := ActiveDoc;
If SchDoc = Nil Then
Begin
SchServer.DestroySchObject(NewObj);
Inc(Failed);
ItemReason := 'NO_SCHEMATIC';
End
Else
Begin
SchDoc.RegisterSchObjectInContainer(NewObj);
SchRegisterObject(SchDoc, NewObj);
Inc(Created);
End;
End;
End;
End;
Expand All @@ -7826,7 +7909,11 @@
FailuresJson := FailuresJson +
'{"index":' + IntToStr(OpCount - 1) +
',"object_type":"' + EscapeJsonString(ObjTypeStr) +
'","reason":"' + ItemReason + '"}';
'","reason":"' + ItemReason + '"';
If UnsupportedProp <> '' Then
FailuresJson := FailuresJson + ',"property":"' +
EscapeJsonString(UnsupportedProp) + '"';
FailuresJson := FailuresJson + '}';
End;
End;
Finally
Expand Down
6 changes: 5 additions & 1 deletion src/eda_agent/tools/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2424,7 +2424,11 @@ async def obj_batch_create(
])

Returns:
Dict with created, failed, total counts.
Dict with created, failed, total counts and indexed failures.
Known unsupported properties (for example IsHidden on eNetLabel
or Text on eParameterSet) reject that item before registration.
Its failure includes reason UNSUPPORTED_PROPERTY, property and
object_type. Other valid items still run.
"""
op_strs: list[str] = []
for op in operations:
Expand Down
Loading