From 2f288c07af79889068a0c2a3c5ae813c34ff1065 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Thu, 26 Feb 2026 12:07:56 +0000 Subject: [PATCH] Expose exit code semantics from YAML definitions Add ExitCodeMeaning and IsFatalExitCode methods to Translator so callers can distinguish non-zero exit codes that produce valid output (like npm ls exiting 1 on peer dep issues) from actual failures. Change npm resolve exit code 1 from "error" to "partial" since npm ls still outputs valid JSON in that case. --- definitions/npm.yaml | 2 +- translator.go | 30 ++++++++++++++++++++ translator_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/definitions/npm.yaml b/definitions/npm.yaml index f4e7990..9732d1a 100644 --- a/definitions/npm.yaml +++ b/definitions/npm.yaml @@ -97,7 +97,7 @@ commands: default_flags: [--depth, Infinity, --json, --long] exit_codes: 0: success - 1: error + 1: partial capabilities: - install diff --git a/translator.go b/translator.go index 1e3e72a..7a4bad2 100644 --- a/translator.go +++ b/translator.go @@ -247,6 +247,36 @@ func (t *Translator) validate(validatorName, value string) error { return nil } +// ExitCodeMeaning returns the semantic meaning of an exit code for a +// manager/operation pair, as defined in the YAML. Returns "" if the +// manager, operation, or exit code is not defined. +func (t *Translator) ExitCodeMeaning(managerName, operation string, exitCode int) string { + def, ok := t.definitions[managerName] + if !ok { + return "" + } + cmd, ok := def.Commands[operation] + if !ok { + return "" + } + return cmd.ExitCodes[exitCode] +} + +// IsFatalExitCode reports whether exitCode represents a fatal error for +// the given manager/operation. Exit code 0 is never fatal. For non-zero +// codes, the result is fatal unless the YAML definition assigns a +// non-"error" meaning. +func (t *Translator) IsFatalExitCode(managerName, operation string, exitCode int) bool { + if exitCode == 0 { + return false + } + meaning := t.ExitCodeMeaning(managerName, operation, exitCode) + if meaning == "" || meaning == "error" { + return true + } + return false +} + func isTruthy(val any) bool { if val == nil { return false diff --git a/translator_test.go b/translator_test.go index 91caba5..01a9310 100644 --- a/translator_test.go +++ b/translator_test.go @@ -3356,3 +3356,68 @@ func TestPipVendor(t *testing.T) { t.Errorf("got %v, want %v", cmd, expected) } } + +// --- ExitCodeMeaning tests --- + +func TestExitCodeMeaning(t *testing.T) { + tr := loadTranslator(t) + + tests := []struct { + name string + manager string + operation string + exitCode int + want string + }{ + {"npm resolve 0", "npm", "resolve", 0, "success"}, + {"npm resolve 1 partial", "npm", "resolve", 1, "partial"}, + {"npm install 0", "npm", "install", 0, "success"}, + {"npm install 1 error", "npm", "install", 1, "error"}, + {"npm outdated 1 outdated", "npm", "outdated", 1, "outdated"}, + {"unknown manager", "nonexistent", "install", 1, ""}, + {"unknown operation", "npm", "nonexistent", 1, ""}, + {"undefined exit code", "npm", "install", 42, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tr.ExitCodeMeaning(tt.manager, tt.operation, tt.exitCode) + if got != tt.want { + t.Errorf("ExitCodeMeaning(%q, %q, %d) = %q, want %q", + tt.manager, tt.operation, tt.exitCode, got, tt.want) + } + }) + } +} + +// --- IsFatalExitCode tests --- + +func TestIsFatalExitCode(t *testing.T) { + tr := loadTranslator(t) + + tests := []struct { + name string + manager string + operation string + exitCode int + want bool + }{ + {"exit 0 never fatal", "npm", "install", 0, false}, + {"npm install 1 is fatal", "npm", "install", 1, true}, + {"npm resolve 1 not fatal", "npm", "resolve", 1, false}, + {"npm outdated 1 not fatal", "npm", "outdated", 1, false}, + {"unknown manager is fatal", "nonexistent", "install", 1, true}, + {"unknown operation is fatal", "npm", "nonexistent", 1, true}, + {"undefined exit code is fatal", "npm", "install", 42, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tr.IsFatalExitCode(tt.manager, tt.operation, tt.exitCode) + if got != tt.want { + t.Errorf("IsFatalExitCode(%q, %q, %d) = %v, want %v", + tt.manager, tt.operation, tt.exitCode, got, tt.want) + } + }) + } +}