Skip to content

feat(mail): consume watch events via event framework - #1534

Open
bubbmon233 wants to merge 1 commit into
larksuite:mainfrom
bubbmon233:feat/97a3db0
Open

feat(mail): consume watch events via event framework#1534
bubbmon233 wants to merge 1 commit into
larksuite:mainfrom
bubbmon233:feat/97a3db0

Conversation

@bubbmon233

@bubbmon233 bubbmon233 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Generated by the harness-coding skill.

  • Branch: feat/97a3db0
  • Target: main

Sprints

ID Title Status Commit
S1 Migrate mail +watch to the event consume framework passed 28b543c

This MR was created autonomously. Quality gates were enforced by the repo's own pre-commit hooks.

Summary by CodeRabbit

  • New Features

    • Added mail event watching via event consumption framework for real-time incoming message notifications.
    • Users can filter watched messages by labels or folders.
  • Bug Fixes & Improvements

    • Refactored mail watch command with improved output handling and error resilience.
  • Documentation

    • Updated mail watch documentation with event consumption framework details.
    • Added guidance on using --print-output-schema to view output format per configuration.
    • Clarified output envelope behavior and file naming conventions for event files.

Migrate mail +watch from direct SDK WebSocket handling to the shared event consume framework while preserving the shortcut flags, dry-run, schema output, mailbox/filter params, and output modes.

Add mail.user_mailbox.event.message_received_v1 EventKey with mailbox SubscriptionKey, NormalizeParams, Match, Process, and PreConsume subscribe/unsubscribe lifecycle.

Document event consume parity and output-dir filename differences.

contract divergence: type=cli prelude references larksuite-cli-registry meta refresh, but this sprint is scoped to github.com/larksuite/cli and explicitly forbids Meta/registry changes; no meta-gen was run.

sprint: S1
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR migrates the mail +watch shortcut from a direct WebSocket/event-dispatcher implementation to the shared event-consume framework. It introduces a new events/mail package containing event registration, a subscribe/unsubscribe pre-consume hook, and a normalize/match/process pipeline for mail.user_mailbox.event.message_received_v1. Helper functions in shortcuts/mail are exported for cross-package use, and the global event registry is updated to include the new mail event key.

Changes

Mail watch event-consume migration

Layer / File(s) Summary
Exported helper wrappers in shortcuts/mail
shortcuts/mail/helpers.go, shortcuts/mail/mail_watch.go
Adds exported wrappers (MailboxPath, ResolveFolderSystemAliasOrIDForWatch, ResolveLabelSystemIDForWatch, ExtractMailEventBody, WatchFetchFormat, MinimalWatchMessage, WatchFetchFailureValue, MessageHasLabel, WrapWatchSubscribeError, EnhanceProfileError) around existing unexported functions for use from the new events/mail package.
events/mail API utility functions
events/mail/api.go
Implements fetchMailboxPrimaryEmail, fetchMessageForWatch, resolveWatchFilterIDs, resolveNamesByAPI, matchingIDs, itemID, parseJSONArrayFlag, splitComma, and sortedSetKeys for mailbox/message access and label/folder filter ID resolution with deduplication.
Event registration and pre-consume subscription lifecycle
events/mail/register.go, events/mail/preconsume.go, events/mail/preconsume_test.go
Defines EventTypeMessageReceived, WatchOutput, and Keys() wiring all pipeline hooks; implements subscribe/unsubscribe POST lifecycle in mailboxEventPreConsume with unit test coverage.
Normalize/match/process watch event pipeline
events/mail/watch.go, events/mail/watch_test.go
Implements normalizeWatchParams (mailbox resolution, filter ID expansion), matchWatchMailbox (case-insensitive address check), and processWatchEvent (conditional message fetch, folder/label filtering, output marshaling). Six test cases cover normalization, matching, fetch+filter, filter drop, and fetch failure.
MailWatch shortcut migration to event-consume
shortcuts/mail/mail_watch.go, shortcuts/mail/mail_watch_test.go
Replaces the inlined WebSocket/signal/shutdown flow in Execute with runMailWatchViaEvent that adapts API calls via mailWatchEventRuntime and delegates to consume.Run. Two new tests verify delegation and --print-output-schema short-circuit behavior.
Global event registry wiring and documentation
events/register.go, skills/lark-mail/SKILL.md, skills/lark-mail/references/lark-mail-watch.md
Adds mail.Keys() to the global init registry. Updates docs to reflect the event-consume approach, revised --format default (data), and timestamp-based --output-dir file naming.

Sequence Diagram(s)

sequenceDiagram
  participant User as User / CLI
  participant MailWatch as MailWatch.Execute
  participant consume.Run as consume.Run
  participant PreConsume as mailboxEventPreConsume
  participant SubscribeAPI as POST .../event/subscribe
  participant normalizeWatchParams
  participant ProfileAPI as GET mailbox profile
  participant matchWatchMailbox
  participant processWatchEvent
  participant MessageAPI as GET message

  User->>MailWatch: flags (mailbox, format, msg_format, labels, folders)
  MailWatch->>consume.Run: Options{EventKey, Params, Runtime}
  consume.Run->>PreConsume: invoke pre-consume hook
  PreConsume->>SubscribeAPI: POST subscribe (event_type:1)
  PreConsume-->>consume.Run: cleanup func
  consume.Run->>normalizeWatchParams: resolve mailbox + filter IDs
  normalizeWatchParams->>ProfileAPI: fetch primary_email_address
  normalizeWatchParams-->>consume.Run: mailbox_api, mailbox_email, label_ids, folder_ids
  loop each incoming event
    consume.Run->>matchWatchMailbox: compare event mail_address to mailbox_email
    matchWatchMailbox-->>consume.Run: match / drop
    consume.Run->>processWatchEvent: extract, fetch, filter, marshal
    processWatchEvent->>MessageAPI: GET message (when msg_format != event)
    processWatchEvent-->>consume.Run: JSON output or nil (filtered)
  end
  consume.Run->>PreConsume: invoke cleanup
  PreConsume->>SubscribeAPI: POST unsubscribe
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • larksuite/cli#198: Both PRs implement mailbox=me resolution by calling the profile API to obtain the primary email address and using it to filter incoming mail events.
  • larksuite/cli#1250: This PR exports WrapWatchSubscribeError and EnhanceProfileError from shortcuts/mail/mail_watch.go; the retrieved PR modifies those same helpers to use typed errs.ProblemOf-based error enrichment, creating a direct overlap at the function level.

Suggested labels

domain/mail, size/L, feature

Suggested reviewers

  • chanthuang
  • zhangjun-bytedance

Poem

🐇 Farewell, WebSocket of old,
A new consume path unfolds—
Mail events route through the frame,
Normalize, match, process by name.
Subscribe, then cleanup on done,
The rabbit hops—watch has won! ✉️

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete and does not follow the provided template structure, missing required sections like Summary, Changes, and Test Plan. Add a proper Summary section (1-3 sentences), list main Changes as bullet points, describe the Test Plan with verification steps, and include Related Issues section following the template.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: migrating the mail watch functionality to use the event framework for consuming watch events.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@github-actions github-actions Bot added domain/mail PR touches the mail domain size/L Large or sensitive change across domains or core paths labels Jun 22, 2026
@github-actions

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add bubbmon233/cli#feat/97a3db0 -y -g

@github-actions

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

@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: 2

🧹 Nitpick comments (1)
events/mail/preconsume_test.go (1)

11-30: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add typed error-path tests for pre-consume failures.

This test only covers success flow. Please add failure-path cases (e.g., subscribe failure and nil runtime) and assert typed metadata (category/subtype) plus cause preservation, so the new error-classification behavior is regression-safe.

As per coding guidelines, "**/*_test.go: Error-path tests must assert typed metadata via errs.ProblemOf (category / subtype / param) and cause preservation, not message substrings alone" and "Every behavior change needs a test alongside the change`."

🤖 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 `@events/mail/preconsume_test.go` around lines 11 - 30, The
TestMailboxEventPreConsumeLifecycle test only covers the success path. Add
additional test cases to verify failure scenarios including when the subscribe
API call fails and when the runtime is nil, using errs.ProblemOf to assert the
typed error metadata fields (category and subtype) and verify cause preservation
rather than just checking error message substrings. These test cases should
validate that the mailboxEventPreConsume function properly classifies and
preserves errors in its error-path behavior.

Source: Coding guidelines

🤖 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 `@events/mail/api.go`:
- Around line 96-99: The resolveNamesByAPI function is being called
unconditionally even when remainingNames is empty, which causes unnecessary API
calls and can fail watch startup on network issues. Add a conditional check to
skip the resolveNamesByAPI call when remainingNames is empty, so the remote
name-resolution is only attempted when there are actually names to resolve.

In `@shortcuts/mail/mail_watch.go`:
- Around line 231-258: The CallAPI method in mailWatchEventRuntime is currently
ignoring the incoming context parameter (note the underscore in the method
signature) and not propagating it to the DoAPI call. To fix this, accept the
context parameter properly in the CallAPI method signature (remove the
underscore), and then pass this context to the DoAPI call through the ApiReq
struct or as a parameter, ensuring that cancellation signals and deadlines from
the caller are properly honored for these API requests.

---

Nitpick comments:
In `@events/mail/preconsume_test.go`:
- Around line 11-30: The TestMailboxEventPreConsumeLifecycle test only covers
the success path. Add additional test cases to verify failure scenarios
including when the subscribe API call fails and when the runtime is nil, using
errs.ProblemOf to assert the typed error metadata fields (category and subtype)
and verify cause preservation rather than just checking error message
substrings. These test cases should validate that the mailboxEventPreConsume
function properly classifies and preserves errors in its error-path behavior.
🪄 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

Run ID: b06a9c20-dc60-4dc5-b60a-76b75db5bb46

📥 Commits

Reviewing files that changed from the base of the PR and between 824aa9e and 28b543c.

📒 Files selected for processing (12)
  • events/mail/api.go
  • events/mail/preconsume.go
  • events/mail/preconsume_test.go
  • events/mail/register.go
  • events/mail/watch.go
  • events/mail/watch_test.go
  • events/register.go
  • shortcuts/mail/helpers.go
  • shortcuts/mail/mail_watch.go
  • shortcuts/mail/mail_watch_test.go
  • skills/lark-mail/SKILL.md
  • skills/lark-mail/references/lark-mail-watch.md

Comment thread events/mail/api.go
Comment on lines +96 to +99
resolvedNames, err := resolveNamesByAPI(ctx, rt, mailboxID, remainingNames, kind)
if err != nil {
return nil, 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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip remote name-resolution calls when there are no names to resolve.

resolveWatchFilterIDs calls resolveNamesByAPI even when remainingNames is empty. This can fail watch startup on network/API issues despite users supplying only explicit IDs.

♻️ Proposed fix
-	resolvedNames, err := resolveNamesByAPI(ctx, rt, mailboxID, remainingNames, kind)
+	if len(remainingNames) == 0 {
+		return sortedSetKeys(set), nil
+	}
+	resolvedNames, err := resolveNamesByAPI(ctx, rt, mailboxID, remainingNames, kind)
 	if err != nil {
 		return nil, err
 	}
📝 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
resolvedNames, err := resolveNamesByAPI(ctx, rt, mailboxID, remainingNames, kind)
if err != nil {
return nil, err
}
if len(remainingNames) == 0 {
return sortedSetKeys(set), nil
}
resolvedNames, err := resolveNamesByAPI(ctx, rt, mailboxID, remainingNames, kind)
if err != nil {
return nil, err
}
🤖 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 `@events/mail/api.go` around lines 96 - 99, The resolveNamesByAPI function is
being called unconditionally even when remainingNames is empty, which causes
unnecessary API calls and can fail watch startup on network issues. Add a
conditional check to skip the resolveNamesByAPI call when remainingNames is
empty, so the remote name-resolution is only attempted when there are actually
names to resolve.

Comment on lines +231 to +258
func (r *mailWatchEventRuntime) CallAPI(_ context.Context, method, path string, body interface{}) (json.RawMessage, error) {
apiPath := path
query := larkcore.QueryParams{}
if u, err := url.Parse(path); err == nil && u.RawQuery != "" {
apiPath = u.Path
for k, values := range u.Query() {
for _, value := range values {
query.Add(k, value)
}
handleEvent(eventData)
return nil
}
}
req := &larkcore.ApiReq{
HttpMethod: method,
ApiPath: apiPath,
QueryParams: query,
}
if body != nil {
req.Body = body
}
resp, err := r.runtime.DoAPI(req)
if err != nil {
return nil, client.WrapDoAPIError(err)
}
if _, err := r.runtime.ClassifyAPIResponse(resp); err != nil {
return json.RawMessage(resp.RawBody), err
}
return json.RawMessage(resp.RawBody), 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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Context cancellation is currently dropped in the API adapter.

Line 231 ignores the incoming ctx, and requests are routed through RuntimeContext.DoAPI, which uses the runtime’s stored context instead of the per-call context. That means consume.Run deadlines/cancel signals (including cleanup timeout paths) are not reliably enforced for these API calls.

🤖 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 `@shortcuts/mail/mail_watch.go` around lines 231 - 258, The CallAPI method in
mailWatchEventRuntime is currently ignoring the incoming context parameter (note
the underscore in the method signature) and not propagating it to the DoAPI
call. To fix this, accept the context parameter properly in the CallAPI method
signature (remove the underscore), and then pass this context to the DoAPI call
through the ApiReq struct or as a parameter, ensuring that cancellation signals
and deadlines from the caller are properly honored for these API requests.

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

Labels

domain/mail PR touches the mail domain 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