Skip to content

feat(im): add message_app_link to message outputs - #668

Merged
YangJunzhou-01 merged 1 commit into
larksuite:mainfrom
sammi-bytedance:feat/im-message-applink
May 8, 2026
Merged

feat(im): add message_app_link to message outputs#668
YangJunzhou-01 merged 1 commit into
larksuite:mainfrom
sammi-bytedance:feat/im-message-applink

Conversation

@sammi-bytedance

@sammi-bytedance sammi-bytedance commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

IM message-related shortcuts now return a deterministic deep link (message_app_link) assembled from message fields when the server does not provide one.

Changes

  • Assemble message_app_link for chat and thread messages in shortcuts/im/convert_lib/FormatMessageItem.
  • Preserve/forward chat_id, message_position, thread_message_position, and existing message_app_link fields.
  • Allow message_position / thread_message_position to be 0 or negative (as returned by IM APIs) when assembling the link.
  • Add IM E2E assertions for +messages-mget, +chat-messages-list, and +threads-messages-list.

Test Plan

  • go test ./shortcuts/im/convert_lib -count=1
  • go test -v ./tests/cli_e2e/im -run TestIM_MessageReplyWorkflowAsBot -count=1
  • go test -v ./tests/cli_e2e/im -run 'TestIM_(MessageGetWorkflowAsUser|ChatMessageWorkflowAsUser)$' -count=1 (skips without user token)

Related Issues

  • None

Summary by CodeRabbit

  • New Features

    • Message results now include message_app_link: existing links are preserved; when missing, a deterministic app-link URL is generated so users can open specific chat or thread messages in the app, using the correct brand domain and message/thread identifiers. Normalization accepts multiple numeric formats (including zero and negative).
  • Tests

    • Expanded unit and end-to-end tests validate app-link generation, brand selection, thread vs chat links, numeric position handling, and link presence/format.

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Preserves server-provided message metadata, adds deterministic assembly of message_app_link from brand-specific endpoints when absent, normalizes numeric message/thread positions from various types, and exposes Endpoints.AppLink. Unit and e2e tests plus helpers validate domain resolution and assembled links.

Changes

Message formatting implementation

Layer / File(s) Summary
Data Shape / Config
internal/core/types.go, internal/core/types_test.go
Adds exported Endpoints.AppLink and test assertions expecting brand-specific AppLink URLs for Feishu and Lark.
Normalization Helpers
shortcuts/im/convert_lib/content_convert.go
Adds normalizeMessagePosition to normalize message_position/thread_message_position from numeric types, json.Number, and numeric strings; rejects empty/NaN/Inf/invalid values.
App-link Resolution & Builder
shortcuts/im/convert_lib/content_convert.go
Adds resolveAppLinkDomain to extract host from Endpoints.AppLink and assembleMessageAppLink to build chat or thread app-link URLs using IDs and normalized positions.
Core Formatting Logic
shortcuts/im/convert_lib/content_convert.go
Extends FormatMessageItem to forward raw fields (chat_id, message_position, thread_message_position, message_app_link) and compute a fallback message_app_link when missing and runtime.Config is available.
Unit Tests
shortcuts/im/convert_lib/content_media_misc_test.go
Adds tests covering app-link domain resolution, pass-through message_app_link, chat vs thread assembly, fallback behaviors, runtime-nil/no-assemble, missing-fields no-panic, and normalization (including zero/negative handling).
E2E Test Helpers & Workflows
tests/cli_e2e/im/helpers_test.go, tests/cli_e2e/im/chat_message_workflow_test.go, tests/cli_e2e/im/message_get_workflow_test.go, tests/cli_e2e/im/message_reply_workflow_test.go
Adds helpers that validate message_app_link structure (scheme, host, path, query) using env-driven brand domain and updates e2e tests to assert presence and correctness of message_app_link for chat and thread messages.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Formatter
  participant Runtime
  participant Endpoints
  participant URLBuilder

  Client->>Formatter: send raw message map
  Formatter->>Formatter: forward known fields and read raw metadata
  Formatter->>Runtime: request Config (brand endpoints)
  Runtime->>Endpoints: return endpoints including AppLink
  Formatter->>URLBuilder: request host resolution and normalize positions
  URLBuilder-->>Formatter: assembled chat/thread app link or empty
  Formatter-->>Client: return enriched message map (includes message_app_link)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

size/L

Suggested reviewers

  • YangJunzhou-01

Poem

🐰 I stitched each message a bright little link,
From chat or from thread, quick as a wink.
Domains chosen true, positions made neat,
Hop over the URL — your message to greet! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding message_app_link to message outputs in the IM module.
Description check ✅ Passed The description includes all required sections: Summary, Changes, Test Plan, and Related Issues, with clear detail about what was implemented and how it was tested.
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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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/im PR touches the im domain size/M Single-domain feat or fix with limited business impact labels Apr 27, 2026

@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.

🧹 Nitpick comments (6)
shortcuts/im/convert_lib/content_convert.go (4)

197-216: Consider URL-escaping the path/query values when assembling message_app_link.

chatID, threadID, and the normalized position strings are interpolated directly into the URL via fmt.Sprintf without URL-encoding. While IM IDs (oc_*, omt_*) and numeric positions are typically URL-safe in practice, building URLs via net/url (e.g., url.URL{Scheme, Host, Path} plus url.Values) would be more defensive against unexpected characters and matches Go idioms for URL construction. Note the produced query parameter casing differs intentionally between the chat (openChatId/position) and thread (open_thread_id/open_chat_id/thread_position) endpoints, so a url.Values map preserves that.

♻️ Optional refactor using net/url
 func assembleMessageAppLink(m map[string]interface{}, brand core.LarkBrand) string {
 	domain := resolveAppLinkDomain(brand)
 	if domain == "" {
 		return ""
 	}
 
 	chatID, _ := m["chat_id"].(string)
 	threadID, _ := m["thread_id"].(string)
 	msgPos, okMsgPos := normalizeMessagePosition(m["message_position"])
 	threadPos, okThreadPos := normalizeMessagePosition(m["thread_message_position"])
 
-	// Thread app link requires both thread_id and chat_id.
-	if threadID != "" && chatID != "" && okThreadPos {
-		return fmt.Sprintf("https://%s/client/thread/open?open_thread_id=%s&open_chat_id=%s&thread_position=%s", domain, threadID, chatID, threadPos)
-	}
-	if chatID != "" && okMsgPos {
-		return fmt.Sprintf("https://%s/client/chat/open?openChatId=%s&position=%s", domain, chatID, msgPos)
-	}
-	return ""
+	u := url.URL{Scheme: "https", Host: domain}
+	q := url.Values{}
+	switch {
+	case threadID != "" && chatID != "" && okThreadPos:
+		u.Path = "/client/thread/open"
+		q.Set("open_thread_id", threadID)
+		q.Set("open_chat_id", chatID)
+		q.Set("thread_position", threadPos)
+	case chatID != "" && okMsgPos:
+		u.Path = "/client/chat/open"
+		q.Set("openChatId", chatID)
+		q.Set("position", msgPos)
+	default:
+		return ""
+	}
+	u.RawQuery = q.Encode()
+	return u.String()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/convert_lib/content_convert.go` around lines 197 - 216, The
assembleMessageAppLink function interpolates chatID, threadID, msgPos and
threadPos directly into the URL which can risk unescaped characters; change it
to build the link with net/url (use url.URL{Scheme:"https", Host: domain, Path:
...} and url.Values for query params) and set the query parameters with the
exact casing used currently ("open_thread_id","open_chat_id","thread_position"
for thread links and "openChatId","position" for chat links) so values are
URL-encoded; update the thread and chat branches in assembleMessageAppLink to
construct and return url.String() rather than using fmt.Sprintf.

263-270: resolveAppLinkDomain swallows empty Host from a parsed URL.

url.Parse returns no error for many malformed inputs (e.g., an empty string, "feishu.cn" with no scheme), in which case u.Host may be empty and the assembler will quietly return "". This is fine if core.ResolveEndpoints(brand).Open is guaranteed to be a well-formed https://host[/...] URL for both BrandFeishu/BrandLark and the unknown-brand fallback (the unit tests confirm this for feishu/lark/other). Worth a brief comment documenting the contract, or a fallback to a literal default (e.g., open.feishu.cn) on empty Host, so a future endpoint config regression doesn't silently disable message_app_link everywhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/convert_lib/content_convert.go` around lines 263 - 270,
resolveAppLinkDomain currently returns u.Host even when url.Parse succeeds but
yields an empty Host (e.g., no scheme), which can silently break
message_app_link; update resolveAppLinkDomain to check if u == nil or u.Host is
empty after parsing core.ResolveEndpoints(brand).Open and, if empty, return a
safe fallback host (for example "open.feishu.cn") or derive a sensible default
based on brand, and add a brief comment documenting the expectation that
ResolveEndpoints(...).Open should be a full https:// URL; reference the
resolveAppLinkDomain function and core.ResolveEndpoints(brand).Open when making
the change.

218-261: normalizeMessagePosition doesn't cover int32/uint* numeric types.

The type switch handles float64, int, int64, json.Number, and string, but not int32, uint, uint32, uint64. JSON unmarshalling into map[string]interface{} uses float64 (or json.Number when UseNumber is set), so this is unlikely to manifest in the current call path. However, if a caller ever feeds in a value from a typed struct or a different decoder, those numeric kinds will silently fall through to the default branch and the link won't be assembled. Consider adding the additional integer cases (or use reflect) for resilience.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/convert_lib/content_convert.go` around lines 218 - 261, The
normalizeMessagePosition function's type switch misses other integer kinds
(e.g., int32, uint, uint32, uint64), causing them to hit the default false path;
update the type switch inside normalizeMessagePosition to add cases for
int8/int16/int32, uint/uint8/uint16/uint32/uint64 (and uintptr if desired) and
convert those to a canonical integer representation (int64 or uint64) then
format with strconv.FormatInt/FormatUint (or convert unsigned to signed only
when safe) so they return the same string,true behavior as int/int64;
alternatively, replace the type-switch branch for numeric types with a
reflect-based check that accepts all integer kinds and floats and normalizes
them the same way.

156-177: Subtle interaction between field preservation and link assembly.

Lines 166-168 unconditionally copy raw["message_app_link"] (any underlying type) into msg, then lines 171-177 may overwrite it only when the type-assertion to string yields an empty string. If the server ever returns message_app_link as a non-string (unlikely but defensible), the preserved non-string value will be silently replaced by an assembled string, which is inconsistent with the intended “pass-through if server provided one” semantics.

Consider gating preservation through the same string type-assertion used by the assembler, so the two branches stay aligned:

♻️ Tighter pass-through gate
-	if v, ok := m["message_app_link"]; ok {
-		msg["message_app_link"] = v
-	}
-
-	// Assemble message_app_link deterministically when server doesn't provide one.
-	if runtime != nil && runtime.Config != nil {
-		if rawAppLink, _ := m["message_app_link"].(string); rawAppLink == "" {
-			if assembled := assembleMessageAppLink(m, runtime.Config.Brand); assembled != "" {
-				msg["message_app_link"] = assembled
-			}
-		}
-	}
+	rawAppLink, _ := m["message_app_link"].(string)
+	if rawAppLink != "" {
+		msg["message_app_link"] = rawAppLink
+	} else if runtime != nil && runtime.Config != nil {
+		if assembled := assembleMessageAppLink(m, runtime.Config.Brand); assembled != "" {
+			msg["message_app_link"] = assembled
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/convert_lib/content_convert.go` around lines 156 - 177, The
current code copies m["message_app_link"] into msg unconditionally which can
conflict with assembleMessageAppLink; change the preservation to the same string
type-assertion/empty check used later: when handling the "message_app_link"
field, only copy it into msg if you can assert it to a non-empty string (use the
same rawAppLink, _ := m["message_app_link"].(string) pattern), otherwise leave
it unset so assembleMessageAppLink(m, runtime.Config.Brand) can
deterministically populate it; update the block that currently references
m["message_app_link"], msg, runtime, and assembleMessageAppLink to follow this
gated behavior.
shortcuts/im/convert_lib/content_media_misc_test.go (1)

66-212: Solid coverage for the new message_app_link and normalizeMessagePosition paths.

The added tests exercise pass-through, chat/thread assembly, brand fallback, nil-runtime no-op, missing-fields no-op, invalid-thread-position fallback, and zero/negative numeric positions across both numeric and string inputs. Two small optional gaps if you want belt-and-suspenders coverage:

  • json.Number input to normalizeMessagePosition (the case json.Number: branch isn't directly exercised — current tests rely on float64/string).
  • An integer-valued float case (float64(12.0)) is exercised indirectly in AssembleChat, but a direct unit assertion against normalizeMessagePosition for math.Trunc(f) == f formatting could make the contract explicit.

Non-blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/convert_lib/content_media_misc_test.go` around lines 66 - 212,
Add two small unit tests to exercise the json.Number and integer-valued float
branches of normalizeMessagePosition: call
normalizeMessagePosition(json.Number("12")) and assert it returns "12", and call
normalizeMessagePosition(float64(12.0)) and assert it returns "12"; place these
alongside the existing TestNormalizeMessagePosition_AllowsZeroAndNegative tests
so the json.Number and the integer float (math.Trunc(f) == f) code paths in
normalizeMessagePosition are covered.
tests/cli_e2e/im/message_reply_workflow_test.go (1)

105-126: Empty-link t.Fatalf is redundant with requireThreadMessageAppLink's non-empty assertion.

requireThreadMessageAppLink already calls require.NotEmpty(t, appLink, ...) first thing, so the if appLink == "" block effectively duplicates that check. The motivation appears to be richer diagnostics (the explicit field dump and item.Raw), which is understandable. If you want to keep both, fine — otherwise consider folding the diagnostic into the helper (e.g., as an additional msgAndArgs payload) so failure reporting lives in one place.

Also, the trailing require.Equal(t, threadID, item.Get("thread_id").String(), ...) on Line 126 is partially covered by the helper (which asserts the URL's open_thread_id equals the passed threadID); it's still a useful end-to-end self-consistency check between the URL and the response field, so leaving it is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/cli_e2e/im/message_reply_workflow_test.go` around lines 105 - 126, The
explicit empty-link t.Fatalf block duplicates the non-empty assertion already
performed by requireThreadMessageAppLink; remove the if appLink == "" {
t.Fatalf(...) } block and rely on requireThreadMessageAppLink(t, appLink,
threadID, chatID, threadMsgPos) instead, or alternatively move the rich
diagnostic payload (item.Raw, threadResult.Stdout and field values) into
requireThreadMessageAppLink so the helper logs those details when it calls
require.NotEmpty; keep the trailing require.Equal(t, threadID,
item.Get("thread_id").String(), ...) as the end-to-end consistency check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@shortcuts/im/convert_lib/content_convert.go`:
- Around line 197-216: The assembleMessageAppLink function interpolates chatID,
threadID, msgPos and threadPos directly into the URL which can risk unescaped
characters; change it to build the link with net/url (use
url.URL{Scheme:"https", Host: domain, Path: ...} and url.Values for query
params) and set the query parameters with the exact casing used currently
("open_thread_id","open_chat_id","thread_position" for thread links and
"openChatId","position" for chat links) so values are URL-encoded; update the
thread and chat branches in assembleMessageAppLink to construct and return
url.String() rather than using fmt.Sprintf.
- Around line 263-270: resolveAppLinkDomain currently returns u.Host even when
url.Parse succeeds but yields an empty Host (e.g., no scheme), which can
silently break message_app_link; update resolveAppLinkDomain to check if u ==
nil or u.Host is empty after parsing core.ResolveEndpoints(brand).Open and, if
empty, return a safe fallback host (for example "open.feishu.cn") or derive a
sensible default based on brand, and add a brief comment documenting the
expectation that ResolveEndpoints(...).Open should be a full https:// URL;
reference the resolveAppLinkDomain function and
core.ResolveEndpoints(brand).Open when making the change.
- Around line 218-261: The normalizeMessagePosition function's type switch
misses other integer kinds (e.g., int32, uint, uint32, uint64), causing them to
hit the default false path; update the type switch inside
normalizeMessagePosition to add cases for int8/int16/int32,
uint/uint8/uint16/uint32/uint64 (and uintptr if desired) and convert those to a
canonical integer representation (int64 or uint64) then format with
strconv.FormatInt/FormatUint (or convert unsigned to signed only when safe) so
they return the same string,true behavior as int/int64; alternatively, replace
the type-switch branch for numeric types with a reflect-based check that accepts
all integer kinds and floats and normalizes them the same way.
- Around line 156-177: The current code copies m["message_app_link"] into msg
unconditionally which can conflict with assembleMessageAppLink; change the
preservation to the same string type-assertion/empty check used later: when
handling the "message_app_link" field, only copy it into msg if you can assert
it to a non-empty string (use the same rawAppLink, _ :=
m["message_app_link"].(string) pattern), otherwise leave it unset so
assembleMessageAppLink(m, runtime.Config.Brand) can deterministically populate
it; update the block that currently references m["message_app_link"], msg,
runtime, and assembleMessageAppLink to follow this gated behavior.

In `@shortcuts/im/convert_lib/content_media_misc_test.go`:
- Around line 66-212: Add two small unit tests to exercise the json.Number and
integer-valued float branches of normalizeMessagePosition: call
normalizeMessagePosition(json.Number("12")) and assert it returns "12", and call
normalizeMessagePosition(float64(12.0)) and assert it returns "12"; place these
alongside the existing TestNormalizeMessagePosition_AllowsZeroAndNegative tests
so the json.Number and the integer float (math.Trunc(f) == f) code paths in
normalizeMessagePosition are covered.

In `@tests/cli_e2e/im/message_reply_workflow_test.go`:
- Around line 105-126: The explicit empty-link t.Fatalf block duplicates the
non-empty assertion already performed by requireThreadMessageAppLink; remove the
if appLink == "" { t.Fatalf(...) } block and rely on
requireThreadMessageAppLink(t, appLink, threadID, chatID, threadMsgPos) instead,
or alternatively move the rich diagnostic payload (item.Raw, threadResult.Stdout
and field values) into requireThreadMessageAppLink so the helper logs those
details when it calls require.NotEmpty; keep the trailing require.Equal(t,
threadID, item.Get("thread_id").String(), ...) as the end-to-end consistency
check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f304490c-57c2-470b-8fb6-9450ca8f87ee

📥 Commits

Reviewing files that changed from the base of the PR and between 1e2144e and 5ed0570.

📒 Files selected for processing (6)
  • shortcuts/im/convert_lib/content_convert.go
  • shortcuts/im/convert_lib/content_media_misc_test.go
  • tests/cli_e2e/im/chat_message_workflow_test.go
  • tests/cli_e2e/im/helpers_test.go
  • tests/cli_e2e/im/message_get_workflow_test.go
  • tests/cli_e2e/im/message_reply_workflow_test.go

@sammi-bytedance
sammi-bytedance force-pushed the feat/im-message-applink branch from 5ed0570 to 4cf23d0 Compare April 28, 2026 09:31

@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.

🧹 Nitpick comments (2)
tests/cli_e2e/im/message_get_workflow_test.go (1)

45-50: Assert chat_id against the created chat before link validation.

Line 47-Line 49 currently pass chat_id from the same response object into the helper, so this only checks internal consistency. Add an equality check against the known chatID to catch cross-chat mismatches.

Suggested patch
 		requireChatMessageAppLink(
-			t,
-			messages[0].Get("message_app_link").String(),
-			messages[0].Get("chat_id").String(),
-			messages[0].Get("message_position").String(),
+			t,
+			messages[0].Get("message_app_link").String(),
+			func() string {
+				msgChatID := messages[0].Get("chat_id").String()
+				require.Equal(t, chatID, msgChatID, "stdout:\n%s", result.Stdout)
+				return msgChatID
+			}(),
+			messages[0].Get("message_position").String(),
 		)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/cli_e2e/im/message_get_workflow_test.go` around lines 45 - 50, The test
currently only validates internal consistency by passing
messages[0].Get("chat_id") into requireChatMessageAppLink; add an explicit
equality assertion that messages[0].Get("chat_id").String() equals the known
chatID before calling requireChatMessageAppLink so the test verifies the message
belongs to the created chat (use the existing t and messages variables and
perform a require.Equal or assert.Equal on chatID vs
messages[0].Get("chat_id").String()).
tests/cli_e2e/im/message_reply_workflow_test.go (1)

106-124: Avoid chatID shadowing and assert against the known workflow chat.

Line 106 shadows the outer chatID, so current validation can pass even if the returned item belongs to the wrong chat but is self-consistent. Use a distinct variable and assert equality with the outer chatID.

Suggested patch
-				chatID := item.Get("chat_id").String()
+				itemChatID := item.Get("chat_id").String()
+				require.Equal(t, chatID, itemChatID, "stdout:\n%s", threadResult.Stdout)
 				threadMsgPos := item.Get("thread_message_position").String()
 				msgPos := item.Get("message_position").String()
 				if appLink == "" {
 					t.Fatalf("thread message_app_link is empty; chat_id=%q thread_id=%q thread_message_position=%q message_position=%q item=%s\nstdout:\n%s",
-						chatID,
+						itemChatID,
 						item.Get("thread_id").String(),
 						threadMsgPos,
 						msgPos,
 						item.Raw,
 						threadResult.Stdout,
 					)
 				}
 				requireThreadMessageAppLink(
 					t,
 					appLink,
 					threadID,
-					chatID,
+					itemChatID,
 					threadMsgPos,
 				)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/cli_e2e/im/message_reply_workflow_test.go` around lines 106 - 124, The
inner variable chatID shadows the outer chatID causing false-positive
validation; rename the inner extraction to a distinct name (e.g., itemChatID)
when calling item.Get("chat_id").String(), add an assertion that itemChatID ==
chatID (the outer variable) before calling requireThreadMessageAppLink, and then
pass itemChatID (or continue using the verified chatID) into
requireThreadMessageAppLink to ensure the returned item belongs to the expected
workflow chat.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/cli_e2e/im/message_get_workflow_test.go`:
- Around line 45-50: The test currently only validates internal consistency by
passing messages[0].Get("chat_id") into requireChatMessageAppLink; add an
explicit equality assertion that messages[0].Get("chat_id").String() equals the
known chatID before calling requireChatMessageAppLink so the test verifies the
message belongs to the created chat (use the existing t and messages variables
and perform a require.Equal or assert.Equal on chatID vs
messages[0].Get("chat_id").String()).

In `@tests/cli_e2e/im/message_reply_workflow_test.go`:
- Around line 106-124: The inner variable chatID shadows the outer chatID
causing false-positive validation; rename the inner extraction to a distinct
name (e.g., itemChatID) when calling item.Get("chat_id").String(), add an
assertion that itemChatID == chatID (the outer variable) before calling
requireThreadMessageAppLink, and then pass itemChatID (or continue using the
verified chatID) into requireThreadMessageAppLink to ensure the returned item
belongs to the expected workflow chat.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ce7224e-d7f0-4f64-9452-cac86405f885

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed0570 and 4cf23d0.

📒 Files selected for processing (8)
  • internal/core/types.go
  • internal/core/types_test.go
  • shortcuts/im/convert_lib/content_convert.go
  • shortcuts/im/convert_lib/content_media_misc_test.go
  • tests/cli_e2e/im/chat_message_workflow_test.go
  • tests/cli_e2e/im/helpers_test.go
  • tests/cli_e2e/im/message_get_workflow_test.go
  • tests/cli_e2e/im/message_reply_workflow_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/core/types_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/cli_e2e/im/chat_message_workflow_test.go
  • shortcuts/im/convert_lib/content_convert.go

@sammi-bytedance
sammi-bytedance force-pushed the feat/im-message-applink branch from 4cf23d0 to aaf8b36 Compare April 28, 2026 09:43

@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.

🧹 Nitpick comments (1)
shortcuts/im/convert_lib/content_media_misc_test.go (1)

108-109: ⚡ Quick win

Make app-link assertions order-insensitive for query params.

Line 108 compares the full URL string; this is brittle if query serialization order changes while semantics stay correct. The same pattern appears in Line 127, Line 146, and Line 163. Prefer parsing the URL and asserting host/path plus query keys/values.

Proposed test helper refactor
+import (
+    "net/url"
+    "testing"
+    ...
+)
+
+func assertURL(t *testing.T, raw string, wantHost, wantPath string, wantQuery map[string]string) {
+    t.Helper()
+    u, err := url.Parse(raw)
+    if err != nil {
+        t.Fatalf("invalid URL %q: %v", raw, err)
+    }
+    if u.Host != wantHost || u.Path != wantPath {
+        t.Fatalf("URL host/path = %s%s, want %s%s", u.Host, u.Path, wantHost, wantPath)
+    }
+    q := u.Query()
+    for k, v := range wantQuery {
+        if q.Get(k) != v {
+            t.Fatalf("query[%s] = %q, want %q", k, q.Get(k), v)
+        }
+    }
+}
- if got["message_app_link"] != "https://applink.feishu.cn/client/chat/open?openChatId=oc_1&position=12" {
-     t.Fatalf("FormatMessageItem() message_app_link = %#v", got["message_app_link"])
- }
+ assertURL(t, got["message_app_link"].(string), "applink.feishu.cn", "/client/chat/open", map[string]string{
+     "openChatId": "oc_1",
+     "position":   "12",
+ })
🤖 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/im/convert_lib/content_media_misc_test.go` around lines 108 - 109,
The test is brittle because it asserts the entire app-link string equality
(got["message_app_link"]) which can break if query param ordering changes;
instead parse the URL returned by FormatMessageItem (the got["message_app_link"]
value), assert the scheme/host/path are correct (e.g., scheme "https" and host
"applink.feishu.cn" and path "/client/chat/open"), then parse the query string
into a map and assert required query keys/values (e.g., openChatId=oc_1 and
position=12). Apply the same change to the other assertions that compare full
URLs (the checks around lines referencing got["message_app_link"] at the other
test cases) so all app-link assertions are order-insensitive by validating
host/path plus query key/value pairs.
🤖 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.

Nitpick comments:
In `@shortcuts/im/convert_lib/content_media_misc_test.go`:
- Around line 108-109: The test is brittle because it asserts the entire
app-link string equality (got["message_app_link"]) which can break if query
param ordering changes; instead parse the URL returned by FormatMessageItem (the
got["message_app_link"] value), assert the scheme/host/path are correct (e.g.,
scheme "https" and host "applink.feishu.cn" and path "/client/chat/open"), then
parse the query string into a map and assert required query keys/values (e.g.,
openChatId=oc_1 and position=12). Apply the same change to the other assertions
that compare full URLs (the checks around lines referencing
got["message_app_link"] at the other test cases) so all app-link assertions are
order-insensitive by validating host/path plus query key/value pairs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95db1870-a467-4634-9973-87449f86ccf4

📥 Commits

Reviewing files that changed from the base of the PR and between aaf8b36 and dfc3da4.

📒 Files selected for processing (2)
  • shortcuts/im/convert_lib/content_convert.go
  • shortcuts/im/convert_lib/content_media_misc_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • shortcuts/im/convert_lib/content_convert.go

@sammi-bytedance
sammi-bytedance force-pushed the feat/im-message-applink branch from dfc3da4 to 176b2f5 Compare May 7, 2026 06:38
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add sammi-bytedance/larksuite-cli#feat/im-message-applink -y -g

@sammi-bytedance
sammi-bytedance force-pushed the feat/im-message-applink branch from 794c22c to d1af175 Compare May 8, 2026 03:22
@codecov

codecov Bot commented May 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.20000% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.42%. Comparing base (88d7ec8) to head (d29c8fa).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
shortcuts/im/convert_lib/content_convert.go 82.92% 15 Missing and 6 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #668      +/-   ##
==========================================
+ Coverage   65.40%   65.42%   +0.02%     
==========================================
  Files         508      509       +1     
  Lines       46795    47019     +224     
==========================================
+ Hits        30605    30763     +158     
- Misses      13548    13592      +44     
- Partials     2642     2664      +22     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sammi-bytedance
sammi-bytedance force-pushed the feat/im-message-applink branch from d1af175 to 490076a Compare May 8, 2026 04:29
- Assemble applinks via net/url to ensure proper encoding
- Normalize message position values across more numeric types
- Avoid leaking null message_app_link; assemble when missing
- Update unit tests to assert URL semantics and cover edge cases

Change-Id: Ic473cb563c8a648c4f6677c32b25b9f371a0f84e
@sammi-bytedance
sammi-bytedance force-pushed the feat/im-message-applink branch from 490076a to d29c8fa Compare May 8, 2026 07:27
@YangJunzhou-01
YangJunzhou-01 merged commit e754b3b into larksuite:main May 8, 2026
17 checks passed
@liangshuo-1 liangshuo-1 mentioned this pull request May 8, 2026
2 tasks
tuxedomm pushed a commit to zhumiaoxin/cli that referenced this pull request Jun 6, 2026
- Assemble applinks via net/url to ensure proper encoding
- Normalize message position values across more numeric types
- Avoid leaking null message_app_link; assemble when missing
- Update unit tests to assert URL semantics and cover edge cases

Change-Id: Ic473cb563c8a648c4f6677c32b25b9f371a0f84e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/im PR touches the im domain size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants