Skip to content

Fix mail rule reorder request IDs - #2166

Open
yangr-happy wants to merge 1 commit into
larksuite:mainfrom
yangr-happy:feat/960cc5c
Open

Fix mail rule reorder request IDs#2166
yangr-happy wants to merge 1 commit into
larksuite:mainfrom
yangr-happy:feat/960cc5c

Conversation

@yangr-happy

@yangr-happy yangr-happy commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes mail rule reordering so the CLI sends the full ordered rule ID list expected by the mail API.

  • Fetches the current rules before applying a reorder.
  • Expands the provided subset into the complete final order.
  • Rejects duplicate or unknown rule IDs before submitting the request.
  • Adds service tests for successful reorder, duplicate IDs, and unknown IDs.

Summary by CodeRabbit

  • New Features

    • Mailbox rules can now be reordered using a partial list of rule IDs; any omitted rules are automatically retained and placed afterward.
    • Rule ordering continues to work across large mailboxes with many rules.
  • Bug Fixes

    • Improved validation prevents duplicate, unknown, or empty rule IDs from being submitted.
    • Reordering is no longer attempted when existing rule information cannot be retrieved successfully.

Fetch the current mail rule list before reorder and expand partial rule_ids into the complete order expected by the API. Reject duplicate or unknown rule IDs before submitting the write request.

Test: go test ./cmd/service

Co-authored-by: TRAE CLI <noreply@bytedance.com>
@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Mailbox rule reorder

Layer / File(s) Summary
Reorder request preparation
cmd/service/service.go
The reorder hook resolves the mailbox ID and validates the request object and rule_ids values.
Rule listing and order completion
cmd/service/service.go
The service retrieves paginated rule IDs, parses supported response shapes, rejects invalid sets, and appends omitted rules.
Reorder workflow tests
cmd/service/service_test.go
Tests cover partial and complete ordering, pagination, invalid IDs, empty rule sets, and list failures before reorder execution.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: liangshuo-1

Sequence Diagram(s)

sequenceDiagram
  participant MailRulesReorderHook
  participant mail_rules_list
  participant mail_user_mailbox_rules_reorder
  MailRulesReorderHook->>MailRulesReorderHook: resolve mailbox ID and validate rule IDs
  MailRulesReorderHook->>mail_rules_list: fetch current rule IDs
  mail_rules_list-->>MailRulesReorderHook: return paginated rule data
  MailRulesReorderHook->>mail_user_mailbox_rules_reorder: submit completed rule ordering
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: fixing mail rule reorder request IDs.
Description check ✅ Passed The description explains the motivation, main changes, and tests, but it omits the explicit Test Plan and Related Issues sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

PR Quality Summary

CI did not complete successfully. Use the failed check links below to decide whether this PR needs a code change or a rerun.

Failed checks

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@b47bee3bc03e8c8b8af411e8df7d368cb91da340

🧩 Skill update

npx skills add yangr-happy/cli#feat/960cc5c -y -g

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmd/service/service.go (1)

405-444: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

--dry-run previews an incomplete rule_ids list for mail rules reorder.

The mail.user_mailbox.rules.reorder completion hook runs at Line 439, after the opts.DryRun early return at Line 405. For this method, --dry-run prints the CLI-supplied rule_ids subset, not the completed ordered list the API call would actually send. This defeats the purpose of --dry-run as a preview for a write operation, and directly undercuts this PR's goal of sending the complete ordered list.

Move the schema-specific completion call before the opts.DryRun check, so the previewed request matches what is actually sent:

🐛 Proposed fix to complete the request before dry-run/confirmation
+	if opts.SchemaPath == "mail.user_mailbox.rules.reorder" {
+		if err := completeMailRulesReorderRequest(opts.Ctx, ac, opts, &request); err != nil {
+			return err
+		}
+	}
+
 	if opts.DryRun {
 		if fileMeta != nil {
 			return cmdutil.PrintDryRunWithFile(f.IOStreams.Out, request, config, opts.Format, fileMeta.FieldName, fileMeta.FilePath, fileMeta.FormFields)
 		}
 		return serviceDryRun(f, request, config, opts.Format)
 	}
 
 	if opts.Method.Risk == cmdutil.RiskHighRiskWrite {
 		if yes, _ := opts.Cmd.Flags().GetBool("yes"); !yes {
 			return cmdutil.RequireConfirmation(opts.SchemaPath)
 		}
 	}
 
 	ac, err := f.NewAPIClientWithConfig(config)
 	if err != nil {
 		return err
 	}
 
 	out := f.IOStreams.Out
 	format, formatOK := output.ParseFormat(opts.Format)
 	if !formatOK {
 		fmt.Fprintf(f.IOStreams.ErrOut, "warning: unknown format %q, falling back to json\n", opts.Format)
 	}
 
 	checkErr := ac.CheckResponse
 
 	if opts.PageAll {
 		return servicePaginate(opts.Ctx, ac, request, format, opts.JqExpr, out, f.IOStreams.ErrOut, opts.Cmd.CommandPath(),
 			client.PaginationOptions{PageLimit: opts.PageLimit, PageDelay: opts.PageDelay}, checkErr)
 	}
 
-	if opts.SchemaPath == "mail.user_mailbox.rules.reorder" {
-		if err := completeMailRulesReorderRequest(opts.Ctx, ac, opts, &request); err != nil {
-			return err
-		}
-	}
-
 	resp, err := ac.DoAPI(opts.Ctx, request)

Note this requires moving the ac construction earlier too, since completeMailRulesReorderRequest needs ac to fetch current rules. Confirm that issuing this read-only GET call during --dry-run is acceptable for this command, since it is a real network call rather than a pure local preview.

🤖 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/service/service.go` around lines 405 - 444, Move API client creation and
the mail-specific completion call for completeMailRulesReorderRequest before the
opts.DryRun early return in the service command flow. This ensures --dry-run
fetches and previews the completed rule_ids list, matching the request sent
during execution; preserve existing error handling and confirm the required
read-only GET is performed during dry-run.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@cmd/service/service_test.go`:
- Around line 482-504: Strengthen
TestMailRulesReorderListFailureDoesNotCallReorder by registering a POST stub for
the reorder endpoint with OnMatch setting a reorderCalled flag, then assert the
flag remains false after Execute returns an error. This must verify the reorder
request is skipped rather than only confirming that some error occurred.

In `@cmd/service/service.go`:
- Around line 808-826: The stringSliceField helper validates trimmed string
entries but stores their raw values, causing rule ID comparisons and duplicate
detection to disagree with normalized IDs. In stringSliceField, trim each
non-empty string and append the trimmed value to out, while preserving the
existing validation and error behavior.
- Around line 692-699: Update completeMailRulesReorderRequest to explicitly
check that body contains the rule_ids key before calling stringSliceField.
Return the existing typed validation error for a missing field, while preserving
the current handling of an explicitly provided empty array.

---

Outside diff comments:
In `@cmd/service/service.go`:
- Around line 405-444: Move API client creation and the mail-specific completion
call for completeMailRulesReorderRequest before the opts.DryRun early return in
the service command flow. This ensures --dry-run fetches and previews the
completed rule_ids list, matching the request sent during execution; preserve
existing error handling and confirm the required read-only GET is performed
during dry-run.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ddf950e-3b0c-4dc7-b959-bd25f40bdf2b

📥 Commits

Reviewing files that changed from the base of the PR and between e8202c2 and b47bee3.

📒 Files selected for processing (2)
  • cmd/service/service.go
  • cmd/service/service_test.go

Comment on lines +482 to +504
func TestMailRulesReorderListFailureDoesNotCallReorder(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, testConfig)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/mail/v1/user_mailboxes/me/rules",
Body: map[string]interface{}{
"code": 999,
"msg": "list failed",
},
})

cmd := newMailRulesReorderCommand(f)
cmd.SetArgs([]string{
"--as", "bot",
"--params", `{"user_mailbox_id":"me"}`,
"--data", `{"rule_ids":["rule_1"]}`,
})

if err := cmd.Execute(); err == nil {
t.Fatal("expected list error")
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Strengthen TestMailRulesReorderListFailureDoesNotCallReorder to actually prove the reorder call is skipped.

The test only asserts err != nil. If the ac.CheckResponse guard in fetchAllMailRuleIDs (service.go Line 757-759) were removed, extractMailRuleIDs would return nil for this malformed body (no data key), and completeMailRuleIDs would still return an "unknown rule_ids" validation error for the non-empty inputIDs. The test would still pass with that regression, so it doesn't verify the behavior its name claims.

Register a POST stub with OnMatch setting a reorderCalled flag (as in TestMailRulesReorderCompletesPartialRuleIDs), and assert !reorderCalled after Execute(), or assert the error message contains "list failed" to pin the failure to the list call specifically.

🧪 Proposed strengthening of the assertion
 func TestMailRulesReorderListFailureDoesNotCallReorder(t *testing.T) {
 	f, _, _, reg := cmdutil.TestFactory(t, testConfig)
 	reg.Register(&httpmock.Stub{
 		Method: "GET",
 		URL:    "/open-apis/mail/v1/user_mailboxes/me/rules",
 		Body: map[string]interface{}{
 			"code": 999,
 			"msg":  "list failed",
 		},
 	})
+	reorderCalled := false
+	reg.Register(&httpmock.Stub{
+		Method:  "POST",
+		URL:     "/open-apis/mail/v1/user_mailboxes/me/rules/reorder",
+		OnMatch: func(req *http.Request) { reorderCalled = true },
+		Body: map[string]interface{}{
+			"code": 0,
+			"msg":  "ok",
+			"data": map[string]interface{}{"ok": true},
+		},
+	})
 
 	cmd := newMailRulesReorderCommand(f)
 	cmd.SetArgs([]string{
 		"--as", "bot",
 		"--params", `{"user_mailbox_id":"me"}`,
 		"--data", `{"rule_ids":["rule_1"]}`,
 	})
 
 	if err := cmd.Execute(); err == nil {
 		t.Fatal("expected list error")
 	}
+	if reorderCalled {
+		t.Fatal("expected reorder API not to be called after list failure")
+	}
 }
Based on coding guidelines: "contract tests must assert the changed field or behavior directly so reverting the implementation causes failure."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestMailRulesReorderListFailureDoesNotCallReorder(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, testConfig)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/mail/v1/user_mailboxes/me/rules",
Body: map[string]interface{}{
"code": 999,
"msg": "list failed",
},
})
cmd := newMailRulesReorderCommand(f)
cmd.SetArgs([]string{
"--as", "bot",
"--params", `{"user_mailbox_id":"me"}`,
"--data", `{"rule_ids":["rule_1"]}`,
})
if err := cmd.Execute(); err == nil {
t.Fatal("expected list error")
}
}
func TestMailRulesReorderListFailureDoesNotCallReorder(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, testConfig)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/mail/v1/user_mailboxes/me/rules",
Body: map[string]interface{}{
"code": 999,
"msg": "list failed",
},
})
reorderCalled := false
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/mail/v1/user_mailboxes/me/rules/reorder",
OnMatch: func(req *http.Request) { reorderCalled = true },
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"data": map[string]interface{}{"ok": true},
},
})
cmd := newMailRulesReorderCommand(f)
cmd.SetArgs([]string{
"--as", "bot",
"--params", `{"user_mailbox_id":"me"}`,
"--data", `{"rule_ids":["rule_1"]}`,
})
if err := cmd.Execute(); err == nil {
t.Fatal("expected list error")
}
if reorderCalled {
t.Fatal("expected reorder API not to be called after list failure")
}
}
🤖 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/service/service_test.go` around lines 482 - 504, Strengthen
TestMailRulesReorderListFailureDoesNotCallReorder by registering a POST stub for
the reorder endpoint with OnMatch setting a reorderCalled flag, then assert the
flag remains false after Execute returns an error. This must verify the reorder
request is skipped rather than only confirming that some error occurred.

Source: Coding guidelines

Comment thread cmd/service/service.go
Comment on lines +692 to +699
body, ok := request.Data.(map[string]interface{})
if !ok {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--data must be a JSON object for mail rules reorder").WithParam("--data")
}
inputIDs, err := stringSliceField(body, "rule_ids")
if err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids must be an array of non-empty strings").WithParam("rule_ids").WithCause(err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

A missing rule_ids key silently becomes a no-op reorder instead of a validation error.

stringSliceField returns (nil, nil) when the rule_ids key is absent from body (Line 810-812). Nothing in completeMailRulesReorderRequest distinguishes "key missing" from "key present as empty array". Downstream, completeMailRuleIDs treats an empty inputIDs against a non-empty currentIDs as "keep the current order" (Lines 867-877), silently returning success instead of surfacing that the required rule_ids field was not supplied.

The method's own metadata declares rule_ids as "required": true in the request body. Rejecting an entirely missing rule_ids field with a typed validation error (distinct from an explicit empty array, if that is meant to be a valid "keep current order" request) makes the behavior consistent with the metadata contract and avoids a request silently doing nothing without any error to the caller.

🐛 Proposed fix to reject a missing `rule_ids` key
 	inputIDs, err := stringSliceField(body, "rule_ids")
 	if err != nil {
 		return errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids must be an array of non-empty strings").WithParam("rule_ids").WithCause(err)
 	}
+	if _, present := body["rule_ids"]; !present {
+		return errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids is required").WithParam("rule_ids")
+	}
 	if duplicates := duplicateStrings(inputIDs); len(duplicates) > 0 {
Based on coding guidelines: "never silently coerce unsupported inputs, ignore unhonored options, default missing identities, or discard writes. Return a typed validation error when a requested behavior cannot be honored."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
body, ok := request.Data.(map[string]interface{})
if !ok {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--data must be a JSON object for mail rules reorder").WithParam("--data")
}
inputIDs, err := stringSliceField(body, "rule_ids")
if err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids must be an array of non-empty strings").WithParam("rule_ids").WithCause(err)
}
body, ok := request.Data.(map[string]interface{})
if !ok {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--data must be a JSON object for mail rules reorder").WithParam("--data")
}
inputIDs, err := stringSliceField(body, "rule_ids")
if err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids must be an array of non-empty strings").WithParam("rule_ids").WithCause(err)
}
if _, present := body["rule_ids"]; !present {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids is required").WithParam("rule_ids")
}
🤖 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/service/service.go` around lines 692 - 699, Update
completeMailRulesReorderRequest to explicitly check that body contains the
rule_ids key before calling stringSliceField. Return the existing typed
validation error for a missing field, while preserving the current handling of
an explicitly provided empty array.

Source: Coding guidelines

Comment thread cmd/service/service.go
Comment on lines +808 to +826
func stringSliceField(values map[string]interface{}, key string) ([]string, error) {
raw, ok := values[key]
if !ok || raw == nil {
return nil, nil
}
items, ok := raw.([]interface{})
if !ok {
return nil, fmt.Errorf("%s is %T", key, raw)
}
out := make([]string, 0, len(items))
for _, item := range items {
s, ok := item.(string)
if !ok || strings.TrimSpace(s) == "" {
return nil, fmt.Errorf("%s contains %T", key, item)
}
out = append(out, s)
}
return out, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validated rule_ids entries are stored untrimmed, causing mismatches with the trimmed currentIDs.

stringSliceField validates each entry using strings.TrimSpace(s) == "" (Line 820) but appends the raw s at Line 823. Meanwhile, firstString (used to build currentIDs from the API response) trims each value at Line 800-801. A user-supplied ID with surrounding whitespace, such as " rule_1", passes validation as non-empty but is then compared against the trimmed "rule_1" in completeMailRuleIDs, producing an incorrect "unknown rule_ids" error even though the ID logically matches. The same untrimmed value also defeats duplicateStrings, since "rule_1" and " rule_1" are treated as distinct.

🐛 Proposed fix to store the trimmed value
 	for _, item := range items {
 		s, ok := item.(string)
-		if !ok || strings.TrimSpace(s) == "" {
+		trimmed := strings.TrimSpace(s)
+		if !ok || trimmed == "" {
 			return nil, fmt.Errorf("%s contains %T", key, item)
 		}
-		out = append(out, s)
+		out = append(out, trimmed)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func stringSliceField(values map[string]interface{}, key string) ([]string, error) {
raw, ok := values[key]
if !ok || raw == nil {
return nil, nil
}
items, ok := raw.([]interface{})
if !ok {
return nil, fmt.Errorf("%s is %T", key, raw)
}
out := make([]string, 0, len(items))
for _, item := range items {
s, ok := item.(string)
if !ok || strings.TrimSpace(s) == "" {
return nil, fmt.Errorf("%s contains %T", key, item)
}
out = append(out, s)
}
return out, nil
}
func stringSliceField(values map[string]interface{}, key string) ([]string, error) {
raw, ok := values[key]
if !ok || raw == nil {
return nil, nil
}
items, ok := raw.([]interface{})
if !ok {
return nil, fmt.Errorf("%s is %T", key, raw)
}
out := make([]string, 0, len(items))
for _, item := range items {
s, ok := item.(string)
trimmed := strings.TrimSpace(s)
if !ok || trimmed == "" {
return nil, fmt.Errorf("%s contains %T", key, item)
}
out = append(out, trimmed)
}
return out, nil
}
🤖 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/service/service.go` around lines 808 - 826, The stringSliceField helper
validates trimmed string entries but stores their raw values, causing rule ID
comparisons and duplicate detection to disagree with normalized IDs. In
stringSliceField, trim each non-empty string and append the trimmed value to
out, while preserving the existing validation and error behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant