diff --git a/mdl/backend/mock/mock_page_mutator.go b/mdl/backend/mock/mock_page_mutator.go new file mode 100644 index 00000000..868a97e8 --- /dev/null +++ b/mdl/backend/mock/mock_page_mutator.go @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mock + +import ( + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +var _ backend.PageMutator = (*MockPageMutator)(nil) + +// MockPageMutator implements backend.PageMutator. Every interface method is +// backed by a public function field. If the field is nil the method returns +// zero values / nil error (never panics). +type MockPageMutator struct { + ContainerTypeFunc func() backend.ContainerKind + SetWidgetPropertyFunc func(widgetRef string, prop string, value any) error + SetWidgetDataSourceFunc func(widgetRef string, ds pages.DataSource) error + SetColumnPropertyFunc func(gridRef string, columnRef string, prop string, value any) error + InsertWidgetFunc func(widgetRef string, columnRef string, position backend.InsertPosition, widgets []pages.Widget) error + DropWidgetFunc func(refs []backend.WidgetRef) error + ReplaceWidgetFunc func(widgetRef string, columnRef string, widgets []pages.Widget) error + FindWidgetFunc func(name string) bool + AddVariableFunc func(name, dataType, defaultValue string) error + DropVariableFunc func(name string) error + SetLayoutFunc func(newLayout string, paramMappings map[string]string) error + SetPluggablePropertyFunc func(widgetRef string, propKey string, op backend.PluggablePropertyOp, ctx backend.PluggablePropertyContext) error + EnclosingEntityFunc func(widgetRef string) string + WidgetScopeFunc func() map[string]model.ID + ParamScopeFunc func() (map[string]model.ID, map[string]string) + SaveFunc func() error +} + +func (m *MockPageMutator) ContainerType() backend.ContainerKind { + if m.ContainerTypeFunc != nil { + return m.ContainerTypeFunc() + } + return backend.ContainerPage +} + +func (m *MockPageMutator) SetWidgetProperty(widgetRef string, prop string, value any) error { + if m.SetWidgetPropertyFunc != nil { + return m.SetWidgetPropertyFunc(widgetRef, prop, value) + } + return nil +} + +func (m *MockPageMutator) SetWidgetDataSource(widgetRef string, ds pages.DataSource) error { + if m.SetWidgetDataSourceFunc != nil { + return m.SetWidgetDataSourceFunc(widgetRef, ds) + } + return nil +} + +func (m *MockPageMutator) SetColumnProperty(gridRef string, columnRef string, prop string, value any) error { + if m.SetColumnPropertyFunc != nil { + return m.SetColumnPropertyFunc(gridRef, columnRef, prop, value) + } + return nil +} + +func (m *MockPageMutator) InsertWidget(widgetRef string, columnRef string, position backend.InsertPosition, widgets []pages.Widget) error { + if m.InsertWidgetFunc != nil { + return m.InsertWidgetFunc(widgetRef, columnRef, position, widgets) + } + return nil +} + +func (m *MockPageMutator) DropWidget(refs []backend.WidgetRef) error { + if m.DropWidgetFunc != nil { + return m.DropWidgetFunc(refs) + } + return nil +} + +func (m *MockPageMutator) ReplaceWidget(widgetRef string, columnRef string, widgets []pages.Widget) error { + if m.ReplaceWidgetFunc != nil { + return m.ReplaceWidgetFunc(widgetRef, columnRef, widgets) + } + return nil +} + +func (m *MockPageMutator) FindWidget(name string) bool { + if m.FindWidgetFunc != nil { + return m.FindWidgetFunc(name) + } + return false +} + +func (m *MockPageMutator) AddVariable(name, dataType, defaultValue string) error { + if m.AddVariableFunc != nil { + return m.AddVariableFunc(name, dataType, defaultValue) + } + return nil +} + +func (m *MockPageMutator) DropVariable(name string) error { + if m.DropVariableFunc != nil { + return m.DropVariableFunc(name) + } + return nil +} + +func (m *MockPageMutator) SetLayout(newLayout string, paramMappings map[string]string) error { + if m.SetLayoutFunc != nil { + return m.SetLayoutFunc(newLayout, paramMappings) + } + return nil +} + +func (m *MockPageMutator) SetPluggableProperty(widgetRef string, propKey string, op backend.PluggablePropertyOp, ctx backend.PluggablePropertyContext) error { + if m.SetPluggablePropertyFunc != nil { + return m.SetPluggablePropertyFunc(widgetRef, propKey, op, ctx) + } + return nil +} + +func (m *MockPageMutator) EnclosingEntity(widgetRef string) string { + if m.EnclosingEntityFunc != nil { + return m.EnclosingEntityFunc(widgetRef) + } + return "" +} + +func (m *MockPageMutator) WidgetScope() map[string]model.ID { + if m.WidgetScopeFunc != nil { + return m.WidgetScopeFunc() + } + return nil +} + +func (m *MockPageMutator) ParamScope() (map[string]model.ID, map[string]string) { + if m.ParamScopeFunc != nil { + return m.ParamScopeFunc() + } + return nil, nil +} + +func (m *MockPageMutator) Save() error { + if m.SaveFunc != nil { + return m.SaveFunc() + } + return nil +} diff --git a/mdl/backend/mock/mock_workflow_mutator.go b/mdl/backend/mock/mock_workflow_mutator.go new file mode 100644 index 00000000..b184953b --- /dev/null +++ b/mdl/backend/mock/mock_workflow_mutator.go @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mock + +import ( + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/sdk/workflows" +) + +var _ backend.WorkflowMutator = (*MockWorkflowMutator)(nil) + +// MockWorkflowMutator implements backend.WorkflowMutator. Every interface +// method is backed by a public function field. If the field is nil the +// method returns zero values / nil error (never panics). +type MockWorkflowMutator struct { + SetPropertyFunc func(prop string, value string) error + SetPropertyWithEntityFunc func(prop string, value string, entity string) error + SetActivityPropertyFunc func(activityRef string, atPos int, prop string, value string) error + InsertAfterActivityFunc func(activityRef string, atPos int, activities []workflows.WorkflowActivity) error + DropActivityFunc func(activityRef string, atPos int) error + ReplaceActivityFunc func(activityRef string, atPos int, activities []workflows.WorkflowActivity) error + InsertOutcomeFunc func(activityRef string, atPos int, outcomeName string, activities []workflows.WorkflowActivity) error + DropOutcomeFunc func(activityRef string, atPos int, outcomeName string) error + InsertPathFunc func(activityRef string, atPos int, pathCaption string, activities []workflows.WorkflowActivity) error + DropPathFunc func(activityRef string, atPos int, pathCaption string) error + InsertBranchFunc func(activityRef string, atPos int, condition string, activities []workflows.WorkflowActivity) error + DropBranchFunc func(activityRef string, atPos int, branchName string) error + InsertBoundaryEventFunc func(activityRef string, atPos int, eventType string, delay string, activities []workflows.WorkflowActivity) error + DropBoundaryEventFunc func(activityRef string, atPos int) error + SaveFunc func() error +} + +func (m *MockWorkflowMutator) SetProperty(prop string, value string) error { + if m.SetPropertyFunc != nil { + return m.SetPropertyFunc(prop, value) + } + return nil +} + +func (m *MockWorkflowMutator) SetPropertyWithEntity(prop string, value string, entity string) error { + if m.SetPropertyWithEntityFunc != nil { + return m.SetPropertyWithEntityFunc(prop, value, entity) + } + return nil +} + +func (m *MockWorkflowMutator) SetActivityProperty(activityRef string, atPos int, prop string, value string) error { + if m.SetActivityPropertyFunc != nil { + return m.SetActivityPropertyFunc(activityRef, atPos, prop, value) + } + return nil +} + +func (m *MockWorkflowMutator) InsertAfterActivity(activityRef string, atPos int, activities []workflows.WorkflowActivity) error { + if m.InsertAfterActivityFunc != nil { + return m.InsertAfterActivityFunc(activityRef, atPos, activities) + } + return nil +} + +func (m *MockWorkflowMutator) DropActivity(activityRef string, atPos int) error { + if m.DropActivityFunc != nil { + return m.DropActivityFunc(activityRef, atPos) + } + return nil +} + +func (m *MockWorkflowMutator) ReplaceActivity(activityRef string, atPos int, activities []workflows.WorkflowActivity) error { + if m.ReplaceActivityFunc != nil { + return m.ReplaceActivityFunc(activityRef, atPos, activities) + } + return nil +} + +func (m *MockWorkflowMutator) InsertOutcome(activityRef string, atPos int, outcomeName string, activities []workflows.WorkflowActivity) error { + if m.InsertOutcomeFunc != nil { + return m.InsertOutcomeFunc(activityRef, atPos, outcomeName, activities) + } + return nil +} + +func (m *MockWorkflowMutator) DropOutcome(activityRef string, atPos int, outcomeName string) error { + if m.DropOutcomeFunc != nil { + return m.DropOutcomeFunc(activityRef, atPos, outcomeName) + } + return nil +} + +func (m *MockWorkflowMutator) InsertPath(activityRef string, atPos int, pathCaption string, activities []workflows.WorkflowActivity) error { + if m.InsertPathFunc != nil { + return m.InsertPathFunc(activityRef, atPos, pathCaption, activities) + } + return nil +} + +func (m *MockWorkflowMutator) DropPath(activityRef string, atPos int, pathCaption string) error { + if m.DropPathFunc != nil { + return m.DropPathFunc(activityRef, atPos, pathCaption) + } + return nil +} + +func (m *MockWorkflowMutator) InsertBranch(activityRef string, atPos int, condition string, activities []workflows.WorkflowActivity) error { + if m.InsertBranchFunc != nil { + return m.InsertBranchFunc(activityRef, atPos, condition, activities) + } + return nil +} + +func (m *MockWorkflowMutator) DropBranch(activityRef string, atPos int, branchName string) error { + if m.DropBranchFunc != nil { + return m.DropBranchFunc(activityRef, atPos, branchName) + } + return nil +} + +func (m *MockWorkflowMutator) InsertBoundaryEvent(activityRef string, atPos int, eventType string, delay string, activities []workflows.WorkflowActivity) error { + if m.InsertBoundaryEventFunc != nil { + return m.InsertBoundaryEventFunc(activityRef, atPos, eventType, delay, activities) + } + return nil +} + +func (m *MockWorkflowMutator) DropBoundaryEvent(activityRef string, atPos int) error { + if m.DropBoundaryEventFunc != nil { + return m.DropBoundaryEventFunc(activityRef, atPos) + } + return nil +} + +func (m *MockWorkflowMutator) Save() error { + if m.SaveFunc != nil { + return m.SaveFunc() + } + return nil +} diff --git a/mdl/backend/mpr/backend.go b/mdl/backend/mpr/backend.go index da20193b..48e5426b 100644 --- a/mdl/backend/mpr/backend.go +++ b/mdl/backend/mpr/backend.go @@ -85,9 +85,11 @@ func (b *MprBackend) Path() string { return b.path } // for new code. func (b *MprBackend) MprReader() *mpr.Reader { return b.reader } -func (b *MprBackend) Version() types.MPRVersion { return convertMPRVersion(b.reader.Version()) } -func (b *MprBackend) ProjectVersion() *types.ProjectVersion { return convertProjectVersion(b.reader.ProjectVersion()) } -func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() } +func (b *MprBackend) Version() types.MPRVersion { return convertMPRVersion(b.reader.Version()) } +func (b *MprBackend) ProjectVersion() *types.ProjectVersion { + return convertProjectVersion(b.reader.ProjectVersion()) +} +func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() } // Commit is a no-op — the MPR writer auto-commits on each write operation. func (b *MprBackend) Commit() error { return nil } @@ -112,7 +114,9 @@ func (b *MprBackend) DeleteModuleWithCleanup(id model.ID, moduleName string) err // FolderBackend // --------------------------------------------------------------------------- -func (b *MprBackend) ListFolders() ([]*types.FolderInfo, error) { return convertFolderInfoSlice(b.reader.ListFolders()) } +func (b *MprBackend) ListFolders() ([]*types.FolderInfo, error) { + return convertFolderInfoSlice(b.reader.ListFolders()) +} func (b *MprBackend) CreateFolder(folder *model.Folder) error { return b.writer.CreateFolder(folder) } func (b *MprBackend) DeleteFolder(id model.ID) error { return b.writer.DeleteFolder(id) } func (b *MprBackend) MoveFolder(id model.ID, newContainerID model.ID) error { @@ -678,8 +682,10 @@ func (b *MprBackend) UpdateRawUnit(unitID string, contents []byte) error { // MetadataBackend // --------------------------------------------------------------------------- -func (b *MprBackend) ListAllUnitIDs() ([]string, error) { return b.reader.ListAllUnitIDs() } -func (b *MprBackend) ListUnits() ([]*types.UnitInfo, error) { return convertUnitInfoSlice(b.reader.ListUnits()) } +func (b *MprBackend) ListAllUnitIDs() ([]string, error) { return b.reader.ListAllUnitIDs() } +func (b *MprBackend) ListUnits() ([]*types.UnitInfo, error) { + return convertUnitInfoSlice(b.reader.ListUnits()) +} func (b *MprBackend) GetUnitTypes() (map[string]int, error) { return b.reader.GetUnitTypes() } func (b *MprBackend) GetProjectRootID() (string, error) { return b.reader.GetProjectRootID() } func (b *MprBackend) ContentsDir() string { return b.reader.ContentsDir() } diff --git a/mdl/backend/mpr/convert_roundtrip_test.go b/mdl/backend/mpr/convert_roundtrip_test.go index a10391fc..157572f0 100644 --- a/mdl/backend/mpr/convert_roundtrip_test.go +++ b/mdl/backend/mpr/convert_roundtrip_test.go @@ -648,4 +648,3 @@ func TestFieldCountDrift(t *testing.T) { assertFieldCount(t, "mpr.EntityAccessRevocation", mpr.EntityAccessRevocation{}, 6) assertFieldCount(t, "types.EntityAccessRevocation", types.EntityAccessRevocation{}, 6) } - diff --git a/mdl/backend/mpr/workflow_mutator.go b/mdl/backend/mpr/workflow_mutator.go index 01c00238..5a1b78ba 100644 --- a/mdl/backend/mpr/workflow_mutator.go +++ b/mdl/backend/mpr/workflow_mutator.go @@ -162,14 +162,19 @@ func (m *mprWorkflowMutator) SetActivityProperty(activityRef string, atPos int, case "PAGE": taskPage := dGetDoc(actDoc, "TaskPage") if taskPage != nil { + // TaskPage exists and has a value — update the Page field in place. dSet(taskPage, "Page", value) - } else { - pageRef := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$PageReference"}, - {Key: "Page", Value: value}, - } - dSet(actDoc, "TaskPage", pageRef) + return nil + } + pageRef := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$PageReference"}, + {Key: "Page", Value: value}, + } + if !dSet(actDoc, "TaskPage", pageRef) { + // TaskPage key absent — append to activity and replace in BSON tree. + actDoc = append(actDoc, bson.E{Key: "TaskPage", Value: pageRef}) + m.replaceActivity(actDoc) } return nil @@ -555,6 +560,42 @@ func (m *mprWorkflowMutator) Save() error { // Internal helpers — activity search // --------------------------------------------------------------------------- +// replaceActivity replaces an activity document in the workflow's BSON tree +// by matching on $ID. This is needed when appending new keys to an activity +// document, because the slice header returned by findActivityByCaption cannot +// propagate appends back to the parent bson.A. +func (m *mprWorkflowMutator) replaceActivity(updated bson.D) { + actID := extractBinaryIDFromDoc(dGet(updated, "$ID")) + if actID == "" { + return + } + flow := dGetDoc(m.rawData, "Flow") + if flow == nil { + return + } + replaceActivityRecursive(flow, actID, updated) +} + +func replaceActivityRecursive(flow bson.D, actID string, updated bson.D) bool { + elements := dGetArrayElements(dGet(flow, "Activities")) + for i, elem := range elements { + actDoc, ok := elem.(bson.D) + if !ok { + continue + } + if extractBinaryIDFromDoc(dGet(actDoc, "$ID")) == actID { + elements[i] = updated + return true + } + for _, nestedFlow := range getNestedFlows(actDoc) { + if replaceActivityRecursive(nestedFlow, actID, updated) { + return true + } + } + } + return false +} + // findActivityByCaption searches the workflow for an activity matching caption. func (m *mprWorkflowMutator) findActivityByCaption(caption string, atPosition int) (bson.D, error) { flow := dGetDoc(m.rawData, "Flow") diff --git a/mdl/backend/mpr/workflow_mutator_test.go b/mdl/backend/mpr/workflow_mutator_test.go index d821614f..6ffec5e0 100644 --- a/mdl/backend/mpr/workflow_mutator_test.go +++ b/mdl/backend/mpr/workflow_mutator_test.go @@ -1136,8 +1136,7 @@ func TestWorkflowMutator_InsertBoundaryEvent_NoDelay(t *testing.T) { // --------------------------------------------------------------------------- func TestWorkflowMutator_SetActivityProperty_Page_New(t *testing.T) { - // Note: When TaskPage key doesn't pre-exist in BSON, dSet silently fails. - // The key must be present (even as nil) for PAGE to work on a new activity. + // TaskPage key present with nil value — should be replaced with a new PageReference. act := makeWfActivity("Workflows$UserTask", "Review", "task1") act = append(act, bson.E{Key: "TaskPage", Value: nil}) m := newMutator(makeWorkflowDoc(act)) @@ -1157,21 +1156,79 @@ func TestWorkflowMutator_SetActivityProperty_Page_New(t *testing.T) { } func TestWorkflowMutator_SetActivityProperty_Page_MissingKey(t *testing.T) { - // BUG: dSet silently fails when TaskPage key is absent — pageRef is lost. + // Regression test: dSet silently failed when TaskPage key was absent. + // Fixed by appending the key to the activity and replacing it in the BSON tree. act := makeWfActivity("Workflows$UserTask", "Review", "task1") // No TaskPage field at all m := newMutator(makeWorkflowDoc(act)) - // No error returned, but the set is silently lost if err := m.SetActivityProperty("Review", 0, "PAGE", "MyModule.TaskPage"); err != nil { t.Fatalf("SetActivityProperty PAGE failed: %v", err) } actDoc, _ := m.findActivityByCaption("Review", 0) taskPage := dGetDoc(actDoc, "TaskPage") - // This documents the bug: TaskPage is nil because dSet can't create new keys - if taskPage != nil { - t.Log("BUG FIXED: TaskPage is now set even when key was absent") + if taskPage == nil { + t.Fatal("TaskPage should be set even when key was absent") + } + if got := dGetString(taskPage, "Page"); got != "MyModule.TaskPage" { + t.Errorf("Page = %q, want MyModule.TaskPage", got) + } +} + +func TestWorkflowMutator_SetActivityProperty_Page_MissingKey_NestedSubFlow(t *testing.T) { + // Exercises the recursive replaceActivity path: the target activity lives + // inside an outcome's sub-flow, not at the top level. + // Use distinct $IDs so replaceActivity cannot accidentally match the parent. + parentID := primitive.Binary{Subtype: 0x04, Data: []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}} + nestedID := primitive.Binary{Subtype: 0x04, Data: []byte{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}} + + nestedAct := bson.D{ + {Key: "$ID", Value: nestedID}, + {Key: "$Type", Value: "Workflows$UserTask"}, + {Key: "Caption", Value: "NestedReview"}, + {Key: "Name", Value: "nested1"}, + } + // No TaskPage field at all on the nested activity. + + outcome := bson.D{ + {Key: "$ID", Value: primitive.Binary{Subtype: 0x04, Data: make([]byte, 16)}}, + {Key: "$Type", Value: "Workflows$BooleanOutcome"}, + {Key: "Flow", Value: bson.D{ + {Key: "$ID", Value: primitive.Binary{Subtype: 0x04, Data: make([]byte, 16)}}, + {Key: "$Type", Value: "Workflows$Flow"}, + {Key: "Activities", Value: bson.A{int32(3), nestedAct}}, + }}, + } + parentAct := bson.D{ + {Key: "$ID", Value: parentID}, + {Key: "$Type", Value: "Workflows$Decision"}, + {Key: "Caption", Value: "Check"}, + {Key: "Name", Value: "decision1"}, + {Key: "Outcomes", Value: bson.A{int32(3), outcome}}, + } + m := newMutator(makeWorkflowDoc(parentAct)) + + if err := m.SetActivityProperty("NestedReview", 0, "PAGE", "MyModule.NestedPage"); err != nil { + t.Fatalf("SetActivityProperty PAGE on nested activity failed: %v", err) + } + + actDoc, _ := m.findActivityByCaption("NestedReview", 0) + taskPage := dGetDoc(actDoc, "TaskPage") + if taskPage == nil { + t.Fatal("TaskPage should be set on nested activity even when key was absent") + } + if got := dGetString(taskPage, "Page"); got != "MyModule.NestedPage" { + t.Errorf("Page = %q, want MyModule.NestedPage", got) + } + + // Verify parent decision still has its Outcomes intact. + parentDoc, _ := m.findActivityByCaption("Check", 0) + if parentDoc == nil { + t.Fatal("parent decision activity should still exist") + } + if outcomes := dGet(parentDoc, "Outcomes"); outcomes == nil { + t.Fatal("parent decision Outcomes should still be present") } } diff --git a/mdl/executor/registry_test.go b/mdl/executor/registry_test.go index 4641412f..932e441b 100644 --- a/mdl/executor/registry_test.go +++ b/mdl/executor/registry_test.go @@ -172,9 +172,11 @@ func allKnownStatements() []ast.Statement { &ast.AlterUserRoleStmt{}, &ast.AlterWorkflowStmt{}, &ast.ConnectStmt{}, + &ast.CreateAgentStmt{}, &ast.CreateAssociationStmt{}, &ast.CreateBusinessEventServiceStmt{}, &ast.CreateConfigurationStmt{}, + &ast.CreateConsumedMCPServiceStmt{}, &ast.CreateConstantStmt{}, &ast.CreateDatabaseConnectionStmt{}, &ast.CreateDataTransformerStmt{}, @@ -188,7 +190,9 @@ func allKnownStatements() []ast.Statement { &ast.CreateImportMappingStmt{}, &ast.CreateJavaActionStmt{}, &ast.CreateJsonStructureStmt{}, + &ast.CreateKnowledgeBaseStmt{}, &ast.CreateMicroflowStmt{}, + &ast.CreateModelStmt{}, &ast.CreateModuleRoleStmt{}, &ast.CreateModuleStmt{}, &ast.CreateODataClientStmt{}, @@ -206,9 +210,11 @@ func allKnownStatements() []ast.Statement { &ast.DescribeStmt{}, &ast.DescribeStylingStmt{}, &ast.DisconnectStmt{}, + &ast.DropAgentStmt{}, &ast.DropAssociationStmt{}, &ast.DropBusinessEventServiceStmt{}, &ast.DropConfigurationStmt{}, + &ast.DropConsumedMCPServiceStmt{}, &ast.DropConstantStmt{}, &ast.DropDataTransformerStmt{}, &ast.DropDemoUserStmt{}, @@ -220,7 +226,9 @@ func allKnownStatements() []ast.Statement { &ast.DropImportMappingStmt{}, &ast.DropJavaActionStmt{}, &ast.DropJsonStructureStmt{}, + &ast.DropKnowledgeBaseStmt{}, &ast.DropMicroflowStmt{}, + &ast.DropModelStmt{}, &ast.DropModuleRoleStmt{}, &ast.DropModuleStmt{}, &ast.DropODataClientStmt{}, @@ -286,3 +294,15 @@ func TestNewRegistry_Completeness(t *testing.T) { t.Fatalf("registry is incomplete: %v", err) } } + +// TestNewRegistry_HandlerCountSnapshot verifies that the number of registered +// handlers matches allKnownStatements(). Keep allKnownStatements() in sync with +// known statement types and handler registrations. +func TestNewRegistry_HandlerCountSnapshot(t *testing.T) { + r := NewRegistry() + known := allKnownStatements() + + if got := r.HandlerCount(); got != len(known) { + t.Errorf("handler count mismatch: registry has %d, allKnownStatements has %d — update allKnownStatements or register missing handlers", got, len(known)) + } +} diff --git a/mdl/executor/widget_registry.go b/mdl/executor/widget_registry.go index 71192c50..f65e9b8d 100644 --- a/mdl/executor/widget_registry.go +++ b/mdl/executor/widget_registry.go @@ -16,8 +16,8 @@ import ( // WidgetRegistry holds loaded widget definitions keyed by uppercase MDL name. type WidgetRegistry struct { - byMDLName map[string]*WidgetDefinition // keyed by uppercase MDLName - byWidgetID map[string]*WidgetDefinition // keyed by widgetId + byMDLName map[string]*WidgetDefinition // keyed by uppercase MDLName + byWidgetID map[string]*WidgetDefinition // keyed by widgetId knownOperations map[string]bool // operations accepted during validation } diff --git a/mdl/types/edmx_test.go b/mdl/types/edmx_test.go index a4159e93..01d2b374 100644 --- a/mdl/types/edmx_test.go +++ b/mdl/types/edmx_test.go @@ -217,9 +217,9 @@ func TestFindEntityType(t *testing.T) { func TestResolveNavType(t *testing.T) { tests := []struct { - input string - typeName string - isMany bool + input string + typeName string + isMany bool }{ {"Collection(NS.Order)", "Order", true}, {"NS.Customer", "Customer", false}, diff --git a/mdl/types/id_test.go b/mdl/types/id_test.go index d30a5e39..aa138154 100644 --- a/mdl/types/id_test.go +++ b/mdl/types/id_test.go @@ -156,7 +156,7 @@ func TestValidateID(t *testing.T) { {"AABBCCDD-EEFF-1122-3344-556677889900", true}, {"", false}, {"too-short", false}, - {"a1b2c3d4-e5f6-7890-abcd-ef123456789", false}, // 35 chars + {"a1b2c3d4-e5f6-7890-abcd-ef123456789", false}, // 35 chars {"a1b2c3d4-e5f6-7890-abcd-ef12345678901", false}, // 37 chars {"a1b2c3d4xe5f6-7890-abcd-ef1234567890", false}, // wrong separator {"g1b2c3d4-e5f6-7890-abcd-ef1234567890", false}, // invalid hex