Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions cmd/identities/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package identities

import (
"errors"
"fmt"

"github.com/spf13/cobra"
Expand All @@ -26,7 +27,7 @@ func NewDeleteCmd() *cobra.Command {

func NewDeleteIdentityCmd() *cobra.Command {
return &cobra.Command{
Use: "identity id-0 [id-1] [id-2] [id-n]",
Use: "identity <id> [<id> ...]",
Short: "Delete one or more identities by their ID(s)",
Long: fmt.Sprintf(`This command deletes one or more identities by ID. To delete an identity by some selector, e.g. the recovery email address, use the list command in combination with jq.

Expand All @@ -43,13 +44,22 @@ func NewDeleteIdentityCmd() *cobra.Command {

var (
deleted = make([]cmdx.OutputIder, 0, len(args))
failed = make(map[string]error)
failed []string
)

for _, a := range args {
_, err := c.IdentityAPI.DeleteIdentity(cmd.Context(), a).Execute()
if err != nil {
failed[a] = cmdx.PrintOpenAPIError(cmd, err)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Failed to delete identity %s:\n", a)
// PrintOpenAPIError prints the message itself and returns the
// ErrNoPrintButFail sentinel when it could parse an API error body.
// For transport-level errors (no body, e.g. timeouts) it prints
// nothing and returns the original error instead, which we must
// still surface here.
if printErr := cmdx.PrintOpenAPIError(cmd, err); !errors.Is(printErr, cmdx.ErrNoPrintButFail) {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", printErr)
}
failed = append(failed, a)
Comment on lines +47 to +62

@coderabbitai coderabbitai Bot Aug 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f 'go\.(mod|sum)$' -x rg -n 'github.com/ory/x' {}
rg -n -C 12 'func PrintOpenAPIError|ErrNoPrintButFail' --glob '*.go' .

Repository: ory/kratos

Length of output: 394


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- delete command ---'
cat -n cmd/identities/delete.go | sed -n '1,110p'

printf '%s\n' '--- local cmdx implementation and sentinel ---'
rg -n -C 20 'func PrintOpenAPIError|ErrNoPrintButFail' oryx --glob '*.go'

printf '%s\n' '--- relevant tests and error handling ---'
rg -n -C 10 'DeleteIdentity|PrintOpenAPIError|ErrNoPrintButFail|Failed to delete identity' --glob '*_test.go' cmd oryx

Repository: ory/kratos

Length of output: 13887


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- complete PrintOpenAPIError implementation ---'
cat -n oryx/cmdx/printing.go | sed -n '322,380p'

printf '%s\n' '--- complete delete command tests ---'
cat -n cmd/identities/delete_test.go | sed -n '1,180p'

printf '%s\n' '--- client transport error paths ---'
rg -n -C 12 'HTTPClient\.Do|\\.Do\\(' --glob '*.go' . | head -240

printf '%s\n' '--- command test setup and injectable client configuration ---'
rg -n -C 10 'func setup|RegisterClientFlags|HTTPClient|NewClient' cmd/identities cmd/cliclient --glob '*.go' | head -320

Repository: ory/kratos

Length of output: 4405


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- generated client files and transport calls ---'
fd -t f -i 'client.go' . -x sh -c 'printf "%s\n" "$1"; rg -n -C 8 "HTTPClient|\\.Do\\(" "$1" || true' sh {}

printf '%s\n' '--- command test setup ---'
rg -n -C 12 'func setup|RegisterClientFlags|NewClient|HTTPClient' cmd/identities cmd/cliclient --glob '*.go' || true

printf '%s\n' '--- read-only verifier for PrintOpenAPIError control flow ---'
python3 - <<'PY'
from pathlib import Path

p = Path("oryx/cmdx/printing.go")
s = p.read_text()

required = [
    "if !errors.As(err, &be) {",
    "return err",
    "if didPrettyPrint {",
    "return FailSilently(cmd)",
    'json.MarshalIndent(json.RawMessage(body), "", "  ")',
]
missing = [x for x in required if x not in s]
if missing:
    raise SystemExit(f"missing source markers: {missing}")

no_body = s.index("if !errors.As(err, &be)")
no_body_return = s.index("return err", no_body)
pretty = s.index("if didPrettyPrint")
pretty_return = s.index("return FailSilently(cmd)", pretty)
marshal = s.index("json.MarshalIndent")
marshal_return = s.index("return FailSilently(cmd)", marshal)

assert no_body_return < pretty, "no-Body path must return before OpenAPI formatting"
assert pretty_return < marshal, "pretty-printed OpenAPI errors must fail silently"
assert marshal_return > marshal, "JSON-body path must fail silently after printing"

print("source control-flow checks passed")
print("no Body() => original error is returned")
print("recognized OpenAPI body => error is printed and ErrNoPrintButFail is returned")
PY

Repository: ory/kratos

Length of output: 24249


Preserve non-OpenAPI errors.

cmdx.PrintOpenAPIError returns transport and timeout errors when the client receives no response body. The command discards these errors and returns only cmdx.ErrNoPrintButFail.

Print each returned error unless errors.Is(printErr, cmdx.ErrNoPrintButFail) is true. Add a transport-error regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/identities/delete.go` around lines 46 - 54, Update the error handling in
the identity deletion loop to capture the error returned by
cmdx.PrintOpenAPIError and print it unless errors.Is(printErr,
cmdx.ErrNoPrintButFail) is true, preserving transport and timeout errors while
retaining the existing failure tracking. Add a regression test covering a
transport error with no response body and verify that the returned error is
printed.

Source: MCP tools

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks. Fixed: PrintOpenAPIError now has its return value checked against the ErrNoPrintButFail sentinel, and the original error is printed explicitly whenever it isn't that sentinel (i.e. transport-level errors that never had a body to pretty-print in the first place). Added a regression test that points the client at an unreachable endpoint to cover that path.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- delete command error handling ---'
sed -n '35,80p' cmd/identities/delete.go

printf '%s\n' '--- delete command test outline ---'
ast-grep outline cmd/identities/delete_test.go --items all --view expanded

printf '%s\n' '--- transport-error and error-output assertions ---'
rg -n -C 12 -i 'unreachable|transport|connection refused|PrintOpenAPIError|ErrNoPrintButFail|Failed to delete identity|never be printed' \
  cmd/identities/delete_test.go

Length of output: 5110


@klimavojtech2002, the implementation correctly preserves and prints non-sentinel errors.

The new transport-error test does not yet verify the transport error output. assert.NotEmpty passes when the command prints only Failed to delete identity <id>:.

Please assert a transport-specific value, such as 127.0.0.1:1, in stdErr. This assertion verifies that the error returned by cmdx.PrintOpenAPIError is printed.

You are interacting with an AI system.

continue
}
deleted = append(deleted, cmdx.OutputIder(a))
Expand All @@ -61,7 +71,6 @@ func NewDeleteIdentityCmd() *cobra.Command {
cmdx.PrintTable(cmd, &cmdx.OutputIderCollection{Items: deleted})
}

cmdx.PrintErrors(cmd, failed)
if len(failed) != 0 {
return cmdx.FailSilently(cmd)
}
Expand Down
77 changes: 76 additions & 1 deletion cmd/identities/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ package identities_test
import (
"context"
"errors"
"net/http"
"strings"
"testing"

"github.com/spf13/cobra"
"github.com/tidwall/gjson"

"github.com/ory/kratos/cmd/cliclient"
"github.com/ory/kratos/cmd/identities"

"github.com/stretchr/testify/assert"
Expand All @@ -19,6 +22,7 @@ import (
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/x"
"github.com/ory/x/cmdx"
"github.com/ory/x/sqlcon"
)

Expand Down Expand Up @@ -54,8 +58,79 @@ func TestDeleteCmd(t *testing.T) {
})

t.Run("case=fails with unknown ID", func(t *testing.T) {
stdErr := cmd.ExecExpectedErr(t, x.NewUUID().String())
unknownID := x.NewUUID().String()
stdErr := cmd.ExecExpectedErr(t, unknownID)

assert.Contains(t, stdErr, "Unable to locate the resource", stdErr)
assert.Contains(t, stdErr, unknownID, stdErr)
assert.NotContains(t, stdErr, "this error should never be printed", stdErr)
})

t.Run("case=fails with mix of known and unknown IDs", func(t *testing.T) {
is, ids := makeIdentities(t, reg, 1)
unknownID := x.NewUUID().String()

// the unknown ID goes first: a regression that returns on the first
// error instead of continuing the loop would still delete ids[0] and
// pass if it were listed first, so this ordering is what actually
// proves deletion continues past a failure.
stdOut, stdErr, err := cmd.Exec(nil, unknownID, ids[0])
require.Error(t, err)

assert.Contains(t, stdOut, ids[0], stdOut)
assert.NotContains(t, stdOut, unknownID, stdOut)
assert.Contains(t, stdErr, unknownID, stdErr)
assert.NotContains(t, stdErr, "this error should never be printed", stdErr)

// the known identity should still have been deleted despite the other ID failing
_, err = reg.Persister().GetIdentity(context.Background(), is[0].ID, identity.ExpandNothing)
assert.True(t, errors.Is(err, sqlcon.ErrNoRows()))
})

t.Run("case=surfaces transport errors instead of the sentinel", func(t *testing.T) {
// PrintOpenAPIError only pretty-prints and returns the ErrNoPrintButFail
// sentinel for errors that carry a parsed API response body. A transport
// failure has no body, so PrintOpenAPIError returns the original error
// unprinted; the command must still surface it instead of discarding it.
//
// The failure is injected via a RoundTripper returning a known error,
// rather than dialing a real port, so the test is hermetic and asserts
// on a value we control instead of an OS-dependent dial error string.
transportErr := errors.New("simulated transport failure")
ctx := context.WithValue(context.Background(), cliclient.ClientContextKey,
func(cmd *cobra.Command) (*cliclient.ClientContext, error) {
return &cliclient.ClientContext{
Endpoint: "http://localhost",
HTTPClient: &http.Client{Transport: failingRoundTripper{err: transportErr}},
}, nil
})

unreachable := &cmdx.CommandExecuter{
Ctx: ctx,
New: func() *cobra.Command {
c := identities.NewDeleteIdentityCmd()
cliclient.RegisterClientFlags(c.Flags())
cmdx.RegisterFormatFlags(c.Flags())
return c
},
PersistentArgs: []string{"--" + cmdx.FlagFormat, string(cmdx.FormatJSON)},
}

stdErr := unreachable.ExecExpectedErr(t, x.NewUUID().String())

assert.NotContains(t, stdErr, "this error should never be printed", stdErr)
// asserts on the actual injected error, not just "something got
// printed" - the bug this guards against prints only the "Failed to
// delete identity <id>:" header and silently drops the real error,
// which would still make a weaker "output is non-empty" check pass.
assert.Contains(t, stdErr, transportErr.Error(), stdErr)
})
}

// failingRoundTripper always fails with a known error, standing in for a real
// transport failure (e.g. a timeout) without depending on real network I/O.
type failingRoundTripper struct{ err error }

func (rt failingRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
return nil, rt.err
}
Loading