← back to Cli Printing Press
fix(cli): harden destructive auth dogfood skips (#628)
5004e4b0788dad5975eb07822417f06ca1e62957 · 2026-05-05 12:25:29 -0700 · Trevin Chow
Files touched
M internal/cli/dogfood.goM internal/generator/generator.goM internal/generator/generator_test.goM internal/generator/templates/auth_browser.go.tmplM internal/generator/templates/command_endpoint.go.tmplM internal/generator/templates/command_promoted.go.tmplM internal/pipeline/live_dogfood.goM internal/pipeline/live_dogfood_destructive_test.goM internal/pipeline/live_dogfood_test.goM internal/pipeline/mcp_size.goM internal/pipeline/mcpsync/sync.goM internal/pipeline/mcpsync/sync_test.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
Diff
commit 5004e4b0788dad5975eb07822417f06ca1e62957
Author: Trevin Chow <trevin@trevinchow.com>
Date: Tue May 5 12:25:29 2026 -0700
fix(cli): harden destructive auth dogfood skips (#628)
---
internal/cli/dogfood.go | 2 +-
internal/generator/generator.go | 5 +
internal/generator/generator_test.go | 36 +++++
internal/generator/templates/auth_browser.go.tmpl | 9 +-
.../generator/templates/command_endpoint.go.tmpl | 2 +-
.../generator/templates/command_promoted.go.tmpl | 2 +-
internal/pipeline/live_dogfood.go | 80 +++++++++--
internal/pipeline/live_dogfood_destructive_test.go | 79 +++++++++++
internal/pipeline/live_dogfood_test.go | 152 +++++++++++++++++++++
internal/pipeline/mcp_size.go | 1 +
internal/pipeline/mcpsync/sync.go | 15 +-
internal/pipeline/mcpsync/sync_test.go | 32 ++++-
.../internal/cli/projects_create.go | 2 +-
.../internal/cli/projects_list.go | 2 +-
.../internal/cli/projects_tasks_list-project.go | 2 +-
.../internal/cli/projects_tasks_update-project.go | 2 +-
.../internal/cli/promoted_public.go | 2 +-
.../internal/cli/items_enterprise.go | 2 +-
.../tier-routing-golden/internal/cli/items_list.go | 2 +-
.../internal/cli/items_premium.go | 2 +-
20 files changed, 397 insertions(+), 34 deletions(-)
diff --git a/internal/cli/dogfood.go b/internal/cli/dogfood.go
index 72a9ede3..6154c0bf 100644
--- a/internal/cli/dogfood.go
+++ b/internal/cli/dogfood.go
@@ -90,7 +90,7 @@ func newDogfoodCmd() *cobra.Command {
cmd.Flags().DurationVar(&timeout, "timeout", 30*time.Second, "Timeout for each live dogfood test")
cmd.Flags().StringVar(&writeAcceptance, "write-acceptance", "", "Write phase5-acceptance.json to this path when live dogfood passes")
cmd.Flags().StringVar(&authEnv, "auth-env", "", "Environment variable that proves an API credential was available for the acceptance marker")
- cmd.Flags().BoolVar(&allowDestructive, "allow-destructive", false, "Re-enable testing of endpoints classified as destructive-at-auth (path/annotation matches refresh/rotate/revoke). Default skips them to prevent runner-credential rotation.")
+ cmd.Flags().BoolVar(&allowDestructive, "allow-destructive", false, "Re-enable testing of endpoints classified as destructive-at-auth. Default skips them to prevent runner-credential rotation.")
_ = cmd.MarkFlagRequired("dir")
return cmd
}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 58fb758e..8bf275b9 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -617,6 +617,7 @@ type clientTemplateData struct {
type endpointTemplateData struct {
ResourceName string
ResourceBaseURL string
+ EffectivePath string
EffectiveTier string
FuncPrefix string
CommandPath string
@@ -1546,6 +1547,7 @@ func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool
epData := endpointTemplateData{
ResourceName: name,
ResourceBaseURL: strings.TrimRight(resource.BaseURL, "/"),
+ EffectivePath: effectiveEndpointPath(resource, endpoint),
EffectiveTier: g.Spec.EffectiveTier(resource, endpoint),
FuncPrefix: name,
CommandPath: name,
@@ -1612,6 +1614,7 @@ func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool
epData := endpointTemplateData{
ResourceName: subName,
ResourceBaseURL: subResourceBaseURL,
+ EffectivePath: effectiveSubEndpointPath(resource, subResource, endpoint),
EffectiveTier: g.Spec.EffectiveTier(effectiveResource, endpoint),
FuncPrefix: name + "-" + subName,
CommandPath: name + " " + subName,
@@ -2014,6 +2017,7 @@ func (g *Generator) renderPromotedCommandFiles(promotedCommands []PromotedComman
PromotedName string
ResourceName string
EndpointName string
+ EffectivePath string
Endpoint spec.Endpoint
EffectiveTier string
HasStore bool
@@ -2025,6 +2029,7 @@ func (g *Generator) renderPromotedCommandFiles(promotedCommands []PromotedComman
PromotedName: pc.PromotedName,
ResourceName: pc.ResourceName,
EndpointName: pc.EndpointName,
+ EffectivePath: effectiveEndpointPath(resource, pc.Endpoint),
Endpoint: pc.Endpoint,
EffectiveTier: g.Spec.EffectiveTier(resource, pc.Endpoint),
HasStore: g.VisionSet.Store,
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index ac27bbfa..eaaf2c0c 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -491,6 +491,42 @@ func TestGenerateAgentContextCommand(t *testing.T) {
assert.Contains(t, payload, "cli")
assert.Contains(t, payload, "auth")
assert.Contains(t, payload, "commands")
+
+ annotatedEndpoint := findAgentContextCommand(payload["commands"], func(command map[string]any) bool {
+ annotations, ok := command["annotations"].(map[string]any)
+ if !ok {
+ return false
+ }
+ return annotations["pp:endpoint"] != "" && annotations["mcp:read-only"] == "true"
+ })
+ require.NotNil(t, annotatedEndpoint, "agent-context must surface endpoint and read-only annotations")
+
+ unannotatedParent := findAgentContextCommand(payload["commands"], func(command map[string]any) bool {
+ _, hasAnnotations := command["annotations"]
+ subcommands, hasSubcommands := command["subcommands"].([]any)
+ return !hasAnnotations && hasSubcommands && len(subcommands) > 0
+ })
+ require.NotNil(t, unannotatedParent, "agent-context must keep annotations omitted when absent")
+}
+
+func findAgentContextCommand(root any, match func(map[string]any) bool) map[string]any {
+ commands, ok := root.([]any)
+ if !ok {
+ return nil
+ }
+ for _, raw := range commands {
+ command, ok := raw.(map[string]any)
+ if !ok {
+ continue
+ }
+ if match(command) {
+ return command
+ }
+ if found := findAgentContextCommand(command["subcommands"], match); found != nil {
+ return found
+ }
+ }
+ return nil
}
func TestGenerateOAuth2AuthTemplateConditionally(t *testing.T) {
diff --git a/internal/generator/templates/auth_browser.go.tmpl b/internal/generator/templates/auth_browser.go.tmpl
index ec03c239..cfc9de52 100644
--- a/internal/generator/templates/auth_browser.go.tmpl
+++ b/internal/generator/templates/auth_browser.go.tmpl
@@ -608,10 +608,11 @@ func newAuthRefreshQueriesCmd(flags *rootFlags) *cobra.Command {
var duration time.Duration
var outputPath string
cmd := &cobra.Command{
- Use: "refresh-queries",
- Short: "Refresh captured GraphQL persisted-query hashes",
- Long: "Refresh captured GraphQL persisted-query hashes for CLIs generated from browser traffic with the graphql_persisted_query hint.",
- Example: " {{.Name}}-pp-cli auth refresh-queries --duration 45s",
+ Use: "refresh-queries",
+ Short: "Refresh captured GraphQL persisted-query hashes",
+ Long: "Refresh captured GraphQL persisted-query hashes for CLIs generated from browser traffic with the graphql_persisted_query hint.",
+ Example: " {{.Name}}-pp-cli auth refresh-queries --duration 45s",
+ Annotations: map[string]string{"pp:destructive-auth": "false"},
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load(flags.configPath)
if err != nil {
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index 4c0e0d7d..92c62328 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -52,7 +52,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- end}}
Short: "{{oneline .Endpoint.Description}}",
Example: "{{exampleLine .CommandPath .EndpointName .Endpoint}}",
- Annotations: map[string]string{"pp:endpoint": "{{.ResourceName}}.{{.EndpointName}}"{{if .IsReadOnly}}, "mcp:read-only": "true"{{end}}},
+ Annotations: map[string]string{"pp:endpoint": {{printf "%q" (printf "%s.%s" .ResourceName .EndpointName)}}, "pp:method": {{printf "%q" (upper .Endpoint.Method)}}, "pp:path": {{printf "%q" .EffectivePath}}{{if .IsReadOnly}}, "mcp:read-only": "true"{{end}}},
RunE: func(cmd *cobra.Command, args []string) error {
{{- if positionalArgs .Endpoint}}
if len(args) == 0 {
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index b6a521f5..ef768ddb 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -29,7 +29,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
Short: "{{oneline .Endpoint.Description}}",
Long: "Shortcut for '{{.ResourceName}} {{.EndpointName}}'. {{oneline .Endpoint.Description}}",
Example: " {{modulePath}} {{.PromotedName}}",
- Annotations: map[string]string{"pp:endpoint": "{{.ResourceName}}.{{.EndpointName}}"{{if .IsReadOnly}}, "mcp:read-only": "true"{{end}}},
+ Annotations: map[string]string{"pp:endpoint": {{printf "%q" (printf "%s.%s" .ResourceName .EndpointName)}}, "pp:method": {{printf "%q" (upper .Endpoint.Method)}}, "pp:path": {{printf "%q" .EffectivePath}}{{if .IsReadOnly}}, "mcp:read-only": "true"{{end}}},
RunE: func(cmd *cobra.Command, args []string) error {
{{- range .Endpoint.Params}}
{{- if and .Required (not .Positional) (not .Default)}}
diff --git a/internal/pipeline/live_dogfood.go b/internal/pipeline/live_dogfood.go
index bea61c10..028cf24c 100644
--- a/internal/pipeline/live_dogfood.go
+++ b/internal/pipeline/live_dogfood.go
@@ -33,8 +33,8 @@ const (
LiveDogfoodTestErrorReal LiveDogfoodTestKind = "error_path_real"
)
-// reasonDestructiveAtAuth is the Skip reason emitted for endpoints whose
-// path or pp:endpoint annotation matches refresh/rotate/revoke. Reused
+// reasonDestructiveAtAuth is the Skip reason emitted for endpoints that
+// can invalidate the credential used by the live-dogfood runner. Reused
// across the matrix builder, the flag help text, and the test fixtures.
const reasonDestructiveAtAuth = "destructive-at-auth"
@@ -873,28 +873,76 @@ func skippedLiveDogfoodResult(command string, kind LiveDogfoodTestKind, reason s
}
}
-// destructiveAuthTerms are case-insensitive substrings classifying a
-// command as destructive-at-auth.
-var destructiveAuthTerms = []string{"refresh", "rotate", "revoke"}
+const (
+ endpointAnnotation = "pp:endpoint"
+ endpointMethodAnnotation = "pp:method"
+ endpointPathAnnotation = "pp:path"
+ mcpReadOnlyAnnotation = "mcp:read-only"
+ destructiveAuthAnnotation = "pp:destructive-auth"
+)
+
+// destructiveAuthTerms are case-insensitive command or endpoint tokens
+// classifying a command as destructive-at-auth.
+var destructiveAuthTerms = map[string]bool{
+ "refresh": true,
+ "rotate": true,
+ "revoke": true,
+ "regenerate": true,
+ "reset": true,
+ "cycle": true,
+}
+
+var destructiveAuthResources = map[string]bool{
+ "api-keys": true,
+ "api_keys": true,
+ "sessions": true,
+ "tokens": true,
+}
-// isDestructiveAtAuth reports whether a command rotates or revokes the
-// bearer the live-dogfood runner is using. Reads pp:endpoint
+// isDestructiveAtAuth reports whether a command can invalidate the bearer
+// the live-dogfood runner is using. Reads pp:endpoint
// (authoritative for endpoint-mirror commands) and falls back to
// path-segment matching across the command path for novel commands.
// Read-only commands are exempt regardless of name.
func isDestructiveAtAuth(annotations map[string]string, commandPath []string) bool {
- if annotations["mcp:read-only"] == "true" {
+ if v, ok := annotations[destructiveAuthAnnotation]; ok {
+ return annotationIsTrueValue(v)
+ }
+ if annotationIsTrueValue(annotations[mcpReadOnlyAnnotation]) {
return false
}
- if endpoint := annotations["pp:endpoint"]; endpoint != "" {
- return containsAnyOf(strings.ToLower(endpoint), destructiveAuthTerms)
+ if endpoint := annotations[endpointAnnotation]; endpoint != "" {
+ if containsDestructiveAuthTerm(endpoint) {
+ return true
+ }
+ if strings.EqualFold(strings.TrimSpace(annotations[endpointMethodAnnotation]), "DELETE") &&
+ endpointTargetsAuthResource(endpoint, annotations[endpointPathAnnotation]) {
+ return true
+ }
+ return false
}
- for _, seg := range commandPath {
- if containsAnyOf(strings.ToLower(seg), destructiveAuthTerms) {
+ return slices.ContainsFunc(commandPath, containsDestructiveAuthTerm)
+}
+
+func containsDestructiveAuthTerm(s string) bool {
+ tokens := strings.FieldsFunc(strings.ToLower(s), func(r rune) bool {
+ return r < 'a' || r > 'z'
+ })
+ return slices.ContainsFunc(tokens, func(token string) bool {
+ return destructiveAuthTerms[token]
+ })
+}
+
+func endpointTargetsAuthResource(endpoint, path string) bool {
+ for _, segment := range splitPath(path) {
+ segment = strings.ToLower(strings.Trim(segment, "{}:"))
+ if destructiveAuthResources[segment] {
return true
}
}
- return false
+ return slices.ContainsFunc(strings.Split(strings.ToLower(endpoint), "."), func(segment string) bool {
+ return destructiveAuthResources[segment]
+ })
}
func liveDogfoodHappyArgs(command liveDogfoodCommand) ([]string, bool) {
@@ -972,13 +1020,17 @@ func finalizeLiveDogfoodReport(report *LiveDogfoodReport) {
}
}
// Failed-or-empty wins: any real failure or a fully empty matrix is FAIL.
+ // Quick runs whose whole slice was explicitly skipped are no-signal, not
+ // a failing matrix: the classifier did its job and avoided live mutation.
// The quick-level PASS arm uses min(5, MatrixSize) for the threshold so
// single-command quick runs (~4 entries when all probes succeed) can
// PASS, while keeping a MatrixSize >= 4 floor that blocks pathological
// matrices where most entries skipped.
switch {
- case report.Failed > 0 || report.MatrixSize == 0:
+ case report.Failed > 0 || (report.MatrixSize == 0 && (report.Level != "quick" || report.Skipped == 0)):
report.Verdict = "FAIL"
+ case report.Level == "quick" && report.MatrixSize == 0 && report.Skipped > 0:
+ report.Verdict = "PASS"
case report.Level == "quick" && report.MatrixSize >= 4 && report.Passed+report.Skipped >= min(5, report.MatrixSize):
report.Verdict = "PASS"
case report.Level == "quick":
diff --git a/internal/pipeline/live_dogfood_destructive_test.go b/internal/pipeline/live_dogfood_destructive_test.go
index 45a6b855..cbeaa745 100644
--- a/internal/pipeline/live_dogfood_destructive_test.go
+++ b/internal/pipeline/live_dogfood_destructive_test.go
@@ -32,6 +32,73 @@ func TestIsDestructiveAtAuth(t *testing.T) {
path: []string{"my-cli", "tokens"},
want: true,
},
+ {
+ name: "annotation primary regenerate",
+ annotations: map[string]string{"pp:endpoint": "api-keys.regenerate-key"},
+ path: []string{"my-cli", "api-keys"},
+ want: true,
+ },
+ {
+ name: "annotation primary reset",
+ annotations: map[string]string{"pp:endpoint": "passwords.reset-token"},
+ path: []string{"my-cli", "passwords"},
+ want: true,
+ },
+ {
+ name: "annotation primary cycle",
+ annotations: map[string]string{"pp:endpoint": "tokens.cycle"},
+ path: []string{"my-cli", "tokens"},
+ want: true,
+ },
+ {
+ name: "delete api-keys endpoint by method and path",
+ annotations: map[string]string{
+ "pp:endpoint": "api-keys.delete-key",
+ "pp:method": "DELETE",
+ "pp:path": "/v1/api-keys/{id}",
+ },
+ path: []string{"my-cli", "api-keys"},
+ want: true,
+ },
+ {
+ name: "delete sessions endpoint by method and path",
+ annotations: map[string]string{
+ "pp:endpoint": "sessions.destroy",
+ "pp:method": "DELETE",
+ "pp:path": "/sessions/{session_id}",
+ },
+ path: []string{"my-cli", "sessions"},
+ want: true,
+ },
+ {
+ name: "delete tokens endpoint by method and endpoint resource fallback",
+ annotations: map[string]string{
+ "pp:endpoint": "tokens.delete",
+ "pp:method": "DELETE",
+ },
+ path: []string{"my-cli", "tokens"},
+ want: true,
+ },
+ {
+ name: "delete ordinary resource is not destructive-at-auth",
+ annotations: map[string]string{
+ "pp:endpoint": "users.delete-user",
+ "pp:method": "DELETE",
+ "pp:path": "/users/{id}",
+ },
+ path: []string{"my-cli", "users"},
+ want: false,
+ },
+ {
+ name: "delete auth resource without DELETE method is not method-classified",
+ annotations: map[string]string{
+ "pp:endpoint": "api-keys.delete-preview",
+ "pp:method": "GET",
+ "pp:path": "/api-keys/{id}",
+ },
+ path: []string{"my-cli", "api-keys"},
+ want: false,
+ },
{
name: "annotation case-insensitive",
annotations: map[string]string{"pp:endpoint": "API-Keys.Refresh-Key"},
@@ -74,6 +141,18 @@ func TestIsDestructiveAtAuth(t *testing.T) {
path: []string{"my-cli", "store", "refresh"},
want: false,
},
+ {
+ name: "destructive-auth false annotation exempts generated refresh helper",
+ annotations: map[string]string{"pp:destructive-auth": "false"},
+ path: []string{"my-cli", "auth", "refresh-queries"},
+ want: false,
+ },
+ {
+ name: "destructive-auth true annotation opts in",
+ annotations: map[string]string{"pp:destructive-auth": "true"},
+ path: []string{"my-cli", "auth", "metadata"},
+ want: true,
+ },
{
name: "empty inputs",
want: false,
diff --git a/internal/pipeline/live_dogfood_test.go b/internal/pipeline/live_dogfood_test.go
index f59727de..419dff3c 100644
--- a/internal/pipeline/live_dogfood_test.go
+++ b/internal/pipeline/live_dogfood_test.go
@@ -108,6 +108,58 @@ func TestRunLiveDogfoodErrorPathAcceptsExpectedNonZeroExit(t *testing.T) {
assert.Equal(t, 2, errorPath.ExitCode)
}
+func TestRunLiveDogfoodSkipsDestructiveByDefault(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("test uses a shell script as the fake binary; skip on Windows")
+ }
+
+ dir, binaryName := writeLiveDogfoodDestructiveFixture(t)
+ report, err := RunLiveDogfood(LiveDogfoodOptions{
+ CLIDir: dir,
+ BinaryName: binaryName,
+ Level: "full",
+ Timeout: 2 * time.Second,
+ })
+ require.NoError(t, err)
+
+ var destructiveSkips int
+ for _, result := range report.Tests {
+ if result.Command == "api-keys refresh" {
+ assert.Equal(t, LiveDogfoodStatusSkip, result.Status)
+ assert.Equal(t, reasonDestructiveAtAuth, result.Reason)
+ destructiveSkips++
+ }
+ }
+ assert.Equal(t, 4, destructiveSkips)
+
+ widgetsHelp := findResultByCommandKind(report, "widgets list", LiveDogfoodTestHelp)
+ require.NotNil(t, widgetsHelp)
+ assert.Equal(t, LiveDogfoodStatusPass, widgetsHelp.Status)
+}
+
+func TestRunLiveDogfoodAllowDestructiveBypass(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("test uses a shell script as the fake binary; skip on Windows")
+ }
+
+ dir, binaryName := writeLiveDogfoodDestructiveFixture(t)
+ report, err := RunLiveDogfood(LiveDogfoodOptions{
+ CLIDir: dir,
+ BinaryName: binaryName,
+ Level: "full",
+ Timeout: 2 * time.Second,
+ AllowDestructive: true,
+ })
+ require.NoError(t, err)
+
+ assert.Equal(t, 0, countResultsWithReason(report.Tests, reasonDestructiveAtAuth))
+ for _, kind := range []LiveDogfoodTestKind{LiveDogfoodTestHelp, LiveDogfoodTestHappy, LiveDogfoodTestJSON} {
+ result := findResultByCommandKind(report, "api-keys refresh", kind)
+ require.NotNil(t, result)
+ assert.Equal(t, LiveDogfoodStatusPass, result.Status, result.Reason)
+ }
+}
+
func TestRunLiveDogfoodExplicitBinaryNameMustExist(t *testing.T) {
dir := t.TempDir()
@@ -216,6 +268,12 @@ func TestFinalizeLiveDogfoodReportVerdictGate(t *testing.T) {
name: "quick all skip — MatrixSize 0",
level: "quick",
results: []LiveDogfoodTestResult{mkResult(LiveDogfoodStatusSkip), mkResult(LiveDogfoodStatusSkip)},
+ want: "PASS",
+ },
+ {
+ name: "full all skip — MatrixSize 0 still blocks acceptance",
+ level: "full",
+ results: []LiveDogfoodTestResult{mkResult(LiveDogfoodStatusSkip), mkResult(LiveDogfoodStatusSkip)},
want: "FAIL",
},
{
@@ -788,6 +846,90 @@ exit 99
return dir, binaryName
}
+func writeLiveDogfoodDestructiveFixture(t *testing.T) (dir string, binaryName string) {
+ t.Helper()
+
+ dir = t.TempDir()
+ binaryName = "fixture-pp-cli"
+ writeTestManifestForLiveDogfood(t, dir)
+
+ binPath := filepath.Join(dir, binaryName)
+ script := `#!/bin/sh
+set -u
+
+if [ "$1" = "agent-context" ]; then
+ cat <<'JSON'
+{
+ "commands": [
+ {"name":"api-keys","subcommands":[
+ {"name":"refresh","annotations":{"pp:endpoint":"api-keys.keys-refresh"}}
+ ]},
+ {"name":"widgets","subcommands":[
+ {"name":"list"}
+ ]}
+ ]
+}
+JSON
+ exit 0
+fi
+
+if [ "$1" = "api-keys" ] && [ "$2" = "refresh" ] && [ "${3:-}" = "--help" ]; then
+ cat <<'HELP'
+Refresh the current API key.
+
+Usage:
+ fixture-pp-cli api-keys refresh [flags]
+
+Examples:
+ fixture-pp-cli api-keys refresh
+
+Flags:
+ --json Output JSON
+HELP
+ exit 0
+fi
+
+if [ "$1" = "api-keys" ] && [ "$2" = "refresh" ]; then
+ if [ "${3:-}" = "--json" ]; then
+ echo '{"refreshed":true}'
+ exit 0
+ fi
+ echo 'refreshed'
+ exit 0
+fi
+
+if [ "$1" = "widgets" ] && [ "$2" = "list" ] && [ "${3:-}" = "--help" ]; then
+ cat <<'HELP'
+List widgets.
+
+Usage:
+ fixture-pp-cli widgets list [flags]
+
+Examples:
+ fixture-pp-cli widgets list
+
+Flags:
+ --json Output JSON
+HELP
+ exit 0
+fi
+
+if [ "$1" = "widgets" ] && [ "$2" = "list" ]; then
+ if [ "${3:-}" = "--json" ]; then
+ echo '{"results":[]}'
+ exit 0
+ fi
+ echo 'widgets'
+ exit 0
+fi
+
+echo "unexpected args: $*" >&2
+exit 99
+`
+ require.NoError(t, os.WriteFile(binPath, []byte(script), 0o755))
+ return dir, binaryName
+}
+
func writeTestManifestForLiveDogfood(t *testing.T, dir string) {
t.Helper()
require.NoError(t, WriteCLIManifest(dir, CLIManifest{
@@ -1373,6 +1515,16 @@ func findResultByCommandKind(report *LiveDogfoodReport, command string, kind Liv
return nil
}
+func countResultsWithReason(results []LiveDogfoodTestResult, reason string) int {
+ count := 0
+ for _, result := range results {
+ if result.Reason == reason {
+ count++
+ }
+ }
+ return count
+}
+
// setupRichFixture is the shared preamble for U2/U3 tests: skip on Windows,
// build the rich fixture, and enable the argv-log side channel via a unique
// per-test tempfile path. Returns the fixture dir, binary name, and the
diff --git a/internal/pipeline/mcp_size.go b/internal/pipeline/mcp_size.go
index 5498b17e..44b3b30e 100644
--- a/internal/pipeline/mcp_size.go
+++ b/internal/pipeline/mcp_size.go
@@ -419,6 +419,7 @@ func cobratreeCommandKind(cmd cobraCommandLiteral) mcpCobraCommandKind {
}
func annotationIsTrueValue(v string) bool {
+ v = strings.ToLower(strings.TrimSpace(v))
return v == "true" || v == "1" || v == "yes"
}
diff --git a/internal/pipeline/mcpsync/sync.go b/internal/pipeline/mcpsync/sync.go
index 9ddd5b8d..33752e74 100644
--- a/internal/pipeline/mcpsync/sync.go
+++ b/internal/pipeline/mcpsync/sync.go
@@ -32,7 +32,8 @@ var (
errAnnotationSoftFail = errors.New("endpoint annotation skipped")
)
-var endpointAnnotationLine = regexp.MustCompile(`(?m)^\s*Annotations: map\[string\]string\{"pp:endpoint": "[^"]+"(?:, "mcp:read-only": "true")?\},\s*$`)
+var endpointAnnotationLine = regexp.MustCompile(`(?m)^\s*Annotations: map\[string\]string\{"pp:endpoint": "[^"]+", "pp:method": "[^"]+", "pp:path": "[^"]+"(?:, "mcp:read-only": "true")?\},\s*$`)
+var staleEndpointAnnotationLine = regexp.MustCompile(`(?m)^\s*Annotations: map\[string\]string\{"pp:endpoint": "[^"]+"(?:, "mcp:read-only": "true")?\},\s*$`)
type Result struct {
Changed bool
@@ -300,6 +301,14 @@ func ensureEndpointAnnotation(path, annotationLine string) error {
return fmt.Errorf("reading endpoint command %s: %w", path, err)
}
src := string(data)
+ if endpointAnnotationLine.MatchString(src) {
+ return nil
+ }
+ annotationLine = strings.TrimSuffix(annotationLine, "\n")
+ if staleEndpointAnnotationLine.MatchString(src) {
+ next := staleEndpointAnnotationLine.ReplaceAllString(src, annotationLine)
+ return writeFileAtomic(path, []byte(next))
+ }
if strings.Contains(src, `"pp:endpoint"`) {
return nil
}
@@ -320,9 +329,7 @@ func ensureEndpointAnnotation(path, annotationLine string) error {
return fmt.Errorf("%s does not match the generated endpoint command shape; cannot add endpoint MCP annotation", path)
}
- if !strings.HasSuffix(annotationLine, "\n") {
- annotationLine += "\n"
- }
+ annotationLine += "\n"
next := src[:insertAt] + annotationLine + src[insertAt:]
return writeFileAtomic(path, []byte(next))
}
diff --git a/internal/pipeline/mcpsync/sync_test.go b/internal/pipeline/mcpsync/sync_test.go
index b066d084..ac458241 100644
--- a/internal/pipeline/mcpsync/sync_test.go
+++ b/internal/pipeline/mcpsync/sync_test.go
@@ -71,13 +71,43 @@ func RegisterNovelFeatureTools() { shellOutToCLI("projects list") }
updatedEndpoint, err := os.ReadFile(endpointPath)
require.NoError(t, err)
- assert.Contains(t, string(updatedEndpoint), `Annotations: map[string]string{"pp:endpoint": "projects.list", "mcp:read-only": "true"}`)
+ assert.Contains(t, string(updatedEndpoint), `Annotations: map[string]string{"pp:endpoint": "projects.list", "pp:method": "GET", "pp:path": "/projects", "mcp:read-only": "true"}`)
updatedTools, err := os.ReadFile(filepath.Join(cliDir, "internal", "mcp", "tools.go"))
require.NoError(t, err)
assert.Contains(t, string(updatedTools), "cobratree.RegisterAll")
}
+func TestEnsureEndpointAnnotationUpgradesStaleEndpointAnnotation(t *testing.T) {
+ t.Parallel()
+
+ path := filepath.Join(t.TempDir(), "api_keys_delete.go")
+ oldAnnotation := `Annotations: map[string]string{"pp:endpoint": "api-keys.delete"}`
+ src := `// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+package cli
+
+func newAPIKeysDeleteCmd() {
+ cmd := struct {
+ Use string
+ Annotations map[string]string
+ }{
+ Use: "delete",
+ ` + oldAnnotation + `,
+ }
+ _ = cmd
+}
+`
+ require.NoError(t, os.WriteFile(path, []byte(src), 0o644))
+
+ newAnnotation := ` Annotations: map[string]string{"pp:endpoint": "api-keys.delete", "pp:method": "DELETE", "pp:path": "/api-keys/{id}"},`
+ require.NoError(t, ensureEndpointAnnotation(path, newAnnotation))
+
+ updated, err := os.ReadFile(path)
+ require.NoError(t, err)
+ assert.Contains(t, string(updated), newAnnotation)
+ assert.NotContains(t, string(updated), oldAnnotation)
+}
+
func TestEnsureRootCmdExportPreservesRootFlagPointer(t *testing.T) {
t.Parallel()
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
index da95c738..578330a7 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
@@ -22,7 +22,7 @@ func newProjectsCreateCmd(flags *rootFlags) *cobra.Command {
Use: "create",
Short: "Create project",
Example: " printing-press-golden-pp-cli projects create --name example-resource",
- Annotations: map[string]string{"pp:endpoint": "projects.create"},
+ Annotations: map[string]string{"pp:endpoint": "projects.create", "pp:method": "POST", "pp:path": "/projects"},
RunE: func(cmd *cobra.Command, args []string) error {
if !stdinBody {
if !cmd.Flags().Changed("name") && !flags.dryRun {
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
index c25b028a..1384bd9f 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
@@ -21,7 +21,7 @@ func newProjectsListCmd(flags *rootFlags) *cobra.Command {
Use: "list",
Short: "List projects",
Example: " printing-press-golden-pp-cli projects list",
- Annotations: map[string]string{"pp:endpoint": "projects.list", "mcp:read-only": "true"},
+ Annotations: map[string]string{"pp:endpoint": "projects.list", "pp:method": "GET", "pp:path": "/projects", "mcp:read-only": "true"},
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("status") {
allowedStatus := []string{ "draft", "active", "archived" }
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
index 57dc171c..ff35ec4f 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
@@ -22,7 +22,7 @@ func newProjectsTasksListProjectCmd(flags *rootFlags) *cobra.Command {
Aliases: []string{"get"},
Short: "List project tasks",
Example: " printing-press-golden-pp-cli projects tasks list-project 550e8400-e29b-41d4-a716-446655440000",
- Annotations: map[string]string{"pp:endpoint": "tasks.list-project", "mcp:read-only": "true"},
+ Annotations: map[string]string{"pp:endpoint": "tasks.list-project", "pp:method": "GET", "pp:path": "/projects/{projectId}/tasks", "mcp:read-only": "true"},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
index 313acb9d..0587b7d1 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
@@ -23,7 +23,7 @@ func newProjectsTasksUpdateProjectCmd(flags *rootFlags) *cobra.Command {
Aliases: []string{"update"},
Short: "Update project task",
Example: " printing-press-golden-pp-cli projects tasks update-project 550e8400-e29b-41d4-a716-446655440000 550e8400-e29b-41d4-a716-446655440000",
- Annotations: map[string]string{"pp:endpoint": "tasks.update-project"},
+ Annotations: map[string]string{"pp:endpoint": "tasks.update-project", "pp:method": "PATCH", "pp:path": "/projects/{projectId}/tasks/{taskId}"},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
index 212bb3b1..57fce0ef 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
@@ -18,7 +18,7 @@ func newPublicPromotedCmd(flags *rootFlags) *cobra.Command {
Short: "Get public service status",
Long: "Shortcut for 'public get-status'. Get public service status",
Example: " printing-press-golden-pp-cli public",
- Annotations: map[string]string{"pp:endpoint": "public.get-status", "mcp:read-only": "true"},
+ Annotations: map[string]string{"pp:endpoint": "public.get-status", "pp:method": "GET", "pp:path": "/public/status", "mcp:read-only": "true"},
RunE: func(cmd *cobra.Command, args []string) error {
c, err := flags.newClient()
if err != nil {
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go
index cf1cc5a6..c8bd6924 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go
@@ -17,7 +17,7 @@ func newItemsEnterpriseCmd(flags *rootFlags) *cobra.Command {
Use: "enterprise",
Short: "List enterprise items",
Example: " tier-routing-golden-pp-cli items enterprise",
- Annotations: map[string]string{"pp:endpoint": "items.enterprise", "mcp:read-only": "true"},
+ Annotations: map[string]string{"pp:endpoint": "items.enterprise", "pp:method": "GET", "pp:path": "/items/enterprise", "mcp:read-only": "true"},
RunE: func(cmd *cobra.Command, args []string) error {
c, err := flags.newClient()
if err != nil {
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go
index a62bd071..e8376e6e 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go
@@ -18,7 +18,7 @@ func newItemsListCmd(flags *rootFlags) *cobra.Command {
Use: "list",
Short: "List free items",
Example: " tier-routing-golden-pp-cli items list",
- Annotations: map[string]string{"pp:endpoint": "items.list", "mcp:read-only": "true"},
+ Annotations: map[string]string{"pp:endpoint": "items.list", "pp:method": "GET", "pp:path": "/items", "mcp:read-only": "true"},
RunE: func(cmd *cobra.Command, args []string) error {
c, err := flags.newClient()
if err != nil {
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
index 80e7d7fb..1a594ba2 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
@@ -17,7 +17,7 @@ func newItemsPremiumCmd(flags *rootFlags) *cobra.Command {
Use: "premium",
Short: "List paid items",
Example: " tier-routing-golden-pp-cli items premium",
- Annotations: map[string]string{"pp:endpoint": "items.premium", "mcp:read-only": "true"},
+ Annotations: map[string]string{"pp:endpoint": "items.premium", "pp:method": "GET", "pp:path": "/items/premium", "mcp:read-only": "true"},
RunE: func(cmd *cobra.Command, args []string) error {
c, err := flags.newClient()
if err != nil {
← 622e15f5 docs(cli): capture mcp handler path-vs-query positional recu
·
back to Cli Printing Press
·
fix(cli): correct no-auth 403 hints (#629) 013f92ad →