diff --git a/pkg/parser/schema_errors.go b/pkg/parser/schema_errors.go index 1e2fc3cefaa..209a171f8bd 100644 --- a/pkg/parser/schema_errors.go +++ b/pkg/parser/schema_errors.go @@ -217,8 +217,31 @@ var knownFieldValidValues = map[string]string{ "/permissions": "Valid permission scopes: actions, all, attestations, checks, contents, deployments, discussions, id-token, issues, metadata, models, organization-projects, packages, pages, pull-requests, repository-projects, security-events, statuses, vulnerability-alerts", } -// appendKnownFieldValidValuesHint appends a "Valid values: …" hint to message when the -// jsonPath matches a well-known field and the message is an unknown-property error. +// knownFieldScopes maps well-known JSON schema paths to a slice of valid scope names. +// This enables spell-check ("Did you mean?") suggestions for unknown-property errors. +// +// The permissions scope list mirrors permissions.oneOf[1].properties in main_workflow_schema.json. +// Update both when the schema changes. +var knownFieldScopes = map[string][]string{ + "/permissions": { + "actions", "all", "attestations", "checks", "contents", "deployments", + "discussions", "id-token", "issues", "metadata", "models", + "organization-projects", "packages", "pages", "pull-requests", + "repository-projects", "security-events", "statuses", "vulnerability-alerts", + }, +} + +// knownFieldDocs maps well-known JSON schema paths to documentation URLs. +var knownFieldDocs = map[string]string{ + "/permissions": "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token", +} + +// unknownPropertyPattern extracts the property name(s) from a rewritten "Unknown property(ies):" message. +var unknownPropertyPattern = regexp.MustCompile(`(?i)^Unknown propert(?:y|ies): (.+)$`) + +// appendKnownFieldValidValuesHint appends a "Valid values: …" hint, "Did you mean?" suggestions, +// and a documentation link to message when the jsonPath matches a well-known field and the +// message is an unknown-property error. // It returns the message unchanged for unknown paths or non-additional-properties messages. func appendKnownFieldValidValuesHint(message string, jsonPath string) string { // Use truncated prefix "unknown propert" to match both singular ("Unknown property") @@ -226,21 +249,77 @@ func appendKnownFieldValidValuesHint(message string, jsonPath string) string { if !strings.Contains(strings.ToLower(message), "unknown propert") { return message } - hint, ok := knownFieldValidValues[jsonPath] - if !ok { - // Check if the path is nested under a known parent (e.g. /permissions/contents) - for path, h := range knownFieldValidValues { + + // Find the best matching known path: exact match first, then the longest matching parent. + hint, hintOK := knownFieldValidValues[jsonPath] + scopes := knownFieldScopes[jsonPath] + docsURL := knownFieldDocs[jsonPath] + if !hintOK { + // Select the longest matching parent path deterministically to avoid + // random map iteration order when multiple known paths share a common prefix. + bestPath := "" + bestLen := 0 + for path := range knownFieldValidValues { if strings.HasPrefix(jsonPath, path+"/") { - hint = h - ok = true - break + if l := len(path); l > bestLen { + bestLen = l + bestPath = path + } } } + if bestPath != "" { + hint = knownFieldValidValues[bestPath] + scopes = knownFieldScopes[bestPath] + docsURL = knownFieldDocs[bestPath] + hintOK = true + } } - if !ok { + if !hintOK { return message } - return message + " (" + hint + ")" + + result := message + " (" + hint + ")" + + // Add "Did you mean?" suggestions when the unknown property name is close to a valid scope. + if len(scopes) > 0 { + // unknownPropertyPattern has exactly one capture group, so a successful match + // returns [fullMatch, captureGroup1], giving len(m) == 2. + if m := unknownPropertyPattern.FindStringSubmatch(message); len(m) == 2 { + unknownProps := strings.Split(m[1], ", ") + var allSuggestions []string + for _, prop := range unknownProps { + prop = strings.TrimSpace(prop) + if prop == "" { + continue + } + // maxClosestMatches is defined in schema_suggestions.go in the same package. + closest := FindClosestMatches(prop, scopes, maxClosestMatches) + allSuggestions = append(allSuggestions, closest...) + } + // Deduplicate suggestions + seen := make(map[string]bool) + var unique []string + for _, s := range allSuggestions { + if !seen[s] { + seen[s] = true + unique = append(unique, s) + } + } + if len(unique) == 1 { + result = fmt.Sprintf("%s. Did you mean '%s'?", result, unique[0]) + } else if len(unique) > 1 { + result = fmt.Sprintf("%s. Did you mean: %s?", result, strings.Join(unique, ", ")) + } + } + } + + // Append documentation link on the same line to avoid breaking bullet-list formatting + // when this message is embedded in "Multiple schema validation failures:" output. + if docsURL != "" { + result = fmt.Sprintf("%s See: %s", result, docsURL) + } + + return result } // rewriteAdditionalPropertiesError rewrites "additional properties not allowed" errors to be more user-friendly diff --git a/pkg/parser/schema_errors_test.go b/pkg/parser/schema_errors_test.go index bcb707edef3..93f24fa1edd 100644 --- a/pkg/parser/schema_errors_test.go +++ b/pkg/parser/schema_errors_test.go @@ -306,3 +306,75 @@ func TestRewriteAdditionalPropertiesErrorOrdering(t *testing.T) { }) } } + +// TestAppendKnownFieldValidValuesHint tests that the hint function appends valid values, +// "Did you mean?" suggestions, and documentation links for well-known schema paths. +func TestAppendKnownFieldValidValuesHint(t *testing.T) { + tests := []struct { + name string + message string + jsonPath string + contains []string // substrings that must appear + excludes []string // substrings that must NOT appear + }{ + { + name: "no hint for non-unknown-property message", + message: "value must be one of 'read', 'write', 'none'", + jsonPath: "/permissions", + contains: []string{"value must be one of"}, + excludes: []string{"Valid permission scopes", "See:"}, + }, + { + name: "hint appended for permissions path", + message: "Unknown property: issuess", + jsonPath: "/permissions", + contains: []string{"Valid permission scopes", "See: https://docs.github.com"}, + }, + { + name: "did you mean suggestion for close typo", + message: "Unknown property: issuess", + jsonPath: "/permissions", + contains: []string{"Did you mean 'issues'?"}, + }, + { + name: "did you mean suggestion for another typo", + message: "Unknown property: contnets", + jsonPath: "/permissions", + contains: []string{"Did you mean 'contents'?"}, + }, + { + name: "no did you mean for unrelated word", + message: "Unknown property: completely-unknown-xyz", + jsonPath: "/permissions", + excludes: []string{"Did you mean"}, + contains: []string{"Valid permission scopes"}, + }, + { + name: "no hint for unknown path", + message: "Unknown property: foo", + jsonPath: "/some-unknown-path", + contains: []string{"Unknown property: foo"}, + excludes: []string{"Valid permission scopes", "See:"}, + }, + { + name: "hint works for nested permissions path", + message: "Unknown property: issuess", + jsonPath: "/permissions/issuess", + contains: []string{"Valid permission scopes", "See: https://docs.github.com"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := appendKnownFieldValidValuesHint(tt.message, tt.jsonPath) + for _, want := range tt.contains { + assert.Contains(t, result, want, + "appendKnownFieldValidValuesHint should contain %q\nResult: %s", want, result) + } + for _, exclude := range tt.excludes { + assert.NotContains(t, result, exclude, + "appendKnownFieldValidValuesHint should not contain %q\nResult: %s", exclude, result) + } + }) + } +} diff --git a/pkg/parser/yaml_error.go b/pkg/parser/yaml_error.go index bf05c09e4a4..682c9d344d2 100644 --- a/pkg/parser/yaml_error.go +++ b/pkg/parser/yaml_error.go @@ -18,6 +18,117 @@ var ( sourceLinePattern = regexp.MustCompile(`(?m)^(>?\s*)(\d+)(\s*\|)`) ) +// yamlErrorTranslations maps common goccy/go-yaml internal error messages to +// user-friendly plain-language descriptions with actionable fix guidance. +// Each pattern is matched case-insensitively against the parser message text only +// (not the surrounding source-context lines in yaml.FormatError() output). +// +// These translations are the single source of truth shared between the parser +// and workflow packages. See TranslateYAMLMessage for public access. +var yamlErrorTranslations = []struct { + pattern string + replacement string +}{ + { + "unexpected key name", + "missing ':' after key — YAML mapping entries require 'key: value' format", + }, + { + "mapping value is not allowed in this context", + "unexpected ':' — check indentation or if this key belongs in a mapping block", + }, + { + "mapping values are not allowed", + "unexpected ':' — check indentation or if this key belongs in a mapping block", + }, + { + "string was used where mapping is expected", + "expected a YAML mapping (key: value pairs) but got a plain string", + }, + { + "non-map value is specified", + "expected a YAML mapping (key: value pairs) — did you forget a colon after the key?", + }, + { + "tab character cannot use as a map key directly", + "tab character in key — YAML requires spaces for indentation, not tabs", + }, + { + "found character that cannot start any token", + "invalid character — check indentation uses spaces, not tabs", + }, + { + "could not find expected ':'", + "missing ':' in key-value pair", + }, + { + "did not find expected key", + "incorrect indentation or missing key in mapping", + }, +} + +// TranslateYAMLMessage translates a raw goccy/go-yaml parser message to a user-friendly +// description. It is the public entry point used by both the parser and workflow packages +// so that both code paths share a single translation table. +// +// The function performs a case-insensitive substring replacement of the first matching +// pattern, leaving any surrounding text intact. This is safe for ASCII patterns because +// strings.ToLower preserves byte positions exactly for ASCII characters. +func TranslateYAMLMessage(message string) string { + lower := strings.ToLower(message) + for _, t := range yamlErrorTranslations { + if idx := strings.Index(lower, t.pattern); idx >= 0 { + yamlErrorLog.Printf("Translating YAML message pattern %q", t.pattern) + // Slice using idx from the lowercase string. Safe because all patterns are ASCII. + return message[:idx] + t.replacement + message[idx+len(t.pattern):] + } + } + return message +} + +// translateYAMLError translates cryptic goccy/go-yaml parser messages to user-friendly descriptions. +// It operates on the full yaml.FormatError() output, which includes a header line and source context: +// +// [line:col] original parser message +// > 1 | some: yaml +// ^ +// +// Only the parser message portion (the header line, after the "[line:col] " prefix) is translated. +// Source-context lines are left untouched to avoid accidentally replacing text inside user YAML content. +func translateYAMLError(formatted string) string { + if formatted == "" { + return formatted + } + + // Split into the header line (which contains the parser message) and the rest (source context). + var header, rest string + if nl := strings.IndexByte(formatted, '\n'); nl >= 0 { + header = formatted[:nl] + rest = formatted[nl:] + } else { + header = formatted + rest = "" + } + + // Within the header, locate the parser message text after the "[line:col] " prefix. + // If the prefix is absent (unusual), treat the entire header as the message. + msgStart := strings.Index(header, "] ") + var prefix, msg string + if msgStart >= 0 { + msgStart += len("] ") + prefix = header[:msgStart] + msg = header[msgStart:] + } else { + prefix = "" + msg = header + } + + // Translate only the message portion, leaving prefix and source context intact. + translated := TranslateYAMLMessage(msg) + + return prefix + translated + rest +} + // FormatYAMLError formats a YAML error with source code context using yaml.FormatError() // frontmatterLineOffset is the line number where the frontmatter content begins in the document (1-based) // Returns the formatted error string with line numbers adjusted for frontmatter position @@ -28,6 +139,9 @@ func FormatYAMLError(err error, frontmatterLineOffset int, sourceYAML string) st // colored=false to avoid ANSI escape codes, inclSource=true to include source lines formatted := yaml.FormatError(err, false, true) + // Translate cryptic parser messages to user-friendly descriptions (header line only) + formatted = translateYAMLError(formatted) + // Adjust line numbers in the formatted output to account for frontmatter position if frontmatterLineOffset > 1 { formatted = adjustLineNumbersInFormattedError(formatted, frontmatterLineOffset-1) diff --git a/pkg/parser/yaml_error_test.go b/pkg/parser/yaml_error_test.go index 1847307bb02..c6ab19a4e8f 100644 --- a/pkg/parser/yaml_error_test.go +++ b/pkg/parser/yaml_error_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/goccy/go-yaml" + "github.com/stretchr/testify/assert" ) // TestFormatYAMLError tests the new FormatYAMLError function that uses yaml.FormatError() @@ -156,3 +157,181 @@ func TestFormatYAMLErrorAdjustment(t *testing.T) { }) } } + +// TestTranslateYAMLError tests that cryptic goccy/go-yaml parser messages are translated +// to user-friendly descriptions, and that source context lines are left untouched. +func TestTranslateYAMLError(t *testing.T) { + tests := []struct { + name string + input string + contains string // expected substring in translated output + excludes string // must NOT appear in the first (header) line + sourceContextCheck string // if non-empty, must appear in the source context (after first line) + }{ + { + name: "unexpected key name translated", + input: "[1:1] unexpected key name\n> 1 | engine claude\n ^", + contains: "missing ':' after key", + excludes: "unexpected key name", + }, + { + name: "mapping value not allowed translated", + input: "[1:6] mapping value is not allowed in this context\n> 1 | key: value: extra\n ^", + contains: "unexpected ':'", + excludes: "mapping value is not allowed in this context", + }, + { + name: "string used where mapping expected translated", + input: "[1:1] string was used where mapping is expected\n> 1 | name value\n ^", + contains: "expected a YAML mapping", + excludes: "string was used where mapping is expected", + }, + { + name: "tab character error translated", + input: "[1:7] tab character cannot use as a map key directly\n> 1 | \tengine: claude\n ^", + contains: "tab character in key", + excludes: "tab character cannot use as a map key directly", + }, + { + name: "unrecognized messages are returned unchanged", + input: "[2:1] mapping key \"name\" already defined at [1:1]\n> 2 | name: dup\n ^", + contains: "already defined", + }, + { + name: "empty string is returned unchanged", + input: "", + contains: "", + }, + { + name: "pattern in source context line is not replaced", + input: "[1:1] unexpected key name\n> 1 | unexpected key name: here\n ^", + // The header should be translated, but source context must remain untouched. + contains: "missing ':' after key", + sourceContextCheck: "unexpected key name: here", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := translateYAMLError(tt.input) + if tt.contains != "" { + assert.Contains(t, result, tt.contains, + "translateYAMLError should contain %q\nResult: %s", tt.contains, result) + } + // Verify source context lines are preserved untouched when applicable. + if tt.sourceContextCheck != "" { + _, rest, _ := strings.Cut(result, "\n") + assert.Contains(t, rest, tt.sourceContextCheck, + "source context should be preserved unchanged\nContext: %s", rest) + } + // For cases with excludes, the pattern should not appear in the header line. + if tt.excludes != "" { + firstLine, _, _ := strings.Cut(result, "\n") + assert.NotContains(t, firstLine, tt.excludes, + "translateYAMLError header should not contain %q\nHeader: %s", tt.excludes, firstLine) + } + }) + } +} + +// TestTranslateYAMLMessage tests the exported TranslateYAMLMessage function used +// by both the parser and workflow packages. +func TestTranslateYAMLMessage(t *testing.T) { + tests := []struct { + name string + input string + contains string + excludes string + }{ + { + name: "unexpected key name", + input: "unexpected key name", + contains: "missing ':' after key", + excludes: "unexpected key name", + }, + { + name: "non-map value is specified", + input: "non-map value is specified", + contains: "expected a YAML mapping", + excludes: "non-map value is specified", + }, + { + name: "found character that cannot start any token", + input: "found character that cannot start any token", + contains: "invalid character", + excludes: "found character that cannot start any token", + }, + { + name: "unrecognized message is unchanged", + input: "some other error", + contains: "some other error", + }, + { + name: "empty string is unchanged", + input: "", + contains: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := TranslateYAMLMessage(tt.input) + if tt.contains != "" { + assert.Contains(t, result, tt.contains, + "TranslateYAMLMessage should contain %q\nResult: %s", tt.contains, result) + } + if tt.excludes != "" { + assert.NotContains(t, result, tt.excludes, + "TranslateYAMLMessage should not contain %q\nResult: %s", tt.excludes, result) + } + }) + } +} + +// TestFormatYAMLErrorTranslation verifies that FormatYAMLError applies translations +// to the underlying goccy/go-yaml error messages. +func TestFormatYAMLErrorTranslation(t *testing.T) { + tests := []struct { + name string + yamlContent string + shouldContain string + shouldExclude string + }{ + { + name: "missing colon translated", + yamlContent: "engine claude\nmodel: gpt-4", + shouldContain: "missing ':' after key", + shouldExclude: "unexpected key name", + }, + { + name: "extra colon translated", + yamlContent: "key: value: extra", + shouldContain: "unexpected ':'", + shouldExclude: "mapping value is not allowed in this context", + }, + { + name: "plain string translated", + yamlContent: "not a mapping", + shouldContain: "expected a YAML mapping", + shouldExclude: "string was used where mapping is expected", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result map[string]any + err := yaml.Unmarshal([]byte(tt.yamlContent), &result) + if err == nil { + t.Skipf("Expected YAML parsing to fail for content: %q", tt.yamlContent) + return + } + formatted := FormatYAMLError(err, 1, tt.yamlContent) + assert.Contains(t, formatted, tt.shouldContain, + "FormatYAMLError should contain %q\nResult: %s", tt.shouldContain, formatted) + if tt.shouldExclude != "" { + assert.NotContains(t, formatted, tt.shouldExclude, + "FormatYAMLError should not contain %q\nResult: %s", tt.shouldExclude, formatted) + } + }) + } +} diff --git a/pkg/workflow/compiler_yaml_test.go b/pkg/workflow/compiler_yaml_test.go index 79e7792f8f2..69ac7c31885 100644 --- a/pkg/workflow/compiler_yaml_test.go +++ b/pkg/workflow/compiler_yaml_test.go @@ -66,7 +66,7 @@ strict: false Invalid YAML with bad mapping.`, expectedErrorLine: 7, // Line 7 in file (line 6 in YAML content after opening ---) expectedErrorColumn: 10, - expectedMessagePart: "Invalid YAML syntax: unexpected value", + expectedMessagePart: "unexpected ':'", description: "invalid mapping context should be detected", }, { @@ -85,7 +85,7 @@ strict: false Invalid YAML with bad indentation.`, expectedErrorLine: 4, // Line 4 in file (line 3 in YAML content after opening ---) expectedErrorColumn: 11, - expectedMessagePart: "Invalid YAML syntax: unexpected value", + expectedMessagePart: "unexpected ':'", description: "bad indentation should be detected", }, { @@ -169,7 +169,7 @@ strict: false Invalid YAML with missing colon.`, expectedErrorLine: 3, // Line 3 in file (line 2 in YAML content - permissions without colon) expectedErrorColumn: 1, - expectedMessagePart: "unexpected key name", + expectedMessagePart: "missing ':' after key", description: "missing colon in mapping should be detected", }, { @@ -365,7 +365,7 @@ engine: copilot Test content.`, expectedLineCol: "[3:10]", // Line 3 in file (line 2 in YAML content) - expectedInError: []string{"Invalid YAML syntax: unexpected value"}, + expectedInError: []string{"unexpected ':'"}, expectPointer: true, description: "simple syntax error shows formatted output", }, @@ -402,7 +402,7 @@ engine: copilot Test content.`, expectedLineCol: "[3:1]", // Line 3 in file (permissions without colon) - expectedInError: []string{"unexpected key name", "permissions"}, + expectedInError: []string{"missing ':' after key", "permissions"}, expectPointer: true, description: "missing colon shows formatted output", }, diff --git a/pkg/workflow/frontmatter_error.go b/pkg/workflow/frontmatter_error.go index d69946180ec..2d90c92a35d 100644 --- a/pkg/workflow/frontmatter_error.go +++ b/pkg/workflow/frontmatter_error.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/parser" ) var frontmatterErrorLog = logger.New("workflow:frontmatter_error") @@ -16,49 +17,6 @@ var ( sourceContextPattern = regexp.MustCompile(`\n(\s+\d+\s*\|)`) ) -// yamlErrorTranslations maps raw goccy/go-yaml internal messages to user-friendly plain English. -// These messages are parser internals that are not helpful to end users. -var yamlErrorTranslations = []struct { - pattern string - translation string -}{ - { - "non-map value is specified", - "Invalid YAML syntax: expected 'key: value' format (did you forget a colon after the key?)", - }, - { - "mapping values are not allowed", - "Invalid YAML syntax: unexpected ':' — check your indentation", - }, - { - "did not find expected", - "Invalid YAML syntax: check indentation or missing key", - }, - { - "mapping value is not allowed in this context", - "Invalid YAML syntax: unexpected value — did you forget a ':' after a key?", - }, - { - "could not find expected ':'", - "Invalid YAML syntax: missing ':' between key and value", - }, - { - "found character that cannot start any token", - "Invalid YAML syntax: invalid character — check indentation uses spaces, not tabs", - }, -} - -// translateYAMLMessage converts raw YAML parser messages to user-friendly plain English. -// This prevents internal library jargon from reaching the end user. -func translateYAMLMessage(message string) string { - for _, t := range yamlErrorTranslations { - if strings.Contains(message, t.pattern) { - return t.translation - } - } - return message -} - // findFrontmatterFieldLine searches frontmatterLines for a line whose first // non-space key matches fieldName (e.g., "engine") and returns the 1-based // document line number. frontmatterStart is the 1-based line number of the @@ -100,8 +58,9 @@ func (c *Compiler) createFrontmatterError(filePath, content string, err error, f if idx := strings.Index(message, "\n"); idx != -1 { message = message[:idx] } - // Translate raw YAML parser messages to user-friendly plain English - message = translateYAMLMessage(message) + // Translate raw YAML parser messages to user-friendly plain English. + // Uses the shared translation table from pkg/parser to keep both code paths in sync. + message = parser.TranslateYAMLMessage(message) // Format as: filename:line:column: error: message // This is compatible with VSCode's problem matcher diff --git a/pkg/workflow/yaml_message_translation_test.go b/pkg/workflow/yaml_message_translation_test.go index 033e628636d..d15036c78a8 100644 --- a/pkg/workflow/yaml_message_translation_test.go +++ b/pkg/workflow/yaml_message_translation_test.go @@ -5,34 +5,36 @@ package workflow import ( "testing" + "github.com/github/gh-aw/pkg/parser" "github.com/stretchr/testify/assert" ) -// TestTranslateYAMLMessage tests that raw goccy/go-yaml error messages are translated to plain English +// TestTranslateYAMLMessage tests that raw goccy/go-yaml error messages are translated to plain English. +// It exercises parser.TranslateYAMLMessage, the shared translation function used by both packages. func TestTranslateYAMLMessage(t *testing.T) { tests := []struct { name string input string wantNot []string // substrings that must NOT appear in output - wantAny []string // at least one of these must appear in output + wantAny []string // ALL of these must appear in output }{ { name: "non-map value translated to user-friendly message", input: "non-map value is specified", wantNot: []string{"non-map value is specified"}, - wantAny: []string{"Invalid YAML syntax", "key: value", "colon"}, + wantAny: []string{"key: value", "colon"}, }, { name: "mapping values not allowed translated", input: "mapping values are not allowed in this context", wantNot: []string{"mapping values are not allowed"}, - wantAny: []string{"Invalid YAML syntax", "indentation"}, + wantAny: []string{"indentation"}, }, { name: "did not find expected translated", input: "did not find expected key", - wantNot: []string{"did not find expected"}, - wantAny: []string{"Invalid YAML syntax"}, + wantNot: []string{"did not find expected key"}, + wantAny: []string{"indentation"}, }, { name: "unrecognized message returned unchanged", @@ -50,13 +52,13 @@ func TestTranslateYAMLMessage(t *testing.T) { name: "partially matching message translated", input: "[3:1] non-map value is specified as a key\n 2 | foo: bar\n> 3 | baz qux\n ^", wantNot: []string{"non-map value is specified"}, - wantAny: []string{"Invalid YAML syntax"}, + wantAny: []string{"key: value"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := translateYAMLMessage(tt.input) + result := parser.TranslateYAMLMessage(tt.input) for _, unwanted := range tt.wantNot { assert.NotContains(t, result, unwanted,