Skip to content

Commit 42ab5db

Browse files
engalarclaude
authored andcommitted
fix: share topic aliases between CLI and REPL, add tests and missing help topic
- Move topicAliases to syntax package so HELP <topic> works in REPL (was broken: HELP entity returned "No syntax help found") - Add duplicate path detection in Register() (panic on duplicate) - Register missing 'test' help topic (replaced deleted test.txt) - Fix capitalize() for UTF-8 safety - Add unit tests for resolveHelpPath and ResolveAlias - Add TestAliasTargetsExist to verify all aliases point to valid paths - Add doctype-test for HELP command Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e47e62c commit 42ab5db

8 files changed

Lines changed: 210 additions & 62 deletions

File tree

cmd/mxcli/help.go

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -11,63 +11,6 @@ import (
1111
"github.com/spf13/cobra"
1212
)
1313

14-
// topicAliases maps legacy topic names and plurals to registry paths.
15-
var topicAliases = map[string]string{
16-
// Domain model aliases
17-
"keywords": "domain-model.keywords",
18-
"reserved": "domain-model.keywords",
19-
"types": "domain-model.types",
20-
"datatypes": "domain-model.types",
21-
"data-types": "domain-model.types",
22-
"delete": "domain-model.association.delete-behavior",
23-
"delete_behavior": "domain-model.association.delete-behavior",
24-
"delete-behavior": "domain-model.association.delete-behavior",
25-
"entity": "domain-model.entity",
26-
"entities": "domain-model.entity",
27-
"enumeration": "domain-model.enumeration",
28-
"enum": "domain-model.enumeration",
29-
"enumerations": "domain-model.enumeration",
30-
"constant": "domain-model.constant",
31-
"constants": "domain-model.constant",
32-
"association": "domain-model.association",
33-
"associations": "domain-model.association",
34-
// Plural aliases
35-
"microflows": "microflow",
36-
"pages": "page",
37-
"snippets": "snippet",
38-
"fragments": "fragment",
39-
"workflows": "workflow",
40-
// Variant aliases
41-
"nav": "navigation",
42-
"project-settings": "settings",
43-
"rest-client": "rest",
44-
"rest-clients": "rest",
45-
"integrations": "integration",
46-
"services": "integration",
47-
"contract": "integration",
48-
"contracts": "integration",
49-
"javaaction": "java-action",
50-
"java_action": "java-action",
51-
"java-actions": "java-action",
52-
"javaactions": "java-action",
53-
"businessevents": "business-events",
54-
"business_events": "business-events",
55-
"be": "business-events",
56-
"xpath-constraints": "xpath",
57-
"external-sql": "sql",
58-
"validation": "errors",
59-
// Agents aliases
60-
"agent": "agents",
61-
"agent-editor": "agents",
62-
"agenteditor": "agents",
63-
"model": "agents.model",
64-
"models": "agents.model",
65-
"knowledge-base": "agents.knowledge-base",
66-
"knowledgebase": "agents.knowledge-base",
67-
"mcp": "agents.mcp-service",
68-
"mcp-service": "agents.mcp-service",
69-
}
70-
7114
var syntaxCmd = &cobra.Command{
7215
Use: "syntax [topic [subtopic...]]",
7316
Short: "Show MDL syntax reference",
@@ -115,9 +58,7 @@ Examples:
11558
path := strings.ToLower(strings.Join(args, "."))
11659

11760
// Apply aliases
118-
if alias, ok := topicAliases[path]; ok {
119-
path = alias
120-
}
61+
path = syntax.ResolveAlias(path)
12162

12263
// Query registry
12364
if syntax.HasPrefix(path) {

cmd/mxcli/syntax/features_misc.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,46 @@ SEARCH '"exact phrase"';
211211
SEARCH 'word*';`,
212212
})
213213

214+
// ── Testing ────────────────────────────────────────────────────────
215+
216+
Register(SyntaxFeature{
217+
Path: "test",
218+
Summary: "Microflow testing — run .test.mdl or .test.md files against a Mendix project in Docker",
219+
Keywords: []string{
220+
"test", "testing", "microflow test", "nanoflow test",
221+
"test.mdl", "test.md", "junit", "docker",
222+
"@test", "@expect", "@throws", "@cleanup",
223+
},
224+
Syntax: `mxcli test <file|dir> -p app.mpr [flags]
225+
226+
Flags:
227+
-l, --list List tests without executing
228+
-j, --junit FILE Write JUnit XML results
229+
-s, --skip-build Skip Docker build (reuse existing)
230+
-v, --verbose Show runtime log lines
231+
-t, --timeout DUR Runtime startup timeout (default: 5m)
232+
233+
Annotations:
234+
@test <name> Test name (required)
235+
@expect $var = value Assert variable equals value
236+
@expect $obj/Attr = val Assert entity attribute
237+
@throws 'message' Expect error
238+
@cleanup rollback|none Cleanup strategy (default: rollback)`,
239+
Example: `-- .test.mdl file format
240+
/**
241+
* @test String concatenation
242+
* @expect $result = 'John Doe'
243+
*/
244+
$result = CALL MICROFLOW MyModule.ConcatNames(
245+
FirstName = 'John', LastName = 'Doe'
246+
);
247+
/
248+
249+
-- Run tests
250+
mxcli test tests/ -p app.mpr
251+
mxcli test tests/ -p app.mpr --junit results.xml`,
252+
})
253+
214254
// ── Errors ──────────────────────────────────────────────────────────
215255

216256
Register(SyntaxFeature{

cmd/mxcli/syntax/format.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ func capitalize(s string) string {
8383
if s == "" {
8484
return s
8585
}
86-
return strings.ToUpper(s[:1]) + s[1:]
86+
r := []rune(s)
87+
return strings.ToUpper(string(r[:1])) + string(r[1:])
8788
}
8889

8990
type featureGroup struct {

cmd/mxcli/syntax/registry.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,86 @@ type SyntaxFeature struct {
1919
}
2020

2121
var registry []SyntaxFeature
22+
var registeredPaths = map[string]bool{}
23+
24+
// topicAliases maps legacy topic names and common variants to registry paths.
25+
var topicAliases = map[string]string{
26+
// Domain model aliases
27+
"keywords": "domain-model.keywords",
28+
"reserved": "domain-model.keywords",
29+
"types": "domain-model.types",
30+
"datatypes": "domain-model.types",
31+
"data-types": "domain-model.types",
32+
"delete": "domain-model.association.delete-behavior",
33+
"delete_behavior": "domain-model.association.delete-behavior",
34+
"delete-behavior": "domain-model.association.delete-behavior",
35+
"entity": "domain-model.entity",
36+
"entities": "domain-model.entity",
37+
"enumeration": "domain-model.enumeration",
38+
"enum": "domain-model.enumeration",
39+
"enumerations": "domain-model.enumeration",
40+
"constant": "domain-model.constant",
41+
"constants": "domain-model.constant",
42+
"association": "domain-model.association",
43+
"associations": "domain-model.association",
44+
// Plural aliases
45+
"microflows": "microflow",
46+
"pages": "page",
47+
"snippets": "snippet",
48+
"fragments": "fragment",
49+
"workflows": "workflow",
50+
// Variant aliases
51+
"nav": "navigation",
52+
"project-settings": "settings",
53+
"rest-client": "rest",
54+
"rest-clients": "rest",
55+
"integrations": "integration",
56+
"services": "integration",
57+
"contract": "integration",
58+
"contracts": "integration",
59+
"javaaction": "java-action",
60+
"java_action": "java-action",
61+
"java-actions": "java-action",
62+
"javaactions": "java-action",
63+
"businessevents": "business-events",
64+
"business_events": "business-events",
65+
"be": "business-events",
66+
"xpath-constraints": "xpath",
67+
"external-sql": "sql",
68+
"validation": "errors",
69+
"testing": "test",
70+
"tests": "test",
71+
// Agents aliases
72+
"agent": "agents",
73+
"agent-editor": "agents",
74+
"agenteditor": "agents",
75+
"model": "agents.model",
76+
"models": "agents.model",
77+
"knowledge-base": "agents.knowledge-base",
78+
"knowledgebase": "agents.knowledge-base",
79+
"mcp": "agents.mcp-service",
80+
"mcp-service": "agents.mcp-service",
81+
}
2282

2383
// Register adds a syntax feature to the global registry.
84+
// Panics if a feature with the same path is already registered.
2485
func Register(f SyntaxFeature) {
86+
if registeredPaths[f.Path] {
87+
panic("syntax: duplicate feature path: " + f.Path)
88+
}
89+
registeredPaths[f.Path] = true
2590
registry = append(registry, f)
2691
}
2792

93+
// ResolveAlias returns the canonical registry path for a topic alias.
94+
// If the input is not an alias, it is returned unchanged.
95+
func ResolveAlias(path string) string {
96+
if alias, ok := topicAliases[path]; ok {
97+
return alias
98+
}
99+
return path
100+
}
101+
28102
// All returns every registered feature, sorted by path.
29103
func All() []SyntaxFeature {
30104
out := make([]SyntaxFeature, len(registry))

cmd/mxcli/syntax/registry_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,48 @@ func TestWriteText_MultipleFeatures(t *testing.T) {
158158
}
159159
}
160160

161+
func TestResolveAlias(t *testing.T) {
162+
tests := []struct {
163+
input string
164+
want string
165+
}{
166+
{"entity", "domain-model.entity"},
167+
{"entities", "domain-model.entity"},
168+
{"enum", "domain-model.enumeration"},
169+
{"association", "domain-model.association"},
170+
{"nav", "navigation"},
171+
{"be", "business-events"},
172+
{"validation", "errors"},
173+
{"testing", "test"},
174+
{"tests", "test"},
175+
// Non-alias passes through unchanged
176+
{"workflow", "workflow"},
177+
{"security", "security"},
178+
{"nonexistent", "nonexistent"},
179+
}
180+
for _, tt := range tests {
181+
t.Run(tt.input, func(t *testing.T) {
182+
got := ResolveAlias(tt.input)
183+
if got != tt.want {
184+
t.Errorf("ResolveAlias(%q) = %q, want %q", tt.input, got, tt.want)
185+
}
186+
})
187+
}
188+
}
189+
190+
func TestAliasTargetsExist(t *testing.T) {
191+
all := All()
192+
pathSet := make(map[string]bool, len(all))
193+
for _, f := range all {
194+
pathSet[f.Path] = true
195+
}
196+
for alias, target := range topicAliases {
197+
if !HasPrefix(target) {
198+
t.Errorf("alias %q -> %q: target path has no matching features", alias, target)
199+
}
200+
}
201+
}
202+
161203
func TestSeeAlsoRefsExist(t *testing.T) {
162204
all := All()
163205
pathSet := make(map[string]bool, len(all))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- Help command examples (syntax-only, no project needed)
2+
3+
-- Bare HELP shows overview
4+
HELP;
5+
6+
-- HELP with single topic
7+
HELP workflow;
8+
9+
-- HELP with multi-word topic
10+
HELP user task;
11+
12+
-- HELP with nested path
13+
HELP workflow user task targeting;
14+
15+
-- HELP with alias (entity -> domain-model.entity)
16+
HELP entity;
17+
18+
-- HELP with unknown topic (should print "No syntax help found")
19+
HELP nonexistenttopic;

mdl/executor/cmd_misc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func execSet(ctx *ExecContext, s *ast.SetStmt) error {
5454
// execHelp handles HELP statements. With topic words, queries the syntax registry.
5555
func execHelp(ctx *ExecContext, s *ast.HelpStmt) error {
5656
if len(s.Topic) > 0 {
57-
path := resolveHelpPath(s.Topic)
57+
path := syntax.ResolveAlias(resolveHelpPath(s.Topic))
5858
features := syntax.ByPrefix(path)
5959
if len(features) == 0 {
6060
fmt.Fprintf(ctx.Output, "No syntax help found for: %s\n", path)

mdl/executor/cmd_misc_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import "testing"
6+
7+
func TestResolveHelpPath(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
words []string
11+
want string
12+
}{
13+
{"single word exact", []string{"workflow"}, "workflow"},
14+
{"single word domain-model", []string{"domain-model"}, "domain-model"},
15+
{"multi-word no match falls through", []string{"user", "task"}, "user.task"},
16+
{"multi-level path", []string{"workflow", "user", "task"}, "workflow.user-task"},
17+
{"three-level path", []string{"workflow", "user", "task", "targeting"}, "workflow.user-task.targeting"},
18+
{"security prefix", []string{"security", "entity", "access"}, "security.entity-access"},
19+
{"non-existent topic", []string{"nonexistent"}, "nonexistent"},
20+
{"mixed known and unknown", []string{"workflow", "bogus"}, "workflow.bogus"},
21+
{"single known nested", []string{"navigation", "create"}, "navigation.create"},
22+
}
23+
for _, tt := range tests {
24+
t.Run(tt.name, func(t *testing.T) {
25+
got := resolveHelpPath(tt.words)
26+
if got != tt.want {
27+
t.Errorf("resolveHelpPath(%v) = %q, want %q", tt.words, got, tt.want)
28+
}
29+
})
30+
}
31+
}

0 commit comments

Comments
 (0)