Skip to content

feat: add markdown +diff shortcut#876

Open
wittam-01 wants to merge 2 commits into
mainfrom
feat/markdown-diff
Open

feat: add markdown +diff shortcut#876
wittam-01 wants to merge 2 commits into
mainfrom
feat/markdown-diff

Conversation

@wittam-01
Copy link
Copy Markdown
Collaborator

@wittam-01 wittam-01 commented May 14, 2026

Summary

Add a new markdown +diff shortcut for comparing Drive-native Markdown versions or comparing remote Markdown against a local .md file. This PR also documents the response fields and adds automated plus real bot-based verification for the supported diff scenarios.

Changes

  • Add markdown +diff for remote_vs_remote and remote_vs_local comparisons with unified diff output
  • Return structured JSON metadata including line counts, labels, and hunk summaries alongside pretty diff output
  • Reuse Drive version download behavior to diff specific historical versions or the latest remote content
  • Add unit tests, dry-run/live E2E coverage, and shortcut registration checks
  • Update lark-markdown and lark-drive skill docs, including full response field descriptions for markdown +diff

Test Plan

  • Targeted tests pass: go test ./shortcuts ./shortcuts/markdown ./tests/cli_e2e/markdown -count=1
  • Manual local verification confirms lark-cli markdown +diff works as expected with --as bot, covering:
    • remote version vs remote version
    • historical version vs latest remote version
    • latest remote version vs local Markdown file
    • specified remote version vs local Markdown file
    • no-difference pretty output (No differences.)

Related Issues

  • None

Summary by CodeRabbit

  • New Features

    • Added four Drive version commands: view version history, download a version, revert to a version, and delete a version.
    • Added a Markdown diff command to compare remote versions or remote vs local drafts.
  • Documentation

    • Added comprehensive reference docs and SKILL guidance for Drive version management and Markdown diff, with examples and parameter details.
  • Tests

    • Added extensive unit and end-to-end tests covering new Drive and Markdown workflows and dry-run behavior.

Review Change Stack

Change-Id: I87bb32c86e3c3362f541ccc6320c656eb795ec9b
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

Adds Drive file version management shortcuts (+version-history, +version-get, +version-revert, +version-delete) and a Markdown diff shortcut (+diff). Changes include implementations, validation, file I/O, unified-diff logic, helpers, unit and e2e tests, SKILL/reference docs, go.mod dependency, and a default endpoint update to feishu-pre.cn.

Changes

Drive Version Management

Layer / File(s) Summary
Endpoint configuration update
internal/core/types.go
Default resolved endpoints for Open and Accounts changed from *.feishu.cn to *.feishu-pre.cn.
Version shortcuts implementation
shortcuts/drive/drive_version.go
Validation/formatting utilities and implementations for +version-history, +version-get, +version-revert, +version-delete (pagination, download streaming, filename derivation, overwrite semantics).
Shortcuts registry & tests
shortcuts/drive/shortcuts.go, shortcuts/drive/shortcuts_test.go
Register new version shortcuts and update test expectations.
Unit tests for version shortcuts
shortcuts/drive/drive_version_test.go
Comprehensive unit tests for validation, response transforms, download/file-save modes, overwrite behavior, directory output, revert/delete request bodies, flag handling, and dry-run.
Integration and e2e tests
tests/cli_e2e/drive/drive_version_dryrun_test.go, tests/cli_e2e/drive/drive_version_workflow_test.go
Dry-run e2e checks (bot and user) and an optional live workflow test (create → overwrite → version-history).
User-facing documentation
skills/lark-drive/SKILL.md, skills/lark-drive/references/lark-drive-version-*.md
Added quick-decision guidance and four reference docs for Drive version commands.

Markdown Diff Feature

Layer / File(s) Summary
Markdown download helpers
shortcuts/markdown/helpers.go
Context-aware download helpers to fetch remote Markdown versions with optional version query and filename derivation.
Markdown diff implementation
shortcuts/markdown/markdown_diff.go
New markdown +diff shortcut: validate args, dry-run, download/read content, build unified diff using diffmatchpatch, summarize added/deleted counts and hunks, colorize/pretty-print, and emit JSON/pretty outputs.
Markdown unit tests
shortcuts/markdown/markdown_diff_test.go
Tests for validation, remote-vs-remote and remote-vs-local modes, multiple hunks, no-diff behavior, pretty colorization, and dry-run.
Shortcuts registry & registration tests
shortcuts/markdown/shortcuts.go, shortcuts/register_markdown_test.go, shortcuts/markdown/markdown_test.go
Register MarkdownDiff in Shortcuts() and update tests that assert mounted commands.
Markdown docs and e2e tests
skills/lark-markdown/SKILL.md, skills/lark-markdown/references/lark-markdown-diff.md, tests/cli_e2e/markdown/*
Add SKILL guidance, reference doc for +diff, dry-run e2e tests and workflow assertions to call markdown +diff between versions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • larksuite/cli#709: Also modifies Drive shortcut registry to add new subcommands.
  • larksuite/cli#696: Adds Drive shortcut entries; related to shortcut registration changes.
  • larksuite/cli#704: Related endpoint updates for feishu-pre.cn affecting Open/Accounts defaults.

Suggested labels

size/L, feature

Suggested reviewers

  • fangshuyu-768
  • liangshuo-1
  • liujinkun2025

"🐰
I hopped through code with eager cheer,
Versions stored and diffs appear,
Downloads saved and histories shown,
Revert, delete — the past is known,
CLI springs forward, bright and clear."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.63% 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 'feat: add markdown +diff shortcut' is clear, specific, and directly reflects the main change: adding a new markdown diff feature. It accurately summarizes the primary contribution of this PR.
Description check ✅ Passed The PR description follows the template structure with Summary, Changes, Test Plan, and Related Issues sections. All required sections are present and completed with sufficient detail about the implementation and testing approach.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/markdown-diff

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/ccm PR touches the ccm domain size/L Large or sensitive change across domains or core paths labels May 14, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/cli_e2e/drive/drive_version_workflow_test.go (1)

75-86: ⚡ Quick win

Strengthen history assertions to validate actual versioning behavior.

Right now this only checks success envelopes. After the overwrite step, assert the history payload contains expected version records (for example, at least one/two items) so this test verifies behavior, not just endpoint availability.

As per coding guidelines, live E2E tests for new flows “must validate real API round-trips, be self-contained (create->use->cleanup)”.

🤖 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 `@tests/cli_e2e/drive/drive_version_workflow_test.go` around lines 75 - 86, The
test currently only checks envelopes; update the history assertions to validate
actual version records by parsing historyResult.Stdout (from clie2e.RunCmd
response) into the history payload returned by the "+version-history" call and
assert expected versioning behavior (e.g., that the returned versions
slice/array contains at least N entries after the overwrite step, or contains
entries matching known metadata); locate the test around historyResult and use
Request Args containing "+version-history" and the fileToken to extract and
unmarshal the payload, then add assertions (using require/Assert helpers) to
check length and/or expected fields to ensure a real round-trip was validated.
shortcuts/drive/drive_version_test.go (1)

180-185: ⚡ Quick win

Prefer structured JSON assertions over substring matching.

These checks are fragile against formatting-only JSON output changes. Decode stdout and assert fields directly to keep tests behavior-focused.

Refactor pattern (example)
- if !strings.Contains(stdout.String(), `"file_name": "report-v7.md"`) {
- 	t.Fatalf("stdout missing file_name: %s", stdout.String())
- }
- if !strings.Contains(stdout.String(), `"content": "# hello\n"`) {
- 	t.Fatalf("stdout missing content: %s", stdout.String())
- }
+ var envelope struct {
+ 	Data struct {
+ 		FileName string `json:"file_name"`
+ 		Content  string `json:"content"`
+ 	} `json:"data"`
+ }
+ if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
+ 	t.Fatalf("unmarshal stdout: %v", err)
+ }
+ if envelope.Data.FileName != "report-v7.md" {
+ 	t.Fatalf("file_name = %q, want %q", envelope.Data.FileName, "report-v7.md")
+ }
+ if envelope.Data.Content != "# hello\n" {
+ 	t.Fatalf("content = %q, want %q", envelope.Data.Content, "# hello\n")
+ }

Also applies to: 211-216, 355-357, 386-388

🤖 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/drive/drive_version_test.go` around lines 180 - 185, Replace
fragile substring checks on stdout.String() with proper JSON decoding and field
assertions: parse stdout (use json.Unmarshal into a map[string]interface{} or
small struct) and assert that the "version" key equals "7633658129540910621" and
that "saved_path" exists/non-empty; update the checks around the stdout variable
in the current test and replicate the same pattern for the other occurrences
referenced (lines 211-216, 355-357, 386-388) so tests validate structured fields
rather than substrings.
🤖 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 `@tests/cli_e2e/drive/drive_version_dryrun_test.go`:
- Around line 143-207: Add a user-mode no-`--output` test to
TestDriveVersionDryRunSupportsUser: in the tests slice (variable tests) add a
new case (e.g., name "get-stdout-default") that mirrors the existing "get" case
but omits the "--output" arg (keep "--dry-run") and include the same expectation
used in TestDriveVersionGetDryRunWithoutOutputUsesStdout (e.g., wantContains
should check that the dry-run payload resolves output to stdout, such as
`"output": "-"` or the same marker used in the bot test).

---

Nitpick comments:
In `@shortcuts/drive/drive_version_test.go`:
- Around line 180-185: Replace fragile substring checks on stdout.String() with
proper JSON decoding and field assertions: parse stdout (use json.Unmarshal into
a map[string]interface{} or small struct) and assert that the "version" key
equals "7633658129540910621" and that "saved_path" exists/non-empty; update the
checks around the stdout variable in the current test and replicate the same
pattern for the other occurrences referenced (lines 211-216, 355-357, 386-388)
so tests validate structured fields rather than substrings.

In `@tests/cli_e2e/drive/drive_version_workflow_test.go`:
- Around line 75-86: The test currently only checks envelopes; update the
history assertions to validate actual version records by parsing
historyResult.Stdout (from clie2e.RunCmd response) into the history payload
returned by the "+version-history" call and assert expected versioning behavior
(e.g., that the returned versions slice/array contains at least N entries after
the overwrite step, or contains entries matching known metadata); locate the
test around historyResult and use Request Args containing "+version-history" and
the fileToken to extract and unmarshal the payload, then add assertions (using
require/Assert helpers) to check length and/or expected fields to ensure a real
round-trip was validated.
🪄 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: 7f3ed2a7-86ac-406d-927a-c0a818e6c2f4

📥 Commits

Reviewing files that changed from the base of the PR and between a18504b and cdd96f3.

📒 Files selected for processing (12)
  • internal/core/types.go
  • shortcuts/drive/drive_version.go
  • shortcuts/drive/drive_version_test.go
  • shortcuts/drive/shortcuts.go
  • shortcuts/drive/shortcuts_test.go
  • skills/lark-drive/SKILL.md
  • skills/lark-drive/references/lark-drive-version-delete.md
  • skills/lark-drive/references/lark-drive-version-get.md
  • skills/lark-drive/references/lark-drive-version-history.md
  • skills/lark-drive/references/lark-drive-version-revert.md
  • tests/cli_e2e/drive/drive_version_dryrun_test.go
  • tests/cli_e2e/drive/drive_version_workflow_test.go

Comment on lines +143 to +207
func TestDriveVersionDryRunSupportsUser(t *testing.T) {
clie2e.SkipWithoutUserToken(t)
setDriveDryRunConfigEnv(t)

tests := []struct {
name string
args []string
wantContains []string
}{
{
name: "history",
args: []string{
"drive", "+version-history",
"--file-token", "boxcnHistoryDryRunUser",
"--limit", "5",
"--cursor", "1777013761763",
"--dry-run",
},
wantContains: []string{
"/open-apis/drive/v1/files/boxcnHistoryDryRunUser/history",
`"only_tag": true`,
`"page_size": 5`,
},
},
{
name: "get",
args: []string{
"drive", "+version-get",
"--file-token", "boxcnVersionDryRunUser",
"--version", "7633658129540910621",
"--output", "./artifact-user.bin",
"--dry-run",
},
wantContains: []string{
"/open-apis/drive/v1/files/boxcnVersionDryRunUser/download",
`"version": "7633658129540910621"`,
`"output": "./artifact-user.bin"`,
},
},
{
name: "revert",
args: []string{
"drive", "+version-revert",
"--file-token", "boxcnVersionDryRunUser",
"--version", "7633658129540910621",
"--dry-run",
},
wantContains: []string{
"/open-apis/drive/v1/files/boxcnVersionDryRunUser/revert",
`"version": "7633658129540910621"`,
},
},
{
name: "delete",
args: []string{
"drive", "+version-delete",
"--file-token", "boxcnVersionDryRunUser",
"--version", "7633658129540910621",
"--dry-run",
},
wantContains: []string{
"/open-apis/drive/v1/files/boxcnVersionDryRunUser/version_del",
`"version": "7633658129540910621"`,
},
},
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 | 🟡 Minor | ⚡ Quick win

Add user-mode parity coverage for +version-get default stdout behavior.

TestDriveVersionGetDryRunWithoutOutputUsesStdout currently validates bot mode only. Please add the same no---output case in the user table to catch auth-specific regressions in default output resolution.

🤖 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 `@tests/cli_e2e/drive/drive_version_dryrun_test.go` around lines 143 - 207, Add
a user-mode no-`--output` test to TestDriveVersionDryRunSupportsUser: in the
tests slice (variable tests) add a new case (e.g., name "get-stdout-default")
that mirrors the existing "get" case but omits the "--output" arg (keep
"--dry-run") and include the same expectation used in
TestDriveVersionGetDryRunWithoutOutputUsesStdout (e.g., wantContains should
check that the dry-run payload resolves output to stdout, such as `"output":
"-"` or the same marker used in the bot test).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 14, 2026

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add larksuite/cli#feat/markdown-diff -y -g

@github-actions github-actions Bot added size/XL Architecture-level or global-impact change and removed size/L Large or sensitive change across domains or core paths labels May 14, 2026
@wittam-01 wittam-01 changed the title feat: add drive version shortcut feat: add markdown +diff shortcut May 14, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@shortcuts/markdown/markdown_diff_test.go`:
- Around line 19-272: For each test in this file (e.g.,
TestMarkdownDiffRejectsUnsupportedFormat,
TestMarkdownDiffRejectsToVersionWithoutFromVersion,
TestMarkdownDiffRemoteVsRemoteJSON, TestMarkdownDiffRemoteVsLocalPretty,
TestMarkdownDiffRemoteVsRemoteJSONMultipleHunks,
TestMarkdownDiffNoChangesPretty, TestMarkdownDiffDryRunRemoteVsLocal) set an
isolated config dir before calling cmdutil.TestFactory by adding
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) immediately at the start of
the test; this ensures config-state isolation across tests and should be applied
consistently in each test function that currently calls cmdutil.TestFactory(...)
in this file.
🪄 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: 275941d6-61a2-4fd1-9b17-c992911af119

📥 Commits

Reviewing files that changed from the base of the PR and between cdd96f3 and c9cdf96.

📒 Files selected for processing (11)
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/markdown/markdown_test.go
  • shortcuts/markdown/shortcuts.go
  • shortcuts/register_markdown_test.go
  • skills/lark-drive/SKILL.md
  • skills/lark-markdown/SKILL.md
  • skills/lark-markdown/references/lark-markdown-diff.md
  • tests/cli_e2e/markdown/markdown_dryrun_test.go
  • tests/cli_e2e/markdown/markdown_workflow_test.go
✅ Files skipped from review due to trivial changes (4)
  • shortcuts/markdown/markdown_test.go
  • skills/lark-markdown/SKILL.md
  • shortcuts/markdown/shortcuts.go
  • skills/lark-markdown/references/lark-markdown-diff.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • skills/lark-drive/SKILL.md

Comment on lines +19 to +272
func TestMarkdownDiffRejectsUnsupportedFormat(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())

err := mountAndRunMarkdown(t, MarkdownDiff, []string{
"+diff",
"--file-token", "box_md_diff",
"--from-version", "7633658129540910621",
"--format", "table",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "only supports --format json or pretty") {
t.Fatalf("expected format validation error, got %v", err)
}
}

func TestMarkdownDiffRejectsToVersionWithoutFromVersion(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())

err := mountAndRunMarkdown(t, MarkdownDiff, []string{
"+diff",
"--file-token", "box_md_diff",
"--to-version", "7633658129540910628",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "--to-version requires --from-version") {
t.Fatalf("expected version validation error, got %v", err)
}
}

func TestMarkdownDiffRemoteVsRemoteJSON(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910621",
Status: 200,
RawBody: []byte("# Title\n\n- alpha\n- beta\n"),
Headers: http.Header{
"Content-Disposition": []string{`attachment; filename="README.md"`},
},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910628",
Status: 200,
RawBody: []byte("# Title\n\n- alpha\n- beta updated\n- gamma\n"),
Headers: http.Header{
"Content-Disposition": []string{`attachment; filename="README.md"`},
},
})

err := mountAndRunMarkdown(t, MarkdownDiff, []string{
"+diff",
"--file-token", "box_md_diff",
"--from-version", "7633658129540910621",
"--to-version", "7633658129540910628",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

var env struct {
OK bool `json:"ok"`
Data struct {
Changed bool `json:"changed"`
Mode string `json:"mode"`
FromVersion string `json:"from_version"`
ToVersion string `json:"to_version"`
AddedLines int `json:"added_lines"`
DeletedLines int `json:"deleted_lines"`
Diff string `json:"diff"`
Hunks []markdownDiffHunk `json:"hunks"`
} `json:"data"`
}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("json unmarshal error: %v\n%s", err, stdout.String())
}
if !env.OK {
t.Fatalf("expected ok=true, got false: %s", stdout.String())
}
if !env.Data.Changed {
t.Fatalf("expected changed=true: %s", stdout.String())
}
if env.Data.Mode != markdownDiffModeRemoteVsRemote {
t.Fatalf("mode = %q, want %q", env.Data.Mode, markdownDiffModeRemoteVsRemote)
}
if env.Data.FromVersion != "7633658129540910621" || env.Data.ToVersion != "7633658129540910628" {
t.Fatalf("versions = %q -> %q", env.Data.FromVersion, env.Data.ToVersion)
}
if env.Data.AddedLines != 2 || env.Data.DeletedLines != 1 {
t.Fatalf("added/deleted = %d/%d, want 2/1", env.Data.AddedLines, env.Data.DeletedLines)
}
if len(env.Data.Hunks) != 1 {
t.Fatalf("len(hunks) = %d, want 1", len(env.Data.Hunks))
}
if !strings.Contains(env.Data.Diff, "@@") || !strings.Contains(env.Data.Diff, "+- gamma") {
t.Fatalf("diff missing expected content: %s", env.Data.Diff)
}
}

func TestMarkdownDiffRemoteVsLocalPretty(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_diff/download",
Status: 200,
RawBody: []byte("# Title\n\nhello old\n"),
Headers: http.Header{
"Content-Disposition": []string{`attachment; filename="README.md"`},
},
})

tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
if err := os.WriteFile("local.md", []byte("# Title\n\nhello new\n"), 0o644); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}

err := mountAndRunMarkdown(t, MarkdownDiff, []string{
"+diff",
"--file-token", "box_md_diff",
"--file", "./local.md",
"--format", "pretty",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(stdout.String(), "@@") {
t.Fatalf("pretty output missing hunk header: %s", stdout.String())
}
if !strings.Contains(stdout.String(), output.Red+"-hello old"+output.Reset) {
t.Fatalf("pretty output missing removed line color: %q", stdout.String())
}
if !strings.Contains(stdout.String(), output.Green+"+hello new"+output.Reset) {
t.Fatalf("pretty output missing added line color: %q", stdout.String())
}
}

func TestMarkdownDiffRemoteVsRemoteJSONMultipleHunks(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910621",
Status: 200,
RawBody: []byte("line1\nline2\nline3\nline4\nline5\nline6\n"),
Headers: http.Header{
"Content-Disposition": []string{`attachment; filename="README.md"`},
},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910628",
Status: 200,
RawBody: []byte("line1\nline2 changed\nline3\nline4\nline5 changed\nline6\n"),
Headers: http.Header{
"Content-Disposition": []string{`attachment; filename="README.md"`},
},
})

err := mountAndRunMarkdown(t, MarkdownDiff, []string{
"+diff",
"--file-token", "box_md_diff",
"--from-version", "7633658129540910621",
"--to-version", "7633658129540910628",
"--context-lines", "0",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

var env struct {
OK bool `json:"ok"`
Data struct {
Changed bool `json:"changed"`
AddedLines int `json:"added_lines"`
DeletedLines int `json:"deleted_lines"`
Hunks []markdownDiffHunk `json:"hunks"`
Diff string `json:"diff"`
} `json:"data"`
}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("json unmarshal error: %v\n%s", err, stdout.String())
}
if !env.OK || !env.Data.Changed {
t.Fatalf("expected changed=true: %s", stdout.String())
}
if env.Data.AddedLines != 2 || env.Data.DeletedLines != 2 {
t.Fatalf("added/deleted = %d/%d, want 2/2", env.Data.AddedLines, env.Data.DeletedLines)
}
if len(env.Data.Hunks) != 2 {
t.Fatalf("len(hunks) = %d, want 2", len(env.Data.Hunks))
}
if !strings.Contains(env.Data.Diff, "-line2") || !strings.Contains(env.Data.Diff, "+line5 changed") {
t.Fatalf("diff missing expected content: %s", env.Data.Diff)
}
}

func TestMarkdownDiffNoChangesPretty(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910621",
Status: 200,
RawBody: []byte("# Title\n"),
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_diff/download",
Status: 200,
RawBody: []byte("# Title\n"),
})

err := mountAndRunMarkdown(t, MarkdownDiff, []string{
"+diff",
"--file-token", "box_md_diff",
"--from-version", "7633658129540910621",
"--format", "pretty",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := strings.TrimSpace(stdout.String()); got != "No differences." {
t.Fatalf("pretty no-change output = %q, want %q", got, "No differences.")
}
}

func TestMarkdownDiffDryRunRemoteVsLocal(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())

tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
localPath := filepath.Join(".", "local.md")
if err := os.WriteFile(localPath, []byte("# local\n"), 0o644); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}

err := mountAndRunMarkdown(t, MarkdownDiff, []string{
"+diff",
"--file-token", "box_md_diff",
"--file", localPath,
"--dry-run",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(stdout.String(), "/open-apis/drive/v1/files/:file_token/download") && !strings.Contains(stdout.String(), "/open-apis/drive/v1/files/box_md_diff/download") {
t.Fatalf("dry-run missing download call: %s", stdout.String())
}
if !strings.Contains(stdout.String(), `"local_file": "local.md"`) && !strings.Contains(stdout.String(), `"local_file": "./local.md"`) {
t.Fatalf("dry-run missing local file metadata: %s", stdout.String())
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add per-test config-dir isolation before creating the factory.

Each test should set an isolated config dir before cmdutil.TestFactory(...) to avoid config-state bleed across tests.

🔧 Suggested pattern
 func TestMarkdownDiffRejectsUnsupportedFormat(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())

Apply the same line to the other test functions in this file.

As per coding guidelines **/*_test.go: Use t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) to isolate config state in tests.

🤖 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/markdown/markdown_diff_test.go` around lines 19 - 272, For each
test in this file (e.g., TestMarkdownDiffRejectsUnsupportedFormat,
TestMarkdownDiffRejectsToVersionWithoutFromVersion,
TestMarkdownDiffRemoteVsRemoteJSON, TestMarkdownDiffRemoteVsLocalPretty,
TestMarkdownDiffRemoteVsRemoteJSONMultipleHunks,
TestMarkdownDiffNoChangesPretty, TestMarkdownDiffDryRunRemoteVsLocal) set an
isolated config dir before calling cmdutil.TestFactory by adding
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) immediately at the start of
the test; this ensures config-state isolation across tests and should be applied
consistently in each test function that currently calls cmdutil.TestFactory(...)
in this file.

Change-Id: I475e0ba99b77f17535c0ac65622e38ddcf1ffd12
@wittam-01 wittam-01 force-pushed the feat/markdown-diff branch from c9cdf96 to f69c3ba Compare May 14, 2026 04:33
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@shortcuts/markdown/markdown_diff.go`:
- Around line 341-344: The current logic writes op.Content and then naively
appends a '\n' when the content lacks a trailing newline, losing the standard "\
No newline at end of file" marker; change the behavior in the diff generation
(around b.WriteString(op.Content)) so that if !strings.HasSuffix(op.Content,
"\n") you do NOT append a raw '\n' but instead append the unified-diff marker
line (for example b.WriteString("\\ No newline at end of file\n") or equivalent)
after writing the content; reference op.Content and the bytes.Buffer variable b
to locate where to replace the existing b.WriteByte('\n') 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: f1b23586-26ed-4bb6-9899-45d9a8aa3331

📥 Commits

Reviewing files that changed from the base of the PR and between c9cdf96 and f69c3ba.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • go.mod
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/markdown/markdown_test.go
  • shortcuts/markdown/shortcuts.go
  • shortcuts/register_markdown_test.go
  • skills/lark-drive/SKILL.md
  • skills/lark-markdown/SKILL.md
  • skills/lark-markdown/references/lark-markdown-diff.md
  • tests/cli_e2e/markdown/markdown_dryrun_test.go
  • tests/cli_e2e/markdown/markdown_workflow_test.go
✅ Files skipped from review due to trivial changes (4)
  • shortcuts/markdown/shortcuts.go
  • skills/lark-markdown/references/lark-markdown-diff.md
  • skills/lark-drive/SKILL.md
  • skills/lark-markdown/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • shortcuts/register_markdown_test.go
  • shortcuts/markdown/markdown_test.go
  • shortcuts/markdown/helpers.go
  • tests/cli_e2e/markdown/markdown_workflow_test.go
  • tests/cli_e2e/markdown/markdown_dryrun_test.go
  • shortcuts/markdown/markdown_diff_test.go

Comment on lines +341 to +344
b.WriteString(op.Content)
if !strings.HasSuffix(op.Content, "\n") {
b.WriteByte('\n')
}
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

Preserve missing trailing newlines in the unified diff.

These lines currently append a newline unconditionally when op.Content lacks \n, but they never emit the standard \ No newline at end of file marker. That makes a file ending with foo and a file ending with foo\n produce the same diff text, so the output is no longer a faithful unified diff.

Suggested fix
 			b.WriteByte(byte(prefix))
 			b.WriteString(op.Content)
 			if !strings.HasSuffix(op.Content, "\n") {
 				b.WriteByte('\n')
+				b.WriteString(`\ No newline at end of file`)
+				b.WriteByte('\n')
 			}
📝 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
b.WriteString(op.Content)
if !strings.HasSuffix(op.Content, "\n") {
b.WriteByte('\n')
}
b.WriteString(op.Content)
if !strings.HasSuffix(op.Content, "\n") {
b.WriteByte('\n')
b.WriteString(`\ No newline at end of file`)
b.WriteByte('\n')
}
🤖 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/markdown/markdown_diff.go` around lines 341 - 344, The current
logic writes op.Content and then naively appends a '\n' when the content lacks a
trailing newline, losing the standard "\ No newline at end of file" marker;
change the behavior in the diff generation (around b.WriteString(op.Content)) so
that if !strings.HasSuffix(op.Content, "\n") you do NOT append a raw '\n' but
instead append the unified-diff marker line (for example b.WriteString("\\ No
newline at end of file\n") or equivalent) after writing the content; reference
op.Content and the bytes.Buffer variable b to locate where to replace the
existing b.WriteByte('\n') behavior.

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

Labels

domain/ccm PR touches the ccm domain size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant