Repository: ratify-project/ratify
Pull request #1675 (2024-07-31 08:28:54)

feat: refactor error message format
# Description

## What this PR does / why we need it:
This PR will refactor the message format returned by Error(). The new format is following the proposed improvement from https://github.com/ratify-project/ratify/pull/1662

Note: this pr is based on https://github.com/ratify-project/ratify/pull/1671, please check the actual diff at: https://github.com/binbin-li/ratify/pull/209

In general, for a nested Ratify error, we'll return a message in format:
`<Error Code>: <Error Description>: <Error Reason>: <Remediation>`,
where `Error Code` is the error code of the root error,
`Error Description` is concatenated details of all nested errors,
`Error Reason` is the detail or original error of the root error,
and `Remediation` is the remediation of the root error.

Let's take the below nested error as example:
```
{
  Code: "TOP_ERR_CODE",
  Detail: "top error detail",
  Remediation: "top error remediation",
  OriginalError: {
    Code: "ROOT_ERR_CODE",
    Detail: "root error detail",
    Remediation: "root error remediation",
    OriginalError: "http request failed with 401"
  }
}
```
Then the `Error()` method should return a string: `ROOT_ERR_CODE: top error detail: root error detail: http request failed with 401: root error remediation`.
`GetDetail()` should return string: `top error detail: root error detail`.
`GetErrorReason()` should return string: `http request failed with 401`.
`GetRemeidation()` should return string: `root error remediation`.

Therefore, we would stop printing all fields from the Error. In this PR, we keep all existing fields under Error struct. And if the new error format looks good in the following releases, we'll start remove those unused fields gradually.

Examples:
Error log before change:
`time=2024-07-31T14:04:45.027923414Z level=error msg=Original Error: (Original Error: (Original Error: (json: cannot unmarshal number into Go struct field azureWIAuthProviderConf.clientID of type string), Error: config invalid, Code: CONFIG_INVALID, Component Type: authProvider, Detail: failed to parse auth provider configuration), Error: auth denied, Code: AUTH_DENIED, Component Type: authProvider), Error: plugin init failure, Code: PLUGIN_INIT_FAILURE, Component Type: referrerStore, Detail: failed to create auth provider from configuration
`

`time=2024-07-31T14:02:52.767626288Z level=error msg=Original Error: (failed to find plugin "notation22" in paths [/.ratify/plugins]), Error: plugin not found, Code: PLUGIN_NOT_FOUND, Component Type: verifier, Detail: plugin not found`

Error log after change:
`time=2024-07-31T13:48:09.976909601Z level=error msg=CONFIG_INVALID: failed to create auth provider from configuration: failed to parse auth provider configuration: json: cannot unmarshal number into Go struct field azureWIAuthProviderConf.clientID of type string`

`time=2024-07-31T13:57:55.837222446Z level=error msg=PLUGIN_NOT_FOUND: plugin not found: failed to find plugin "notation22" in paths [/.ratify/plugins]`

## Which issue(s) this PR fixes *(optional, using `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when the PR gets merged)*:
Fixes: https://github.com/ratify-project/ratify/issues/1655

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Helm Chart Change (any edit/addition/update that is necessary for changes merged to the `main` branch)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Please also list any relevant details for your test configuration

- [ ] Test A
- [ ] Test B

# Checklist:

- [ ] Does the affected code have corresponding tests?
- [ ] Are the changes documented, not just with inline documentation, but also with conceptual documentation such as an overview of a new feature, or task-based documentation like a tutorial? Consider if this change should be announced on your project blog.
- [ ] Does this introduce breaking changes that would require an announcement or bumping the major version?
- [ ] Do all new files have appropriate license header?

# Post Merge Requirements
- [ ] MAINTAINERS: manually trigger the "Publish Package" workflow after merging any PR that indicates `Helm Chart Change`


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project) Report
Attention: Patch coverage is `98.03922%` with `2 lines` in your changes missing coverage. Please review.

| [Files](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project) | Patch % | Lines |
|---|---|---|
| [errors/types.go](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?src=pr&el=tree&filepath=errors%2Ftypes.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project#diff-ZXJyb3JzL3R5cGVzLmdv) | 95.23% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project) |

| [Files](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project) | Coverage Δ | |
|---|---|---|
| [httpserver/types.go](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?src=pr&el=tree&filepath=httpserver%2Ftypes.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project#diff-aHR0cHNlcnZlci90eXBlcy5nbw==) | `100.00% <100.00%> (ø)` | |
| [pkg/executor/core/executor.go](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?src=pr&el=tree&filepath=pkg%2Fexecutor%2Fcore%2Fexecutor.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project#diff-cGtnL2V4ZWN1dG9yL2NvcmUvZXhlY3V0b3IuZ28=) | `89.15% <100.00%> (+0.33%)` | :arrow_up: |
| [pkg/verifier/cosign/cosign.go](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?src=pr&el=tree&filepath=pkg%2Fverifier%2Fcosign%2Fcosign.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project#diff-cGtnL3ZlcmlmaWVyL2Nvc2lnbi9jb3NpZ24uZ28=) | `78.55% <100.00%> (+0.44%)` | :arrow_up: |
| [pkg/verifier/notation/notation.go](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?src=pr&el=tree&filepath=pkg%2Fverifier%2Fnotation%2Fnotation.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project#diff-cGtnL3ZlcmlmaWVyL25vdGF0aW9uL25vdGF0aW9uLmdv) | `88.11% <100.00%> (+0.24%)` | :arrow_up: |
| [pkg/verifier/types/types.go](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?src=pr&el=tree&filepath=pkg%2Fverifier%2Ftypes%2Ftypes.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project#diff-cGtnL3ZlcmlmaWVyL3R5cGVzL3R5cGVzLmdv) | `92.30% <100.00%> (+2.83%)` | :arrow_up: |
| [errors/types.go](https://app.codecov.io/gh/ratify-project/ratify/pull/1675?src=pr&el=tree&filepath=errors%2Ftypes.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratify-project#diff-ZXJyb3JzL3R5cGVzLmdv) | `90.47% <95.23%> (+1.58%)` | :arrow_up: |


Patch:
diff --git a/errors/types.go b/errors/types.go
index 3a211c8..cb5e031 100644
--- a/errors/types.go
+++ b/errors/types.go
@@ -60,15 +60,16 @@ type ErrorCode int
 
 // Error provides a wrapper around ErrorCode with extra Details provided.
 type Error struct {
-	OriginalError error         `json:"originalError,omitempty"`
-	Code          ErrorCode     `json:"code"`
-	Message       string        `json:"message"`
-	Detail        interface{}   `json:"detail,omitempty"`
-	ComponentType ComponentType `json:"componentType,omitempty"`
-	PluginName    string        `json:"pluginName,omitempty"`
-	LinkToDoc     string        `json:"linkToDoc,omitempty"`
-	Stack         string        `json:"stack,omitempty"`
-	Description   string        `json:"description,omitempty"`
+	originalError error
+	code          ErrorCode
+	message       string
+	detail        interface{}
+	componentType ComponentType
+	remediation   string
+	pluginName    string
+	stack         string
+	description   string
+	isRootError   bool // isRootError is true if the originalError is either nil or not an Error type
 }
 
 // ErrorDescriptor provides relevant information about a given error code.
@@ -150,9 +151,9 @@ func (ec ErrorCode) WithComponentType(componentType ComponentType) Error {
 	return newError(ec, ec.Message()).WithComponentType(componentType)
 }
 
-// WithLinkToDoc returns a new Error object with attached link to the documentation.
-func (ec ErrorCode) WithLinkToDoc(link string) Error {
-	return newError(ec, ec.Message()).WithLinkToDoc(link)
+// WithRemediation returns a new Error object with remediation.
+func (ec ErrorCode) WithRemediation(link string) Error {
+	return newError(ec, ec.Message()).WithRemediation(link)
 }
 
 func (ec ErrorCode) WithDescription() Error {
@@ -165,27 +166,29 @@ func (ec ErrorCode) WithPluginName(pluginName string) Error {
 }
 
 // NewError returns a new Error object.
-func (ec ErrorCode) NewError(componentType ComponentType, pluginName, link string, err error, detail interface{}, printStackTrace bool) Error {
+func (ec ErrorCode) NewError(componentType ComponentType, pluginName, remediation string, err error, detail interface{}, printStackTrace bool) Error {
 	stack := ""
 	if printStackTrace {
 		stack = getStackTrace()
 	}
 	return Error{
-		Code:          ec,
-		Message:       ec.Message(),
-		OriginalError: err,
-		Detail:        detail,
-		ComponentType: componentType,
-		PluginName:    pluginName,
-		LinkToDoc:     link,
-		Stack:         stack,
+		code:          ec,
+		message:       ec.Message(),
+		originalError: err,
+		detail:        detail,
+		componentType: componentType,
+		pluginName:    pluginName,
+		remediation:   remediation,
+		stack:         stack,
+		isRootError:   err == nil || !errors.As(err, &Error{}),
 	}
 }
 
 func newError(code ErrorCode, message string) Error {
 	return Error{
-		Code:    code,
-		Message: message,
+		code:        code,
+		message:     message,
+		isRootError: true,
 	}
 }
 
@@ -193,90 +196,114 @@ func newError(code ErrorCode, message string) Error {
 func (e Error) Is(target error) bool {
 	t := &Error{}
 	if errors.As(target, t) {
-		return e.Code.ErrorCode() == t.Code.ErrorCode()
+		return e.code.ErrorCode() == t.code.ErrorCode()
 	}
 	return false
 }
 
 // ErrorCode returns the ID/Value of this Error
 func (e Error) ErrorCode() ErrorCode {
-	return e.Code
+	return e.code
 }
 
 // Unwrap returns the original error
 func (e Error) Unwrap() error {
-	return e.OriginalError
+	return e.originalError
 }
 
 // Error returns a human readable representation of the error.
+// An Error message includes the error code, detail from nested errors, root cause and remediation, all separated by ": ".
 func (e Error) Error() string {
-	var errStr string
-	if e.OriginalError != nil {
-		errStr += fmt.Sprintf("Original Error: (%s), ", e.OriginalError.Error())
+	err, details := e.getRootError()
+	if err.detail != nil {
+		details = append(details, fmt.Sprintf("%s", err.detail))
 	}
-
-	errStr += fmt.Sprintf("Error: %s, Code: %s", e.Message, e.Code.String())
-
-	if e.PluginName != "" {
-		errStr += fmt.Sprintf(", Plugin Name: %s", e.PluginName)
+	if err.originalError != nil {
+		details = append(details, err.originalError.Error())
 	}
 
-	if e.ComponentType != "" {
-		errStr += fmt.Sprintf(", Component Type: %s", e.ComponentType)
+	if err.remediation != "" {
+		details = append(details, err.remediation)
 	}
+	return fmt.Sprintf("%s: %s", err.ErrorCode().Descriptor().Value, strings.Join(details, ": "))
+}
 
-	if e.LinkToDoc != "" {
-		errStr += fmt.Sprintf(", Documentation: %s", e.LinkToDoc)
+// GetDetail returns details from all nested errors.
+func (e Error) GetDetail() string {
+	err, details := e.getRootError()
+	if err.originalError != nil && err.detail != nil {
+		details = append(details, fmt.Sprintf("%s", err.detail))
 	}
 
-	if e.Detail != nil {
-		errStr += fmt.Sprintf(", Detail: %v", e.Detail)
-	}
+	return strings.Join(details, ": ")
+}
 
-	if e.Description != "" {
-		errStr += fmt.Sprintf(", Description: %v", e.Description)
+// GetErrorReason returns the root cause of the error.
+func (e Error) GetErrorReason() string {
+	err, _ := e.getRootError()
+	if err.originalError != nil {
+		return err.originalError.Error()
 	}
+	return fmt.Sprintf("%s", err.detail)
+}
 
-	if e.Stack != "" {
-		errStr += fmt.Sprintf(", Stack trace: %s", e.Stack)
-	}
+// GetRemiation returns the remediation of the root error.
+func (e Error) GetRemediation() string {
+	err, _ := e.getRootError()
+	return err.remediation
+}
 
-	return errStr
+func (e Error) getRootError() (err Error, details []string) {
+	err = e
+	for !err.isRootError {
+		if err.detail != nil {
+			details = append(details, fmt.Sprintf("%s", err.detail))
+		}
+		var ratifyError Error
+		if errors.As(err.originalError, &ratifyError) {
+			err = ratifyError
+		} else {
+			// break is unnecessary, but added for safety
+			break
+		}
+	}
+	return err, details
 }
 
 // WithDetail will return a new Error, based on the current one, but with
 // some Detail info added
 func (e Error) WithDetail(detail interface{}) Error {
-	e.Detail = detail
+	e.detail = detail
 	return e
 }
 
 // WithPluginName returns a new Error object with pluginName set.
 func (e Error) WithPluginName(pluginName string) Error {
-	e.PluginName = pluginName
+	e.pluginName = pluginName
 	return e
 }
 
 // WithComponentType returns a new Error object with ComponentType set.
 func (e Error) WithComponentType(componentType ComponentType) Error {
-	e.ComponentType = componentType
+	e.componentType = componentType
 	return e
 }
 
 // WithError returns a new Error object with original error.
 func (e Error) WithError(err error) Error {
-	e.OriginalError = err
+	e.originalError = err
+	e.isRootError = err == nil || !errors.As(err, &Error{})
 	return e
 }
 
-// WithLinkToDoc returns a new Error object attached with link to documentation.
-func (e Error) WithLinkToDoc(link string) Error {
-	e.LinkToDoc = link
+// WithRemediation returns a new Error object attached with remediation.
+func (e Error) WithRemediation(remediation string) Error {
+	e.remediation = remediation
 	return e
 }
 
 func (e Error) WithDescription() Error {
-	e.Description = e.Code.Description()
+	e.description = e.code.Description()
 	return e
 }
 


Test patch:
diff --git a/cmd/ratify/cmd/cmd_test.go b/cmd/ratify/cmd/cmd_test.go
index 49149dc..9016198 100644
--- a/cmd/ratify/cmd/cmd_test.go
+++ b/cmd/ratify/cmd/cmd_test.go
@@ -50,8 +50,8 @@ func TestDiscover(t *testing.T) {
 
 	// TODO: make ratify cli more unit testable
 	// unit test should not need to resolve real image
-	if !strings.Contains(err.Error(), "referrer store failure") {
-		t.Errorf("error expected")
+	if !strings.Contains(err.Error(), "REFERRER_STORE_FAILURE") {
+		t.Errorf("expected containing: %s, but got: %s", "REFERRER_STORE_FAILURE", err.Error())
 	}
 }
 
diff --git a/errors/types_test.go b/errors/types_test.go
index 5e97400..a1f66e3 100644
--- a/errors/types_test.go
+++ b/errors/types_test.go
@@ -22,26 +22,31 @@ import (
 )
 
 const (
-	testGroup         = "test-group"
-	testValue         = "test-value"
-	testMessage       = "test-message"
-	testDescription   = "test-description"
-	testDetail        = "test-detail"
-	testComponentType = "test-component-type"
-	testLink          = "test-link"
-	testPluginName    = "test-plugin-name"
-	testErrorString   = "Original Error: (Error: , Code: UNKNOWN), Error: test-message, Code: test-value, Plugin Name: test-plugin-name, Component Type: test-component-type, Documentation: test-link, Detail: test-detail"
-	nonexistentEC     = 2000
+	testGroup          = "test-group"
+	testErrCode1       = "TEST_ERROR_CODE_1"
+	testErrCode2       = "TEST_ERROR_CODE_2"
+	testMessage        = "test-message"
+	testDescription    = "test-description"
+	testDetail1        = "test-detail-1"
+	testDetail2        = "test-detail-2"
+	testComponentType1 = "test-component-type-1"
+	testComponentType2 = "test-component-type-2"
+	testLink1          = "test-link-1"
+	testLink2          = "test-link-2"
+	testPluginName     = "test-plugin-name"
+	nonexistentEC      = 2000
 )
 
 var (
 	testEC = Register(testGroup, ErrorDescriptor{
-		Value:       testValue,
+		Value:       testErrCode1,
 		Message:     testMessage,
 		Description: testDescription,
 	})
 
-	testEC2 = Register(testGroup, ErrorDescriptor{})
+	testEC2 = Register(testGroup, ErrorDescriptor{
+		Value: testErrCode2,
+	})
 )
 
 func TestErrorCode(t *testing.T) {
@@ -52,8 +57,9 @@ func TestErrorCode(t *testing.T) {
 }
 
 func TestError(t *testing.T) {
-	if testEC.Error() != testValue {
-		t.Fatalf("expected: %s, got: %s", testValue, testEC.Error())
+	expectedStr := "test error code 1"
+	if testEC.Error() != expectedStr {
+		t.Fatalf("expected: %s, got: %s", expectedStr, testEC.Error())
 	}
 }
 
@@ -66,7 +72,7 @@ func TestDescriptor(t *testing.T) {
 		{
 			name:          "existing error code",
 			ec:            testEC,
-			expectedValue: testValue,
+			expectedValue: testErrCode1,
 		},
 		{
 			name:          "nonexistent error code",
@@ -92,9 +98,9 @@ func TestMessage(t *testing.T) {
 }
 
 func TestWithDetail(t *testing.T) {
-	err := testEC.WithDetail(testDetail)
-	if err.Detail != testDetail {
-		t.Fatalf("expected detail: %s, got: %s", testDetail, err.Detail)
+	err := testEC.WithDetail(testDetail1)
+	if err.detail != testDetail1 {
+		t.Fatalf("expected detail: %s, got: %s", testDetail1, err.detail)
 	}
 }
 
@@ -106,35 +112,35 @@ func TestWithError(t *testing.T) {
 }
 
 func TestWithComponentType(t *testing.T) {
-	err := testEC.WithComponentType(testComponentType)
-	if err.ComponentType != testComponentType {
-		t.Fatalf("expected component type: %s, got: %s", testComponentType, err.ComponentType)
+	err := testEC.WithComponentType(testComponentType1)
+	if err.componentType != testComponentType1 {
+		t.Fatalf("expected component type: %s, got: %s", testComponentType1, err.componentType)
 	}
 }
 
-func TestWithLinkToDoc(t *testing.T) {
-	err := testEC.WithLinkToDoc(testLink)
-	if err.LinkToDoc != testLink {
-		t.Fatalf("expected link to doc: %s, got: %s", testLink, err.LinkToDoc)
+func TestWithRemediation(t *testing.T) {
+	err := testEC.WithRemediation(testLink1)
+	if err.remediation != testLink1 {
+		t.Fatalf("expected remediation: %s, got: %s", testLink1, err.remediation)
 	}
 }
 
 func TestWithPluginName(t *testing.T) {
 	err := testEC.WithPluginName(testPluginName)
-	if err.PluginName != testPluginName {
-		t.Fatalf("expected plugin name: %s, got: %s", testPluginName, err.PluginName)
+	if err.pluginName != testPluginName {
+		t.Fatalf("expected plugin name: %s, got: %s", testPluginName, err.pluginName)
 	}
 }
 
 func TestWithDescription(t *testing.T) {
 	err := testEC.WithDescription()
-	if err.Description != testDescription {
-		t.Fatalf("expected description: %s, got: %s", testDescription, err.Description)
+	if err.description != testDescription {
+		t.Fatalf("expected description: %s, got: %s", testDescription, err.description)
 	}
 }
 
 func TestIs(t *testing.T) {
-	err := testEC.WithDetail(testDetail)
+	err := testEC.WithDetail(testDetail1)
 	result := err.Is(err)
 	if !result {
 		t.Fatalf("expected true, got: %v", result)
@@ -149,7 +155,7 @@ func TestIs(t *testing.T) {
 
 func TestError_ErrorCode(t *testing.T) {
 	err := Error{
-		Code: 1,
+		code: 1,
 	}
 	if err.ErrorCode() != 1 {
 		t.Fatalf("expected 1, got: %d", err.ErrorCode())
@@ -168,17 +174,64 @@ func TestIsEmpty(t *testing.T) {
 }
 
 func TestError_Error(t *testing.T) {
-	err := testEC.WithPluginName(testPluginName).WithComponentType(testComponentType).WithLinkToDoc(testLink).WithDetail(testDetail).WithError(Error{}).WithDescription()
-	result := err.Error()
-	if !strings.HasPrefix(result, testErrorString) {
-		t.Fatalf("expected string starts with: %s, but got: %s", testErrorString, result)
+	// Nested errors.
+	rootErr := testEC.NewError(testComponentType1, "", testLink1, errors.New(testMessage), testDetail1, false)
+	err := testEC2.WithPluginName(testPluginName).WithComponentType(testComponentType2).WithRemediation(testLink2).WithDetail(testDetail2).WithError(rootErr)
+
+	expectedErrStr := strings.Join([]string{testErrCode1, testDetail2, testDetail1, testMessage, testLink1}, ": ")
+	if err.Error() != expectedErrStr {
+		t.Fatalf("expected string: %s, but got: %s", expectedErrStr, err.Error())
+	}
+
+	// Single error.
+	err = testEC.WithDetail(testDetail1)
+	expectedErrStr = "TEST_ERROR_CODE_1: test-detail-1"
+	if err.Error() != expectedErrStr {
+		t.Fatalf("expected string: %s, but got: %s", expectedErrStr, err.Error())
+	}
+}
+
+func TestError_GetRootCause(t *testing.T) {
+	// rootErr contains original error.
+	rootErr := testEC.NewError(testComponentType1, "", testLink1, errors.New(testMessage), testDetail1, false)
+	err := testEC.WithPluginName(testPluginName).WithComponentType(testComponentType2).WithRemediation(testLink2).WithDetail(testDetail2).WithError(rootErr)
+
+	if err.GetErrorReason() != testMessage {
+		t.Fatalf("expected root cause: %v, but got: %v", err.GetErrorReason(), testMessage)
+	}
+
+	// rootErr does not contain original error.
+	rootErr = testEC.NewError(testComponentType1, "", testLink1, nil, testDetail1, false)
+	err = testEC.WithPluginName(testPluginName).WithComponentType(testComponentType2).WithRemediation(testLink2).WithDetail(testDetail2).WithError(rootErr)
+
+	if err.GetErrorReason() != testDetail1 {
+		t.Fatalf("expected root cause: %v, but got: %v", err.GetErrorReason(), testDetail1)
+	}
+}
+
+func TestError_GetFullDetails(t *testing.T) {
+	rootErr := testEC.NewError(testComponentType1, "", testLink1, errors.New(testMessage), testDetail1, false)
+	err := testEC.WithPluginName(testPluginName).WithComponentType(testComponentType2).WithRemediation(testLink2).WithDetail(testDetail2).WithError(rootErr)
+
+	expectedDetails := strings.Join([]string{testDetail2, testDetail1}, ": ")
+	if err.GetDetail() != expectedDetails {
+		t.Fatalf("expected full details: %v, but got: %v", expectedDetails, err.GetDetail())
+	}
+}
+
+func TestError_GetRootRemediation(t *testing.T) {
+	rootErr := testEC.NewError(testComponentType1, "", testLink1, errors.New(testMessage), testDetail1, false)
+	err := testEC.WithPluginName(testPluginName).WithComponentType(testComponentType2).WithRemediation(testLink2).WithDetail(testDetail2).WithError(rootErr)
+
+	if err.GetRemediation() != testLink1 {
+		t.Fatalf("expected root remediation: %v, but got: %v", err.GetRemediation(), testLink1)
 	}
 }
 
 func TestNewError(t *testing.T) {
-	err := testEC.NewError(testComponentType, testPluginName, testLink, Error{}, testDetail, false)
+	err := testEC.NewError(testComponentType1, testPluginName, testLink1, Error{}, testDetail1, false)
 
-	if err.ComponentType != testComponentType || err.PluginName != testPluginName || err.LinkToDoc != testLink || err.Detail != testDetail {
-		t.Fatalf("expected component type: %s, plugin name: %s, link to doc: %s, detail: %s, but got: %s, %s, %s, %s", testComponentType, testPluginName, testLink, testDetail, err.ComponentType, err.PluginName, err.LinkToDoc, err.Detail)
+	if err.componentType != testComponentType1 || err.pluginName != testPluginName || err.remediation != testLink1 || err.detail != testDetail1 {
+		t.Fatalf("expected component type: %s, plugin name: %s, link to doc: %s, detail: %s, but got: %s, %s, %s, %s", testComponentType1, testPluginName, testLink1, testDetail1, err.componentType, err.pluginName, err.remediation, err.detail)
 	}
 }
diff --git a/test/bats/plugin-test.bats b/test/bats/plugin-test.bats
index 3d37bbd..0cc9dea 100644
--- a/test/bats/plugin-test.bats
+++ b/test/bats/plugin-test.bats
@@ -312,7 +312,7 @@ RATIFY_NAMESPACE=gatekeeper-system
     sed 's/licensechecker/invalidlicensechecker/' ./config/samples/clustered/verifier/config_v1beta1_verifier_complete_licensechecker.yaml >invalidVerifier.yaml
     run kubectl apply -f invalidVerifier.yaml
     assert_success
-    run bash -c "kubectl describe verifiers.config.ratify.deislabs.io/verifier-license-checker -n ${RATIFY_NAMESPACE} | grep 'Brieferror:  Original Error:'"
+    run bash -c "kubectl describe verifiers.config.ratify.deislabs.io/verifier-license-checker -n ${RATIFY_NAMESPACE} | grep 'Brieferror:  PLUGIN_NOT_FOUND:'"
     assert_success
 
     # apply a valid verifier, validate status property shows success