[object Object]

← back to Cli Printing Press

fix(generator): rename trailing '_test' stems to avoid Go test-file exclusion (#1020) (#1021)

a03a7b81685062ec0abd6faa5fb182d021ae04d7 · 2026-05-11 00:34:24 -0600 · Juany899

safeResourceFileStem already renames stems ending in a GOOS or GOARCH token
to dodge Go's filename-based build constraints. The 'test' suffix follows the
same logic: Go treats *_test.go as a test file and excludes it from the normal
package build, so any operationId whose lowercase prefix is 'test' (e.g.,
testWebhook -> internal/cli/webhook_test.go) yields an unresolved constructor
in the parent grouper file:

  internal/cli/webhook.go:20: undefined: newWebhookTestCmd

Treat trailing 'test' the same way: rename to *_test_cmd.go. Caller surfaces
(webhook.go's AddCommand and webhook_test_cmd.go's constructor) line up.

Repro discovered while generating a CLI for Salesforce Spiff; the
testWebhook operationId is the natural name in the upstream Spiff docs.

Co-authored-by: Juan Andrés Fernández Camacho <juanandresfernandezcamacho@Juans-MacBook-Pro.local>

Files touched

Diff

commit a03a7b81685062ec0abd6faa5fb182d021ae04d7
Author: Juany899 <77075893+Juany899@users.noreply.github.com>
Date:   Mon May 11 00:34:24 2026 -0600

    fix(generator): rename trailing '_test' stems to avoid Go test-file exclusion (#1020) (#1021)
    
    safeResourceFileStem already renames stems ending in a GOOS or GOARCH token
    to dodge Go's filename-based build constraints. The 'test' suffix follows the
    same logic: Go treats *_test.go as a test file and excludes it from the normal
    package build, so any operationId whose lowercase prefix is 'test' (e.g.,
    testWebhook -> internal/cli/webhook_test.go) yields an unresolved constructor
    in the parent grouper file:
    
      internal/cli/webhook.go:20: undefined: newWebhookTestCmd
    
    Treat trailing 'test' the same way: rename to *_test_cmd.go. Caller surfaces
    (webhook.go's AddCommand and webhook_test_cmd.go's constructor) line up.
    
    Repro discovered while generating a CLI for Salesforce Spiff; the
    testWebhook operationId is the natural name in the upstream Spiff docs.
    
    Co-authored-by: Juan Andrés Fernández Camacho <juanandresfernandezcamacho@Juans-MacBook-Pro.local>
---
 internal/generator/generator.go          | 19 ++++++++++++++-----
 internal/generator/safe_filename_test.go | 24 ++++++++++++++++++++++++
 2 files changed, 38 insertions(+), 5 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 8ffa6d30..28a0885f 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -3653,22 +3653,25 @@ var goarchTokens = map[string]struct{}{
 
 // safeResourceFileStem returns a basename (without .go) safe to write under
 // internal/cli/, suffixing "_cmd" if the bare stem matches Go's filename-based
-// build-constraint pattern (*_<GOOS>.go, *_<GOARCH>.go, *_<GOOS>_<GOARCH>.go).
-// Without this rename, a file like scheduling_windows.go would get an implicit
-// Windows-only build tag and be silently excluded on macOS/Linux builds.
+// build-constraint pattern (*_<GOOS>.go, *_<GOARCH>.go, *_<GOOS>_<GOARCH>.go)
+// or Go's test-file pattern (*_test.go). Without this rename a file like
+// scheduling_windows.go would get an implicit Windows-only build tag and be
+// silently excluded on macOS/Linux builds, and a file like webhook_test.go
+// would be treated as a test file and excluded from the normal package build.
 //
 // The reserved-name collision is handled separately at spec-parse time
 // (see ReservedCLIResourceNames) because the function-name collision needs a
 // hard error rather than a silent rename — `new<Name>Cmd` would clash with
 // the reserved template's identically-named cobra builder.
 //
-// The suffix "_cmd" is never itself a GOOS or GOARCH token, so a single
-// application is sufficient.
+// The suffix "_cmd" is never itself a GOOS, GOARCH, or "test" token, so a
+// single application is sufficient.
 //
 // Examples:
 //
 //	safeResourceFileStem("scheduling_windows")     -> "scheduling_windows_cmd"
 //	safeResourceFileStem("foo_linux_amd64")        -> "foo_linux_amd64_cmd"
+//	safeResourceFileStem("webhook_test")           -> "webhook_test_cmd"
 //	safeResourceFileStem("scheduling_window_days") -> "scheduling_window_days" (no change)
 //	safeResourceFileStem("feedback")               -> "feedback" (no change; rejected at parse)
 func safeResourceFileStem(stem string) string {
@@ -3681,6 +3684,12 @@ func safeResourceFileStem(stem string) string {
 		if _, isArch := goarchTokens[last]; isArch {
 			return stem + "_cmd"
 		}
+		if last == "test" {
+			// Go treats *_test.go as a test file and excludes it from
+			// the normal package build. Suffix "_cmd" keeps the file in
+			// the normal build.
+			return stem + "_cmd"
+		}
 	}
 	if len(parts) >= 3 {
 		// Match the *_GOOS_GOARCH.go pattern (e.g., foo_linux_amd64.go).
diff --git a/internal/generator/safe_filename_test.go b/internal/generator/safe_filename_test.go
index 7122cd24..4e3b6df4 100644
--- a/internal/generator/safe_filename_test.go
+++ b/internal/generator/safe_filename_test.go
@@ -86,6 +86,30 @@ func TestSafeResourceFileStem(t *testing.T) {
 			expected: "store_arm64_endpoint",
 			why:      "'endpoint' is not a GOARCH/GOOS; only triggers on the last token",
 		},
+		{
+			name:     "trailing 'test' triggers rename",
+			stem:     "webhook_test",
+			expected: "webhook_test_cmd",
+			why:      "Go treats *_test.go as a test file and excludes it from the normal package build",
+		},
+		{
+			name:     "trailing 'test' from multi-segment stem triggers rename",
+			stem:     "connector_verify_test",
+			expected: "connector_verify_test_cmd",
+			why:      "only the trailing token matters; nested operationIds ending in 'test' also collide",
+		},
+		{
+			name:     "embedded 'test' in middle position unchanged",
+			stem:     "test_helpers_endpoint",
+			expected: "test_helpers_endpoint",
+			why:      "'test' must be the trailing token for the *_test.go rule to apply",
+		},
+		{
+			name:     "stem with 'tester' suffix unchanged",
+			stem:     "load_tester",
+			expected: "load_tester",
+			why:      "'tester' is not 'test'; only exact-suffix tokens trigger",
+		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {

← 4fac827e feat(cli): point users at where to get a token (URL + instru  ·  back to Cli Printing Press  ·  fix(cli): sync skips resources with unresolved {key} placeho 5fabad63 →