* fix: ensure attribute slot resolution correctly references parent membership in exporter

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* fix: update resolveParentSlot to correctly propagate memberOf references and add regression test for attribute slot scoping

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* style: reformat testResultClassDoc for readability in detectSlotsRefined test suite

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

---------

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2026-06-03 14:44:04 +05:00
committed by GitHub
parent be5d41cecc
commit 46c7fed5dc
2 changed files with 90 additions and 4 deletions
@@ -1138,4 +1138,84 @@ describe('detectSlots refined', () => {
expect(subProcSlot).toBeDefined()
expect(subProcSlot.memberOf).toBeDefined() // It should refer to slot created for ClassB
})
test('Test 24: User Example (Execute-2.json simulation)', async () => {
const mainClassId = 'ClassA'
const testResultClassId = '69e8d19c618de58dbaf9ecf2'
const attemptsAttrId = 'custom6a00914a295035d76ab16e1d'
const attemptNumAttrId = 'custom69eb7f41889a0214e41368cb'
const mainClassDoc = { _id: mainClassId, _class: 'core:class:Class', name: 'ClassA', label: 'Main Entity' }
const testResultClassDoc = {
_id: testResultClassId,
_class: 'core:class:Class',
name: 'TestResult',
label: '(T) Test Result'
}
const attemptsAttrDoc = {
_id: attemptsAttrId,
_class: 'core:class:Attribute',
name: 'attempts',
label: 'Attempts',
attributeOf: mainClassId,
type: 'core:class:TypeNumber'
}
const attemptNumAttrDoc = {
_id: attemptNumAttrId,
_class: 'core:class:Attribute',
name: 'attemptNumber',
label: 'Attempt number',
attributeOf: testResultClassId,
type: 'core:class:TypeNumber'
}
m.findObject = jest.fn().mockImplementation((id) => {
if (id === mainClassId) return mainClassDoc
if (id === testResultClassId) return testResultClassDoc
if (id === attemptsAttrId) return attemptsAttrDoc
if (id === attemptNumAttrId) return attemptNumAttrDoc
if (id === 'process:method:CreateCard') return { _id: 'process:method:CreateCard', requiredParams: ['_class'] }
return undefined
})
h.findAttribute = jest.fn().mockImplementation((tag, attrId) => {
if (tag === mainClassId && attrId === attemptsAttrId) return attemptsAttrDoc
if (tag === testResultClassId && attrId === attemptNumAttrId) return attemptNumAttrDoc
return undefined
})
const transitions = [
{
_id: 'tr1',
actions: [
{
_id: 'act1',
methodId: 'process:method:CreateCard',
params: {
_class: testResultClassId,
[attemptNumAttrId]: `\${@${attemptsAttrId}}`
}
}
]
}
]
const slots: Record<string, any> = {}
const bindings: Record<string, string> = {}
detectSlots({ masterTag: mainClassId } as any, transitions as any[], slots, bindings, m, h)
// attempts (attemptsAttrId) belongs to mainClassId (masterTag), so its memberOf must be undefined.
// attemptNumber (attemptNumAttrId) belongs to testResultClassId, so its memberOf must point to the testResultClassId slot.
const attemptsSlotName = 'attempts'
const attemptNumSlotName = 'attemptNumber'
const testResultSlotName = '(T) Test Result'
expect(slots[attemptsSlotName]).toBeDefined()
expect(slots[attemptNumSlotName]).toBeDefined()
expect(slots[testResultSlotName]).toBeDefined()
expect(slots[attemptsSlotName].memberOf).toBeUndefined()
expect(slots[attemptNumSlotName].memberOf).toBe(`__SLOT_${testResultSlotName}__`)
})
})
+10 -4
View File
@@ -552,15 +552,18 @@ function resolveActionContext (
* its `__SLOT_name__` reference for use as `memberOf`.
*/
function resolveParentSlot (
targetTag: Ref<MasterTag>,
targetTag: Ref<MasterTag> | undefined,
masterTag: Ref<MasterTag>,
memberOfRef: string | undefined,
getOrAddSlot: (id: string, model: DetailedSlotModel) => string,
m: ModelDb
): string | undefined {
if (targetTag === undefined || targetTag === '' || normalizeId(targetTag) === normalizeId(masterTag)) {
if (targetTag === undefined || targetTag === '') {
return memberOfRef
}
if (normalizeId(targetTag) === normalizeId(masterTag)) {
return undefined
}
const targetDoc = m.findObject(targetTag) as Class<any> | undefined
const parentSlotId = getOrAddSlot(
targetTag,
@@ -807,7 +810,8 @@ function scanParamKey (
if (isSystemId(parentClassId) && !isContextualValue(val, activeTag, masterTag, h, m)) {
return
}
getOrAddSlot(key, attributeToSlot(keyAttr, memberOfRef))
const attrMemberOf = resolveParentSlot(keyAttr.attributeOf, masterTag, memberOfRef, getOrAddSlot, m)
getOrAddSlot(key, attributeToSlot(keyAttr, attrMemberOf))
} else if (key.length >= 24) {
getOrAddSlot(key, unknownToSlot(key, memberOfRef))
}
@@ -872,7 +876,9 @@ function classifyAndRegisterSlot (
const assocMemberOf = resolveParentSlot(targetClass, masterTag, memberOfRef, getOrAddSlot, m)
getOrAddSlot(id, associationToSlot(assoc, direction, assocMemberOf))
} else if (classId === normalizeId(core.class.Attribute)) {
getOrAddSlot(id, attributeToSlot(obj as Attribute<any>, memberOfRef))
const attr = obj as Attribute<any>
const attrMemberOf = resolveParentSlot(attr.attributeOf, masterTag, memberOfRef, getOrAddSlot, m)
getOrAddSlot(id, attributeToSlot(attr, attrMemberOf))
} else if (classId === normalizeId(core.class.Class) || h.isDerived(obj._class, core.class.Class)) {
getOrAddSlot(id, classToSlot(obj as any as Class<any>, memberOfRef))
} else {