Skip to content

refactor: introduce FileIO extension to abstract file transfer operations#297

Open
tuxedomm wants to merge 17 commits intomainfrom
feat/fileio-extension-refactor-plugin
Open

refactor: introduce FileIO extension to abstract file transfer operations#297
tuxedomm wants to merge 17 commits intomainfrom
feat/fileio-extension-refactor-plugin

Conversation

@tuxedomm
Copy link
Copy Markdown
Collaborator

@tuxedomm tuxedomm commented Apr 7, 2026

Summary

Introduce a FileIO extension layer that abstracts file transfer operations (Open/Stat/Save) in shortcuts behind a replaceable interface, eliminating direct os package dependencies and providing an extension point for server mode and other scenarios.

Changes

  • Add extension/fileio package with Provider/FileIO/File interfaces and registry
  • Add LocalFileIO default implementation with built-in path validation and atomic writes
  • Migrate all download shortcuts to use FileIO.Save
  • Migrate all upload/read shortcuts to use FileIO.Open/FileIO.Stat
  • Move FileIO resolution to runtime — Factory holds Provider only
  • Add RuntimeContext.ValidatePath to deduplicate Stat-based path checks
  • Privatize internal helpers (AtomicWriteFromReader/SafeInputPath/SafeLocalFlagPath)
  • Add lint rules to prevent shortcuts from depending on vfs/localfileio/os directly
  • Tighten file write permissions to 0700/0600

Test Plan

  • Unit tests pass
  • Manual local verification confirms the lark xxx command works as expected

Related Issues

  • None

Summary by CodeRabbit

  • Refactor
    • Introduced extensible file I/O provider system to enable future customization of file handling
    • Consolidated path validation and safety mechanisms across the codebase into a unified abstraction
    • Unified error handling for file operations across all CLI commands
    • Enhanced linter configuration to enforce filesystem operation boundaries

Introduce extension/fileio package with Provider + FileIO interface
(Open/Stat/Save) following the same pattern as extension/credential.
Default LocalFileIO implementation in internal/cmdutil handles path
validation, directory creation, and atomic writes.

This enables internal forks to inject custom FileIO implementations
(e.g. streaming) without modifying shortcut code.

Change-Id: I20dfbc1a597cb638690ead0c398d003f7eb5cae6
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 806c2c24-5fc7-49df-97b6-5f764ee25b0f

📥 Commits

Reviewing files that changed from the base of the PR and between 8867dca and 1c30e74.

📒 Files selected for processing (1)
  • shortcuts/common/runner_jq_test.go
✅ Files skipped from review due to trivial changes (1)
  • shortcuts/common/runner_jq_test.go

📝 Walkthrough

Walkthrough

Adds a pluggable file I/O abstraction (extension/fileio) with a local filesystem provider and threads it through factory/runtime, client response saving, many shortcuts, mail/EML flows, and tests; delegates prior path-validation and atomic-write helpers to the new localfileio implementation and tightens linter rules forbidding direct VFS imports in shortcuts.

Changes

Cohort / File(s) Summary
Linter config
/.golangci.yml
Enable depguard; block internal/vfs imports from **/shortcuts/**; consolidate forbidigo filesystem/function bans into regex groups.
File I/O API & registry
extension/fileio/types.go, extension/fileio/registry.go
Add fileio interfaces (Provider, FileIO, File, SaveResult, SaveOptions) and global Register/GetProvider registry.
Factory & bootstrap
internal/cmdutil/factory.go, internal/cmdutil/factory_default.go, internal/cmdutil/factory_default_test.go, internal/cmdutil/testing.go
Add FileIOProvider to Factory, ResolveFileIO(ctx) method, initialize provider from registry in NewDefault, and test helpers (TestChdir) and tests for lazy resolution.
Client response handling
internal/client/response.go, internal/client/response_test.go
Add ResponseOptions.FileIO; route saves through fio.Save(...); metadata now from fio.ResolvePath and result.Size().
Local filesystem provider
internal/vfs/localfileio/...
Implement local provider: Provider, LocalFileIO with Open/Stat/ResolvePath/Save, path-safety, input sanitization, atomic-write helpers, and self-register in init.
Remove/delegate validators
internal/validate/path.go, internal/validate/atomicwrite.go
Remove in-file path/atomic helpers; delegate SafeOutputPath and AtomicWrite to localfileio.
Runtime / runner plumbing
shortcuts/common/runner.go, shortcuts/common/*_test.go
Add RuntimeContext.FileIO(), ValidatePath, ResolveSavePath; replace vfs/validate reads with FileIO().Open/Stat; tests updated and blank-import localfileio for registration.
Shortcuts — input parsing & file ops
shortcuts/base/*, shortcuts/*/* (many files)
Thread fileio.FileIO into JSON loaders/parsers and shortcut call sites; replace validate.SafeInputPath/vfs.* and atomic writes with runtime.FileIO().Open/Stat/Save and ResolveSavePath; remove EnsureWritableFile/ValidateSafeOutputDir usage.
Mail & EML builder
shortcuts/mail/*, shortcuts/mail/emlbuilder/*
EML builder gains WithFileIO; Apply/patch flows accept fileio.FileIO and use fio.Open/Stat; tests updated to pass localfileio.
Drive/Docs/Minutes/Sheets/VC/IM flows
shortcuts/drive/*, shortcuts/doc/*, shortcuts/minutes/*, shortcuts/sheets/*, shortcuts/vc/*, shortcuts/im/*
Refactor download/upload/import/export helpers to use runtime.FileIO() for Stat/Open/Save; overwrite and error semantics adjusted; persistence moved to provider Save.
Tests
multiple *_test.go
Numerous tests updated to provide localfileio.LocalFileIO{} or adapt expectations (size types, messages); removed tests for deleted helpers.

Sequence Diagram(s)

sequenceDiagram
    participant Cmd as Shortcut/Command
    participant RT as Runtime/Factory
    participant Prov as fileio.Provider
    participant FIO as fileio.FileIO
    participant Client as internal client (HandleResponse)
    participant Local as localfileio (implementation)

    Cmd->>RT: Runtime.FileIO()
    RT->>Prov: ResolveFileIO(ctx)
    Prov-->>RT: FIO (may be Local)
    RT-->>Cmd: return FIO

    Cmd->>Client: call HandleResponse(..., FileIO: FIO)
    Client->>FIO: Save(path, SaveOptions, body)
    FIO->>Local: (if local) validate+atomic-write via local helpers
    Local-->>FIO: SaveResult(size)
    FIO-->>Client: saved path/size
    Client-->>Cmd: output metadata (saved_path, size_bytes)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • chanthuang
  • liangshuo-1
  • fangshuyu-768

Poem

🐰 Files once scattered, paths once raw,
A provider hops in, tidy and all,
Resolve, Open, Save — a unified song,
Local or remote, it carries along,
The CLI hums cleaner, I nibble a carrot. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing a FileIO extension to abstract file transfer operations.
Description check ✅ Passed The description includes a comprehensive summary, detailed list of changes, test plan with checkbox items, and related issues section, matching the repository template well.

✏️ 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/fileio-extension-refactor-plugin

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

@github-actions github-actions bot added domain/base PR touches the base domain domain/ccm PR touches the ccm domain domain/im PR touches the im domain domain/mail PR touches the mail domain domain/vc PR touches the vc domain size/XL Architecture-level or global-impact change labels Apr 7, 2026
@tuxedomm tuxedomm closed this Apr 7, 2026
@tuxedomm tuxedomm deleted the feat/fileio-extension-refactor-plugin branch April 7, 2026 13:05
tuxedomm and others added 12 commits April 7, 2026 21:07
Replace direct os.MkdirAll + validate.AtomicWrite with FileIO.Save
in all download shortcuts: drive +download, docs +media-download,
drive +export, im +messages-resources-download, sheets +export,
vc +notes, and internal/client/response.go SaveResponse.

LocalFileIO.Save now uses AtomicWriteFromReader for streaming writes
instead of io.ReadAll, avoiding full in-memory buffering.

Path validation and overwrite checks use FileIO.Stat instead of
direct validate.SafeOutputPath + os.Stat calls.

Change-Id: I516d2c9911f6d7bc464b1abfa7d6b6050270debf
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace direct os.Stat + os.Open + os.ReadFile with FileIO.Stat +
FileIO.Open across all upload and file-read shortcuts: drive, doc, im,
base, mail (draft/patch, emlbuilder, helpers, draft_edit).

Add PlainFileIO in cmdutil/testing.go for tests that need absolute
paths (e.g. t.TempDir()) without path validation. Production code
continues to use LocalFileIO with SafeInputPath enforcement.

Change-Id: I7afd62a39da253896ef089dfb3954f293039b967
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clean up leftover conflict markers from Phase 3 merge and remove
unused vfs/bytes imports now that FileIO abstraction is in place.

Change-Id: Id4a9435a4db5f24eefda2e2112454d99d685d6f6
Change-Id: I30647cd56089668bcab015e14ee226a4f73af1ee
Change-Id: I69a2c63497a755e2137dd5a6808c554ce2f4d9fe
…os/vfs bypasses

- Move LocalFileIO from internal/cmdutil to internal/vfs/localfileio
- Move SafeInputPath/SafeOutputPath/AtomicWrite from validate to localfileio,
  keep thin wrappers in validate for backward compatibility
- Fix shortcuts bypassing FileIO: drive_upload os.Open, drive_import vfs.Stat,
  runner vfs.ReadFile, minutes_download manual SafeOutputPath+MkdirAll+AtomicWrite
- Remove dead code: EnsureWritableFile, ValidateSafeOutputDir
- RuntimeContext.FileIO() falls back to global registered provider when Factory
  or FileIOProvider is nil

Change-Id: I2e00711c648499fd679321ffab7da353c496d246
…tion in shortcuts

- im: add validateMediaFlagPath helper using FileIO.Stat, replace 20 SafeLocalFlagPath calls
- im/helpers.go: remove redundant SafeInputPath (FileIO.Open does it internally)
- sheets/sheet_export.go: replace SafeOutputPath with FileIO.Stat for early path validation

Change-Id: Iebad72763fc30632f2b7169a4379c861df68c140
…leio

- Remove validate.SafeInputPath and validate.SafeLocalFlagPath (no external callers)
- Downgrade to unexported safeInputPath/safeLocalFlagPath in localfileio
- Replace testLocalFileIO in response_test.go with localfileio.LocalFileIO

Change-Id: I9fca2536abf028c2f81497a5551ac8d0558a5eb4
…dateMediaFlagPath test

- Make AtomicWriteFromReader private (only used inside localfileio.Save)
- Remove dead safeLocalFlagPath function and its tests
- Add unit test for validateMediaFlagPath covering skip/accept/reject cases

Change-Id: I3aa6959279b51c7d8d3ff87402153e5c52f61522
…path checks

Replace 4 instances of the repeated FileIO().Stat + os.IsNotExist pattern
with a single runtime.ValidatePath(path) call. drive_download retains
inline Stat for its combined validate+overwrite check.

Change-Id: I69e74c7e82b14f92dfb53d851c911fde301b4714
- depguard: block shortcuts from importing internal/vfs and localfileio
- forbidigo: add missing os functions (Chtimes, CopyFS, MkdirTemp) and
  filepath functions that access filesystem (EvalSymlinks, Walk, Glob, Abs)
- Consolidate forbidigo patterns using regex alternation to reduce config size

Change-Id: I39bd750c5e74fd753751dd4220ec5eb124a2a0d4
Change-Id: I6206a8622bdc65caebf29a57d9f0e4ec487685eb
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add larksuite/cli#feat/fileio-extension-refactor-plugin -y -g

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 7, 2026

Greptile Summary

This PR introduces a clean FileIO extension layer that abstracts file-transfer operations behind a replaceable interface, successfully eliminating direct os/vfs dependencies from all download and upload shortcuts. The core abstraction (extension/fileio), local implementation (localfileio), and factory wiring are well-structured. The previously flagged issues around saved_path returning relative paths and parseStringList's nil-fio trap have been addressed by ResolveSavePath and the simplified comma-split implementation respectively.

Confidence Score: 4/5

Safe to merge with minor UX regressions in error messages; no functional breakage or data-loss risk.

All P0/P1 concerns from prior threads appear addressed (saved_path now uses ResolveSavePath, parseStringList nil-fio trap eliminated). Remaining findings are P2: error messages on invalid output paths say '--file' instead of '--output', and invalid paths in overwrite checks are silently forwarded to Save with a less informative error. These are UX regressions, not correctness bugs, which keeps the score at 4 rather than 5.

shortcuts/drive/drive_download.go, shortcuts/doc/doc_media_download.go, shortcuts/sheets/sheet_export.go, shortcuts/minutes/minutes_download.go, shortcuts/drive/drive_export_common.go — all use FileIO.Stat for output path validation which gives '--file' error messages.

Important Files Changed

Filename Overview
extension/fileio/types.go Defines FileIO/Provider/File/SaveResult interfaces; clean design with SaveOptions for future server-mode extensibility.
extension/fileio/registry.go Thread-safe global registry for FileIO providers; straightforward mutex-guarded singleton.
internal/vfs/localfileio/localfileio.go LocalFileIO implementation; Stat uses safeInputPath for all paths, causing '--file' prefix in errors on output path validation calls.
internal/vfs/localfileio/path.go Path safety with symlink-aware ancestor resolution; good TOCTOU protection for both input and output paths.
internal/vfs/localfileio/atomicwrite.go Atomic write via temp-file-and-rename pattern with fsync; minor harmless double-close on rename failure.
shortcuts/common/runner.go Adds FileIO/ValidatePath/ResolveSavePath to RuntimeContext; FileIO() may return nil if no provider is registered.
shortcuts/drive/drive_download.go Migrated to FileIO.Save; overwrite check via Stat gives '--file' error messages on unsafe output paths.
shortcuts/doc/doc_media_download.go Migrated to FileIO.Save; overwrite check for finalPath silently ignores invalid path errors before forwarding to Save.
shortcuts/drive/drive_export_common.go saveContentToOutputDir now accepts FileIO; overwrite Stat check passes through on invalid paths, deferring rejection to Save.
shortcuts/minutes/minutes_download.go Migrated to FileIO; ResolveSavePath used for saved_path; consistent '--file' message issue on output path validation.
shortcuts/sheets/sheet_export.go Migrated to FileIO.Save; ValidatePath early check gives '--file' error prefix on unsafe output paths.
internal/cmdutil/factory.go Adds FileIOProvider field and nil-safe ResolveFileIO method to Factory.
internal/cmdutil/testing.go TestFactory seeds FileIOProvider from global registry; TestChdir helper added for relative-path tests.
.golangci.yml New depguard rule prevents shortcuts from importing vfs/localfileio directly; two existing callers outside this PR (event/pipeline.go, mail/mail_watch.go) not yet migrated.
shortcuts/im/helpers.go Migrated file uploads to FileIO.Open/Stat; readOggDuration/readMp4Duration now accept fileio.File interface.
shortcuts/base/base_shortcut_helpers.go loadJSONInput/parseObjectList/parseJSONValue thread fio through; nil-fio safe for non-@ inputs only.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI Command
    participant RC as RuntimeContext
    participant Factory as Factory
    participant Registry as fileio.Registry
    participant FIO as LocalFileIO
    participant FS as Filesystem

    Note over CLI,FS: Initialization
    Registry->>Registry: init() registers LocalFileIO Provider
    Factory->>Registry: GetProvider()
    Registry-->>Factory: LocalFileIO.Provider

    Note over CLI,FS: Download / Upload
    CLI->>RC: runtime.FileIO()
    RC->>Factory: ResolveFileIO(ctx)
    Factory->>FIO: Provider.ResolveFileIO(ctx)
    FIO-->>RC: &LocalFileIO{}
    RC-->>CLI: FileIO

    CLI->>RC: ValidatePath(outputPath)
    RC->>FIO: Stat(outputPath) [safeInputPath]
    FIO-->>RC: (info, err)
    RC-->>CLI: nil or error

    CLI->>RC: FileIO().Save(path, opts, body)
    RC->>FIO: Save(path, opts, body)
    FIO->>FIO: SafeOutputPath(path)
    FIO->>FS: MkdirAll + atomicWrite
    FIO-->>RC: SaveResult{size}
    RC-->>CLI: SaveResult

    CLI->>RC: ResolveSavePath(path)
    RC->>FIO: ResolvePath(path)
    FIO->>FIO: SafeOutputPath(path)
    FIO-->>CLI: absolute path
Loading

Reviews (3): Last reviewed commit: "fix: gofmt runner_jq_test.go" | Re-trigger Greptile

Comment on lines +61 to +72
shortcuts-no-vfs:
files:
- "**/shortcuts/**"
deny:
- pkg: "github.com/larksuite/cli/internal/vfs"
desc: >-
shortcuts must not import internal/vfs directly.
Use runtime.FileIO() for file operations or runtime.ValidatePath() for path validation.
- pkg: "github.com/larksuite/cli/internal/vfs/localfileio"
desc: >-
shortcuts must not import internal/vfs/localfileio directly.
Use runtime.FileIO() for file operations or runtime.ValidatePath() for path validation.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 New depguard rule breaks existing shortcuts files

The rule denies internal/vfs imports from **/shortcuts/**, but two non-test production files that are not in this PR still import it:

  • shortcuts/event/pipeline.govfs.MkdirAll (line 20)
  • shortcuts/mail/mail_watch.govfs.UserHomeDir and vfs.MkdirAll (line 28)

The current FileIO interface only exposes Open, Stat, and Save, so these callers cannot be migrated without first extending the interface. Running golangci-lint on the full codebase will produce new depguard errors on these files.

@tuxedomm tuxedomm reopened this Apr 7, 2026
@tuxedomm tuxedomm force-pushed the feat/fileio-extension-refactor-plugin branch from ee0bc07 to a4e0fcf Compare April 7, 2026 13:15
@tuxedomm tuxedomm changed the title Feat/fileio extension refactor plugin refactor: introduce FileIO extension to abstract file transfer operations Apr 7, 2026
Change-Id: I9a7c4dd296d9c742144ec9e8e11465b5fbe6debf
Change-Id: Ic04827e1f1be20c036c5b731825f894ce57052d0
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: 11

Caution

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

⚠️ Outside diff range comments (2)
shortcuts/sheets/sheet_export.go (1)

114-141: ⚠️ Potential issue | 🟠 Major

Return after the no-download branch.

--output-path is optional, but this still falls through into Save(outputPath, ...) after emitting {file_token, ticket}. With an empty path that turns into a bogus save attempt, and the command can write a success JSON to stdout before returning an error.

🐛 Proposed fix
 		if outputPath == "" {
 			runtime.Out(map[string]interface{}{
 				"file_token": fileToken,
 				"ticket":     ticket,
 			}, nil)
+			return nil
 		}
 
 		// Download
 		resp, err := runtime.DoAPIStream(ctx, &larkcore.ApiReq{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/sheets/sheet_export.go` around lines 114 - 141, When outputPath is
empty the code writes the file_token/ticket via runtime.Out but then continues
and attempts to call runtime.FileIO().Save with an empty path; change the branch
handling outputPath == "" (the block that calls runtime.Out with file_token and
ticket) to return immediately after emitting the JSON so execution does not fall
through to the download and Save logic—ensure you return the appropriate
nil/error value from the surrounding function so no further download or success
JSON (saved_path/size_bytes) is produced.
internal/vfs/localfileio/path_test.go (1)

223-235: ⚠️ Potential issue | 🟡 Minor

Don’t codify --file as the shared input-path label.

safeInputPath now sits underneath generic FileIO reads, and this expectation hard-codes --file into any caller that forwards the raw error. The newly migrated --patch-file and draft patch path flows do exactly that, so users will get guidance for the wrong flag/op. Make the validator return a neutral path error or accept a caller-supplied label instead.

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

In `@internal/vfs/localfileio/path_test.go` around lines 223 - 235, The test fails
because safeInputPath currently returns an error that hard-codes the flag name
"--file", which is incorrect for callers like --patch-file or other flows;
change safeInputPath (and any helper it uses) to return a neutral path
validation error message (no specific flag name) or refactor safeInputPath to
accept a caller-provided label argument (e.g., safeInputPath(path string, label
string)) and use that label in the error; update
Test_safeInputPath_ErrorMessageContainsCorrectFlagName to assert for the
neutral/error type or the provided label behavior instead of checking for
"--file".
🧹 Nitpick comments (9)
.golangci.yml (1)

92-116: Duplicate forbidigo patterns for os.Stdin/Stdout/Stderr and os.Exit.

The patterns at lines 92-98 (os\.Std(in|out|err) and os\.Exit) duplicate the individual patterns at lines 106-116. This redundancy may cause duplicate lint warnings or confusion.

Consider removing the duplicate individual patterns (lines 106-116) since the grouped regex patterns already cover them.

♻️ Remove duplicate patterns
         - pattern: os\.Exit\b
           msg: >-
             Do not use os.Exit in shortcuts/. Return an error instead and let
             the caller (cmd layer) decide how to terminate.
         # ── filepath: functions that access the filesystem ──
         - pattern: filepath\.(EvalSymlinks|Walk|WalkDir|Glob|Abs)\b
           msg: >-
             These filepath functions access the filesystem directly.
             internal/: use vfs helpers or localfileio path validation.
             shortcuts/: use runtime.ValidatePath() or runtime.FileIO().
-        # ── IO streams: use IOStreams from cmdutil instead ──
-        - pattern: os\.Stdin\b
-          msg: "use IOStreams.In instead of os.Stdin"
-        - pattern: os\.Stdout\b
-          msg: "use IOStreams.Out instead of os.Stdout"
-        - pattern: os\.Stderr\b
-          msg: "use IOStreams.ErrOut instead of os.Stderr"
-        # ── Process-level rules ──
-        - pattern: os\.Exit\b
-          msg: >-
-            Do not use os.Exit in shortcuts/. Return an error instead and let
-            the caller (cmd layer) decide how to terminate.
       analyze-types: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.golangci.yml around lines 92 - 116, Remove the duplicated forbidigo rules:
keep the grouped regex entries (pattern: os\.Std(in|out|err)\b and pattern:
os\.Exit\b) and delete the redundant individual rules (pattern: os\.Stdin\b,
pattern: os\.Stdout\b, pattern: os\.Stderr\b and the second pattern: os\.Exit\b)
so each prohibition appears only once while preserving the original msg text for
the retained patterns.
shortcuts/drive/drive_upload.go (1)

177-193: Resource leak: file handle not closed on upload error before partFile.Close().

If runtime.DoAPI at line 188 returns an error, partFile.Close() at line 193 is still called. However, if larkcore.NewFormdata() or fd.AddFile fails internally before DoAPI, or if there's a panic, the file might not be closed properly.

Consider using defer partFile.Close() immediately after opening, or restructuring to ensure cleanup.

♻️ Suggested restructure using defer
 		partFile, err := runtime.FileIO().Open(filePath)
 		if err != nil {
 			return "", output.ErrValidation("cannot open file: %v", err)
 		}
+		defer partFile.Close()

 		fd := larkcore.NewFormdata()
 		fd.AddField("upload_id", uploadID)
 		fd.AddField("seq", fmt.Sprintf("%d", seq))
 		fd.AddField("size", fmt.Sprintf("%d", partSize))
 		fd.AddFile("file", io.NewSectionReader(partFile, offset, partSize))

 		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
 			HttpMethod: http.MethodPost,
 			ApiPath:    "/open-apis/drive/v1/files/upload_part",
 			Body:       fd,
 		}, larkcore.WithFileUpload())
-		partFile.Close()
 		if err != nil {

Note: Using defer inside a loop creates a closure per iteration. If the loop has many iterations and files are large, this could delay cleanup until function return. The current explicit Close() is acceptable if you prefer immediate cleanup, but ensure all error paths are covered.

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

In `@shortcuts/drive/drive_upload.go` around lines 177 - 193, The opened file
handle partFile (from runtime.FileIO().Open) can leak if NewFormdata,
fd.AddFile, runtime.DoAPI or other intermediate operations return an error or
panic before the explicit partFile.Close() call; ensure partFile is always
closed by calling defer partFile.Close() immediately after a successful Open (or
restructure to close on every error path) so that fd.AddFile,
larkcore.NewFormdata and runtime.DoAPI error paths cannot leak the handle.
shortcuts/drive/drive_import_test.go (1)

15-16: Prefer the standard test bootstrap over a blank import here.

This package-level side effect wires localfileio differently from the real CLI bootstrap, so these tests can still pass even if provider registration drifts elsewhere. Using the normal test factory plus an isolated config dir would keep the FileIO path closer to production.

As per coding guidelines, "Use cmdutil.TestFactory(t, config) for creating test factories in Go tests" and "Isolate config state in Go tests by using t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())".

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

In `@shortcuts/drive/drive_import_test.go` around lines 15 - 16, Remove the
package-level blank import of
"github.com/larksuite/cli/internal/vfs/localfileio" and replace the implicit
provider wiring by creating an explicit test factory using
cmdutil.TestFactory(t, config) in the test setup; also isolate config state by
calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) before initializing
the factory so FileIO registration follows the normal CLI bootstrap path instead
of package side-effects (refer to symbols localfileio, cmdutil.TestFactory, and
the env var LARKSUITE_CLI_CONFIG_DIR).
shortcuts/common/runner_jq_test.go (1)

155-167: Prefer cmdutil.TestFactory(t, config) for test factories.

newTestFactory still builds cmdutil.Factory manually; using the standard helper will keep test setup consistent with repo conventions.

As per coding guidelines: **/*_test.go: Use cmdutil.TestFactory(t, config) for creating test factories in Go tests.

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

In `@shortcuts/common/runner_jq_test.go` around lines 155 - 167, The test helper
newTestFactory manually constructs a cmdutil.Factory; replace it with the
repo-standard cmdutil.TestFactory(t, config) call: remove newTestFactory and in
tests call cmdutil.TestFactory(t, func() *cmdutil.Factory { return
&cmdutil.Factory{ Config: func() (*core.CliConfig, error){ return
&core.CliConfig{AppID:"test",AppSecret:"test",Brand:core.BrandFeishu}, nil },
LarkClient: func() (*lark.Client, error){ return lark.NewClient("test","test"),
nil }, IOStreams: &cmdutil.IOStreams{Out: &bytes.Buffer{}, ErrOut:
&bytes.Buffer{}}, FileIOProvider: fileio.GetProvider(), } }) so the test uses
TestFactory(t, ...) instead of the manual newTestFactory construction and
follows repository test conventions.
extension/fileio/registry.go (1)

22-27: Consider using sync.RWMutex for better read concurrency.

GetProvider() is likely called frequently at runtime, while Register() is typically called once during init. Using RWMutex with RLock() for reads would allow concurrent GetProvider() calls without blocking each other.

♻️ Optional: Use RWMutex for better read performance
 var (
-	mu       sync.Mutex
+	mu       sync.RWMutex
 	provider Provider
 )

 func Register(p Provider) {
 	mu.Lock()
 	defer mu.Unlock()
 	provider = p
 }

 func GetProvider() Provider {
-	mu.Lock()
-	defer mu.Unlock()
+	mu.RLock()
+	defer mu.RUnlock()
 	return provider
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extension/fileio/registry.go` around lines 22 - 27, GetProvider currently
uses mu.Lock() which serializes reads; change the mutex type from sync.Mutex to
sync.RWMutex (symbol mu) and update GetProvider to use mu.RLock() / mu.RUnlock()
so concurrent reads don't block each other; also update Register (the provider
registration function) to use mu.Lock() / mu.Unlock() for writes and ensure the
import of "sync" is preserved/updated accordingly while keeping the provider
variable usage the same.
shortcuts/base/base_shortcuts_test.go (2)

21-48: Use the standard test factory in this helper.

This helper now wires Factory manually, which bypasses the repo’s test-factory defaults and still leaves no place to isolate config state. Please thread t *testing.T into newBaseTestRuntime and build the runtime with cmdutil.TestFactory(t, config) plus a temp LARKSUITE_CLI_CONFIG_DIR.

As per coding guidelines, **/*_test.go: Use cmdutil.TestFactory(t, config) for creating test factories in Go tests, and isolate config state via t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()).

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

In `@shortcuts/base/base_shortcuts_test.go` around lines 21 - 48, Update
newBaseTestRuntime to accept t *testing.T, set isolated config dir with
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()), and build the runtime using
cmdutil.TestFactory(t, config) instead of manually wiring Factory; ensure the
returned RuntimeContext uses Factory from cmdutil.TestFactory and keep other
flag setup the same so tests use the standard test factory and isolated config
state.

77-96: Add a file-backed case for the new FileIO branch.

These updates only adapt the signature by passing nil, so this test still never exercises the new @path loading path that motivated the extra parameter. A regression in loadJSONInput/parseObjectList would slip through unnoticed.

shortcuts/im/validate_media_test.go (1)

14-19: Use cmdutil.TestChdir for consistency with other tests.

Other tests in this PR (e.g., helpers_network_test.go) use cmdutil.TestChdir(t, t.TempDir()) instead of manual os.Getwd/os.Chdir/defer bookkeeping. This helper handles cleanup more robustly.

Proposed fix
 func TestValidateMediaFlagPath(t *testing.T) {
-	dir := t.TempDir()
-	orig, _ := os.Getwd()
-	defer os.Chdir(orig)
-	os.Chdir(dir)
-	os.WriteFile(filepath.Join(dir, "photo.jpg"), []byte("img"), 0644)
+	cmdutil.TestChdir(t, t.TempDir())
+	os.WriteFile("photo.jpg", []byte("img"), 0644)

This requires adding the import:

import (
	"os"
	"testing"

	"github.com/larksuite/cli/internal/cmdutil"
	"github.com/larksuite/cli/internal/vfs/localfileio"
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/validate_media_test.go` around lines 14 - 19, Replace the manual
working-directory bookkeeping in TestValidateMediaFlagPath with the test helper
cmdutil.TestChdir: call cmdutil.TestChdir(t, t.TempDir()) at the start of
TestValidateMediaFlagPath and remove the orig := os.Getwd(), defer
os.Chdir(orig) and os.Chdir(dir) lines; also add the import for the cmdutil
package (e.g., import "github.com/larksuite/cli/internal/cmdutil") so the test
compiles.
internal/cmdutil/factory_default_test.go (1)

211-230: Missing config directory isolation.

Other tests in this file use t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) to isolate config state. This test should follow the same pattern to prevent interference with machine-local configuration.

Proposed fix
 func TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
+
 	prev := fileio.GetProvider()
 	provider := &countingFileIOProvider{}
 	fileio.Register(provider)

As per coding guidelines: "Isolate config state in Go tests by using t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())"

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

In `@internal/cmdutil/factory_default_test.go` around lines 211 - 230,
TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization fails to isolate
config state; update the test to set the config dir to a temporary directory by
calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start of the
test (before calling NewDefault/InvocationContext) so machine-local config
cannot affect NewDefault/ResolveFileIO behavior; ensure this change is applied
inside the TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization
function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/client/response.go`:
- Around line 82-83: When handling errors returned from FileIO.Save (fio.Save)
where currently code does return output.Errorf(output.ExitInternal,
"file_error", "%s", err) and also uses fmt.Errorf("cannot write file: %s", err),
change the logic to detect typed validation errors from fio.Save (use errors.Is
/ errors.As against the validation error type returned by the FileIO layer) and,
when matched, return output.Errorf(output.ErrValidation, "file_error", "%v",
err) (preserving the original error rather than wrapping with fmt.Errorf); for
non-validation failures keep the ExitInternal mapping. Apply the same change to
the other similar return sites that currently remap all failures to ExitInternal
(the other occurrences in this file).
- Line 30: The code unconditionally calls fio.Save(...) which will panic when
ResponseOptions.FileIO is nil; update SaveResponse and the other save paths
(where fio.Save is used) to guard against nil by resolving a fallback OS-backed
FileIO (e.g., create or use a fileio.NewOSFileIO()/fileio.Default
implementation) before calling Save, or return a clear error if no FileIO is
available; locate the ResponseOptions struct and the SaveResponse (and related
save) functions and insert a small nil-check: if ro.FileIO == nil { ro.FileIO =
fileio.NewOSFileIO() } (or assign a local fio := ro.FileIO; if nil { fio =
fileio.NewOSFileIO() }) then call fio.Save(...) to avoid the panic.

In `@internal/cmdutil/testing.go`:
- Line 10: In TestChdir (and the related helper code around lines 96-104)
replace direct os package calls with the repository VFS wrappers: use the VFS
method to get the current working dir instead of os.Getwd and use the VFS
chdir-equivalent to change directories instead of os.Chdir; update any error
handling to reflect the VFS method signatures and ensure the function restores
the original VFS working directory on return (same semantics as before) so all
filesystem access goes through the vfs layer.

In `@internal/validate/path.go`:
- Around line 6-11: Update the stale docs/comments that still instruct callers
to use validate.SafeInputPath: change AGENTS.md (around the previous line 72)
and the comment in shortcuts/mail/draft/limits.go (previously referencing
"Callers must validate the file path via validate.SafeInputPath") to instead
state that path validation is performed by FileIO operations and callers should
use FileIO methods such as fio.Stat(), fio.Open(), fio.Read(), etc.; reference
the existing example in shortcuts/drive/drive_import.go (around the usage at
line 144) to show the new pattern and remove any mention of SafeInputPath or
validate.SafeInputPath.

In `@internal/vfs/localfileio/localfileio.go`:
- Around line 34-49: The Open and Stat methods in LocalFileIO (and the other
method around MkdirAll at lines ~59-67) call os.Open, os.Stat, and os.MkdirAll
directly which bypasses the repo VFS wrapper; change these to call the vfs
package equivalents (e.g., vfs.Open, vfs.Stat, vfs.MkdirAll or the project’s
vfs.FS interface methods used elsewhere) after validating the path with
safeInputPath so all filesystem access consistently goes through internal/vfs;
update imports to include the vfs package and ensure error propagation stays the
same.

In `@internal/vfs/localfileio/path.go`:
- Around line 21-23: The SafeOutputPath implementation only validates the path
string but does not prevent TOCTOU races; update the code so that subsequent I/O
operations (LocalFileIO.Open, LocalFileIO.Stat, LocalFileIO.Save and any code
paths referenced in the same file regions) are anchored to trusted directory
file descriptors using openat/mkdirat and O_NOFOLLOW semantics rather than
relying on a vetted path string. Concretely: change callers to walk and open
each path component relative to a secured dirfd (using openat with
O_DIRECTORY|O_NOFOLLOW or mkdirat when creating tails), avoid following symlinks
on the final component, and propagate the dirfd or opened file descriptor
instead of re-resolving the string path; ensure all usages referenced
(SafeOutputPath and the LocalFileIO Open/Stat/Save code paths) adopt this
dirfd-based approach so the TOCTOU window is closed.

In `@shortcuts/common/runner.go`:
- Around line 299-316: ValidatePath and resolveInputFlags currently call
ctx.FileIO() and dereference it without checking for nil; add defensive nil
checks where FileIO() is used (in the ValidatePath and resolveInputFlags
functions) and return a clear error (or fallback behavior) if ctx.FileIO() ==
nil. Specifically, in ValidatePath and in resolveInputFlags locate calls like
ctx.FileIO().Stat/Read/Open and guard them with fio := ctx.FileIO(); if fio ==
nil { return fmt.Errorf("file IO provider not available") } (or appropriate
error handling), ensuring RuntimeContext.FileIO() is safely used even if
Factory/FileIOProvider or global provider are absent; alternatively, if you
prefer contract documentation, add a comment on RuntimeContext.FileIO/Execute
guaranteeing non-nil in production and add a runtime check in Execute to panic
or return an error if Factory/FileIOProvider is unset.

In `@shortcuts/doc/doc_media_insert.go`:
- Around line 115-119: The current branch that handles errors from
runtime.FileIO().Stat(filePath) hides the original error by always returning
output.ErrValidation("file not found: %s", filePath); change it to preserve and
return the underlying error (or wrap it) so path/permission errors from
runtime.FileIO().Stat are conveyed. Locate the Stat call in doc_media_insert.go
(runtime.FileIO().Stat(filePath)), and replace the unconditional "file not
found" return with a return that includes err (e.g., wrap err into
output.ErrValidation or return err directly) while keeping the filePath context.

In `@shortcuts/drive/drive_download.go`:
- Line 10: The code currently treats any fileio.FileIO.Stat error as an
os.IsNotExist miss and labels it an "unsafe output path"; instead, validate the
path up-front and treat Stat errors generically: call validate.SafeInputPath (or
runtime.ValidatePath where used in the codebase) on the path before invoking
fileio.FileIO.Stat, remove any os.IsNotExist-based branching, and on Stat errors
return or propagate the original error (or a clear not-found error) rather than
assuming os.IsNotExist; update the logic around the fileio.FileIO.Stat call in
drive_download.go to use validate.SafeInputPath/runtime.ValidatePath and handle
Stat errors without os.IsNotExist checks.

In `@shortcuts/im/im_messages_send.go`:
- Around line 214-221: validateMediaFlagPath currently ignores missing local
files because it only returns an error when Stat fails with something other than
os.IsNotExist; update validateMediaFlagPath to reject non-empty, non-URL,
non-media-key paths that do not exist by checking fio.Stat(value) and returning
output.ErrValidation("%s: %v", flagName, err) whenever err != nil (including
os.IsNotExist), while preserving the early return for URLs/media keys/empty
values — change the conditional around fio.Stat in validateMediaFlagPath to
return a validation error on any Stat error instead of skipping when
os.IsNotExist(err).

In `@shortcuts/mail/emlbuilder/builder.go`:
- Around line 62-70: The readFile method can panic if Builder.fio is nil (when
WithFileIO was not called); update Builder.readFile to check if b.fio == nil and
return a clear error (e.g., "file IO not initialized: call WithFileIO") instead
of calling b.fio.Open, and reference the Builder.readFile and WithFileIO symbols
so the check is added near the start of that method; ensure the error message is
wrapped/returned consistently with existing fmt.Errorf usage.

---

Outside diff comments:
In `@internal/vfs/localfileio/path_test.go`:
- Around line 223-235: The test fails because safeInputPath currently returns an
error that hard-codes the flag name "--file", which is incorrect for callers
like --patch-file or other flows; change safeInputPath (and any helper it uses)
to return a neutral path validation error message (no specific flag name) or
refactor safeInputPath to accept a caller-provided label argument (e.g.,
safeInputPath(path string, label string)) and use that label in the error;
update Test_safeInputPath_ErrorMessageContainsCorrectFlagName to assert for the
neutral/error type or the provided label behavior instead of checking for
"--file".

In `@shortcuts/sheets/sheet_export.go`:
- Around line 114-141: When outputPath is empty the code writes the
file_token/ticket via runtime.Out but then continues and attempts to call
runtime.FileIO().Save with an empty path; change the branch handling outputPath
== "" (the block that calls runtime.Out with file_token and ticket) to return
immediately after emitting the JSON so execution does not fall through to the
download and Save logic—ensure you return the appropriate nil/error value from
the surrounding function so no further download or success JSON
(saved_path/size_bytes) is produced.

---

Nitpick comments:
In @.golangci.yml:
- Around line 92-116: Remove the duplicated forbidigo rules: keep the grouped
regex entries (pattern: os\.Std(in|out|err)\b and pattern: os\.Exit\b) and
delete the redundant individual rules (pattern: os\.Stdin\b, pattern:
os\.Stdout\b, pattern: os\.Stderr\b and the second pattern: os\.Exit\b) so each
prohibition appears only once while preserving the original msg text for the
retained patterns.

In `@extension/fileio/registry.go`:
- Around line 22-27: GetProvider currently uses mu.Lock() which serializes
reads; change the mutex type from sync.Mutex to sync.RWMutex (symbol mu) and
update GetProvider to use mu.RLock() / mu.RUnlock() so concurrent reads don't
block each other; also update Register (the provider registration function) to
use mu.Lock() / mu.Unlock() for writes and ensure the import of "sync" is
preserved/updated accordingly while keeping the provider variable usage the
same.

In `@internal/cmdutil/factory_default_test.go`:
- Around line 211-230:
TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization fails to isolate
config state; update the test to set the config dir to a temporary directory by
calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start of the
test (before calling NewDefault/InvocationContext) so machine-local config
cannot affect NewDefault/ResolveFileIO behavior; ensure this change is applied
inside the TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization
function.

In `@shortcuts/base/base_shortcuts_test.go`:
- Around line 21-48: Update newBaseTestRuntime to accept t *testing.T, set
isolated config dir with t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()), and
build the runtime using cmdutil.TestFactory(t, config) instead of manually
wiring Factory; ensure the returned RuntimeContext uses Factory from
cmdutil.TestFactory and keep other flag setup the same so tests use the standard
test factory and isolated config state.

In `@shortcuts/common/runner_jq_test.go`:
- Around line 155-167: The test helper newTestFactory manually constructs a
cmdutil.Factory; replace it with the repo-standard cmdutil.TestFactory(t,
config) call: remove newTestFactory and in tests call cmdutil.TestFactory(t,
func() *cmdutil.Factory { return &cmdutil.Factory{ Config: func()
(*core.CliConfig, error){ return
&core.CliConfig{AppID:"test",AppSecret:"test",Brand:core.BrandFeishu}, nil },
LarkClient: func() (*lark.Client, error){ return lark.NewClient("test","test"),
nil }, IOStreams: &cmdutil.IOStreams{Out: &bytes.Buffer{}, ErrOut:
&bytes.Buffer{}}, FileIOProvider: fileio.GetProvider(), } }) so the test uses
TestFactory(t, ...) instead of the manual newTestFactory construction and
follows repository test conventions.

In `@shortcuts/drive/drive_import_test.go`:
- Around line 15-16: Remove the package-level blank import of
"github.com/larksuite/cli/internal/vfs/localfileio" and replace the implicit
provider wiring by creating an explicit test factory using
cmdutil.TestFactory(t, config) in the test setup; also isolate config state by
calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) before initializing
the factory so FileIO registration follows the normal CLI bootstrap path instead
of package side-effects (refer to symbols localfileio, cmdutil.TestFactory, and
the env var LARKSUITE_CLI_CONFIG_DIR).

In `@shortcuts/drive/drive_upload.go`:
- Around line 177-193: The opened file handle partFile (from
runtime.FileIO().Open) can leak if NewFormdata, fd.AddFile, runtime.DoAPI or
other intermediate operations return an error or panic before the explicit
partFile.Close() call; ensure partFile is always closed by calling defer
partFile.Close() immediately after a successful Open (or restructure to close on
every error path) so that fd.AddFile, larkcore.NewFormdata and runtime.DoAPI
error paths cannot leak the handle.

In `@shortcuts/im/validate_media_test.go`:
- Around line 14-19: Replace the manual working-directory bookkeeping in
TestValidateMediaFlagPath with the test helper cmdutil.TestChdir: call
cmdutil.TestChdir(t, t.TempDir()) at the start of TestValidateMediaFlagPath and
remove the orig := os.Getwd(), defer os.Chdir(orig) and os.Chdir(dir) lines;
also add the import for the cmdutil package (e.g., import
"github.com/larksuite/cli/internal/cmdutil") so the test compiles.
🪄 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: a1810c48-aa8f-4f85-96be-94ceb14109c1

📥 Commits

Reviewing files that changed from the base of the PR and between 1980b99 and a4e0fcf.

📒 Files selected for processing (81)
  • .golangci.yml
  • cmd/api/api.go
  • cmd/service/service.go
  • extension/fileio/registry.go
  • extension/fileio/types.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/factory.go
  • internal/cmdutil/factory_default.go
  • internal/cmdutil/factory_default_test.go
  • internal/cmdutil/testing.go
  • internal/validate/atomicwrite.go
  • internal/validate/path.go
  • internal/vfs/localfileio/atomicwrite.go
  • internal/vfs/localfileio/atomicwrite_test.go
  • internal/vfs/localfileio/input.go
  • internal/vfs/localfileio/localfileio.go
  • internal/vfs/localfileio/path.go
  • internal/vfs/localfileio/path_test.go
  • shortcuts/base/base_shortcut_helpers.go
  • shortcuts/base/base_shortcuts_test.go
  • shortcuts/base/dashboard_block_create.go
  • shortcuts/base/dashboard_block_update.go
  • shortcuts/base/dashboard_ops.go
  • shortcuts/base/field_ops.go
  • shortcuts/base/helpers.go
  • shortcuts/base/helpers_test.go
  • shortcuts/base/record_ops.go
  • shortcuts/base/record_upload_attachment.go
  • shortcuts/base/table_ops.go
  • shortcuts/base/view_ops.go
  • shortcuts/base/workflow_create.go
  • shortcuts/base/workflow_update.go
  • shortcuts/common/common_test.go
  • shortcuts/common/helpers.go
  • shortcuts/common/runner.go
  • shortcuts/common/runner_input_test.go
  • shortcuts/common/runner_jq_test.go
  • shortcuts/common/validate.go
  • shortcuts/common/validate_test.go
  • shortcuts/doc/doc_media_download.go
  • shortcuts/doc/doc_media_insert.go
  • shortcuts/doc/doc_media_upload.go
  • shortcuts/drive/drive_download.go
  • shortcuts/drive/drive_export.go
  • shortcuts/drive/drive_export_common.go
  • shortcuts/drive/drive_export_test.go
  • shortcuts/drive/drive_import.go
  • shortcuts/drive/drive_import_common.go
  • shortcuts/drive/drive_import_test.go
  • shortcuts/drive/drive_upload.go
  • shortcuts/im/coverage_additional_test.go
  • shortcuts/im/helpers.go
  • shortcuts/im/helpers_network_test.go
  • shortcuts/im/helpers_test.go
  • shortcuts/im/im_messages_reply.go
  • shortcuts/im/im_messages_resources_download.go
  • shortcuts/im/im_messages_send.go
  • shortcuts/im/validate_media_test.go
  • shortcuts/mail/draft/acceptance_test.go
  • shortcuts/mail/draft/patch.go
  • shortcuts/mail/draft/patch_attachment_test.go
  • shortcuts/mail/draft/patch_body_test.go
  • shortcuts/mail/draft/patch_header_test.go
  • shortcuts/mail/draft/patch_recipient_test.go
  • shortcuts/mail/draft/patch_test.go
  • shortcuts/mail/draft/serialize_golden_test.go
  • shortcuts/mail/draft/serialize_test.go
  • shortcuts/mail/emlbuilder/builder.go
  • shortcuts/mail/emlbuilder/builder_test.go
  • shortcuts/mail/helpers.go
  • shortcuts/mail/helpers_test.go
  • shortcuts/mail/mail_draft_create.go
  • shortcuts/mail/mail_draft_edit.go
  • shortcuts/mail/mail_forward.go
  • shortcuts/mail/mail_reply.go
  • shortcuts/mail/mail_reply_all.go
  • shortcuts/mail/mail_send.go
  • shortcuts/minutes/minutes_download.go
  • shortcuts/sheets/sheet_export.go
  • shortcuts/vc/vc_notes.go
💤 Files with no reviewable changes (4)
  • shortcuts/common/validate_test.go
  • shortcuts/common/common_test.go
  • shortcuts/common/helpers.go
  • shortcuts/common/validate.go

JqExpr string // if set, apply jq filter instead of Format
Out io.Writer // stdout
ErrOut io.Writer // stderr
FileIO fileio.FileIO // file transfer abstraction; nil falls back to direct os calls
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 | 🔴 Critical

Avoid a nil FileIO panic on save paths.

ResponseOptions still says nil falls back, but SaveResponse now unconditionally calls fio.Save(...). Any caller using the zero value will panic on --output or on the auto-save binary path instead of getting a normal error.

🩹 Minimal guard
 func SaveResponse(fio fileio.FileIO, resp *larkcore.ApiResp, outputPath string) (map[string]interface{}, error) {
+	if fio == nil {
+		return nil, fmt.Errorf("file I/O not configured")
+	}
 	result, err := fio.Save(outputPath, fileio.SaveOptions{
 		ContentType:   resp.Header.Get("Content-Type"),
 		ContentLength: int64(len(resp.RawBody)),
 	}, bytes.NewReader(resp.RawBody))

Also applies to: 63-63, 77-81, 122-126

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

In `@internal/client/response.go` at line 30, The code unconditionally calls
fio.Save(...) which will panic when ResponseOptions.FileIO is nil; update
SaveResponse and the other save paths (where fio.Save is used) to guard against
nil by resolving a fallback OS-backed FileIO (e.g., create or use a
fileio.NewOSFileIO()/fileio.Default implementation) before calling Save, or
return a clear error if no FileIO is available; locate the ResponseOptions
struct and the SaveResponse (and related save) functions and insert a small
nil-check: if ro.FileIO == nil { ro.FileIO = fileio.NewOSFileIO() } (or assign a
local fio := ro.FileIO; if nil { fio = fileio.NewOSFileIO() }) then call
fio.Save(...) to avoid the panic.

Comment on lines 82 to 83
if err != nil {
return output.Errorf(output.ExitInternal, "file_error", "%s", 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

Preserve validation failures from FileIO.Save.

fio.Save can now return user-input errors such as an invalid --output path, but both save paths remap every failure to file_error/ExitInternal. fmt.Errorf("cannot write file: %s", err) also strips the cause chain, so callers cannot recover the right exit code later. Please keep typed validation errors intact and map them to output.ErrValidation here.

Also applies to: 92-93, 123-129

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

In `@internal/client/response.go` around lines 82 - 83, When handling errors
returned from FileIO.Save (fio.Save) where currently code does return
output.Errorf(output.ExitInternal, "file_error", "%s", err) and also uses
fmt.Errorf("cannot write file: %s", err), change the logic to detect typed
validation errors from fio.Save (use errors.Is / errors.As against the
validation error type returned by the FileIO layer) and, when matched, return
output.Errorf(output.ErrValidation, "file_error", "%v", err) (preserving the
original error rather than wrapping with fmt.Errorf); for non-validation
failures keep the ExitInternal mapping. Apply the same change to the other
similar return sites that currently remap all failures to ExitInternal (the
other occurrences in this file).

"bytes"
"context"
"net/http"
"os"
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

Use vfs.* wrappers instead of direct os.* in TestChdir.

TestChdir currently uses os.Getwd/os.Chdir, which breaks the repo filesystem abstraction rule. Please switch this helper to the repository VFS layer for consistency.

As per coding guidelines: Use vfs.* instead of os.* for all filesystem access.

Also applies to: 96-104

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

In `@internal/cmdutil/testing.go` at line 10, In TestChdir (and the related helper
code around lines 96-104) replace direct os package calls with the repository
VFS wrappers: use the VFS method to get the current working dir instead of
os.Getwd and use the VFS chdir-equivalent to change directories instead of
os.Chdir; update any error handling to reflect the VFS method signatures and
ensure the function restores the original VFS working directory on return (same
semantics as before) so all filesystem access goes through the vfs layer.

Comment on lines +6 to 11
import "github.com/larksuite/cli/internal/vfs/localfileio"

"github.com/larksuite/cli/internal/vfs"
)

// SafeOutputPath validates a download/export target path for --output flags.
// It rejects absolute paths, resolves symlinks to their real location, and
// verifies the canonical result is still under the current working directory.
// This prevents an AI Agent from being tricked into writing files outside the
// working directory (e.g. "../../.ssh/authorized_keys") or following symlinks
// to sensitive locations.
//
// The returned absolute path MUST be used for all subsequent I/O to prevent
// time-of-check-to-time-of-use (TOCTOU) race conditions.
// SafeOutputPath delegates to localfileio.SafeOutputPath.
func SafeOutputPath(path string) (string, error) {
return safePath(path, "--output")
}

// SafeInputPath validates an upload/read source path for --file flags.
// It applies the same rules as SafeOutputPath — rejecting absolute paths,
// resolving symlinks, and enforcing working directory containment — to prevent an AI Agent
// from being tricked into reading sensitive files like /etc/passwd.
func SafeInputPath(path string) (string, error) {
return safePath(path, "--file")
}

// SafeLocalFlagPath validates a flag value as a local file path.
// Empty values and http/https URLs are returned unchanged without validation,
// allowing the caller to handle non-path inputs (e.g. API keys, URLs) upstream.
// For all other values, SafeInputPath rules apply.
// The original relative path is returned unchanged (not resolved to absolute) so
// upload helpers can re-validate at the actual I/O point via SafeUploadPath.
func SafeLocalFlagPath(flagName, value string) (string, error) {
if value == "" || strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") {
return value, nil
}
if _, err := SafeInputPath(value); err != nil {
return "", fmt.Errorf("%s: %v", flagName, err)
}
return value, nil
}

// safePath is the shared implementation for SafeOutputPath and SafeInputPath.
func safePath(raw, flagName string) (string, error) {
if err := RejectControlChars(raw, flagName); err != nil {
return "", err
}

path := filepath.Clean(raw)

if filepath.IsAbs(path) {
return "", fmt.Errorf("%s must be a relative path within the current directory, got %q (hint: cd to the target directory first, or use a relative path like ./filename)", flagName, raw)
}

cwd, err := vfs.Getwd()
if err != nil {
return "", fmt.Errorf("cannot determine working directory: %w", err)
}
resolved := filepath.Join(cwd, path)

// Resolve symlinks: for existing paths, follow to real location;
// for non-existing paths, walk up to the nearest existing ancestor,
// resolve its symlinks, and re-attach the remaining tail segments.
// This prevents TOCTOU attacks where a non-existent intermediate
// directory is replaced with a symlink between check and use.
if _, err := vfs.Lstat(resolved); err == nil {
resolved, err = filepath.EvalSymlinks(resolved)
if err != nil {
return "", fmt.Errorf("cannot resolve symlinks: %w", err)
}
} else {
resolved, err = resolveNearestAncestor(resolved)
if err != nil {
return "", fmt.Errorf("cannot resolve symlinks: %w", err)
}
}

canonicalCwd, _ := filepath.EvalSymlinks(cwd)
if !isUnderDir(resolved, canonicalCwd) {
return "", fmt.Errorf("%s %q resolves outside the current working directory (hint: the path must stay within the working directory after resolving .. and symlinks)", flagName, raw)
}

return resolved, nil
}

// resolveNearestAncestor walks up from path until it finds an existing
// ancestor, resolves that ancestor's symlinks, and re-joins the tail.
// This ensures even deeply nested non-existent paths are anchored to a
// real filesystem location, closing the TOCTOU symlink gap.
func resolveNearestAncestor(path string) (string, error) {
var tail []string
cur := path
for {
if _, err := vfs.Lstat(cur); err == nil {
real, err := filepath.EvalSymlinks(cur)
if err != nil {
return "", err
}
parts := append([]string{real}, tail...)
return filepath.Join(parts...), nil
}
parent := filepath.Dir(cur)
if parent == cur {
// Reached filesystem root without finding an existing ancestor;
// return path as-is and let the containment check reject it.
parts := append([]string{cur}, tail...)
return filepath.Join(parts...), nil
}
tail = append([]string{filepath.Base(cur)}, tail...)
cur = parent
}
}

// isUnderDir checks whether child is under parent directory.
func isUnderDir(child, parent string) bool {
rel, err := filepath.Rel(parent, child)
if err != nil {
return false
}
return !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != ".."
return localfileio.SafeOutputPath(path)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if SafeInputPath is still used anywhere in the codebase
echo "=== Searching for SafeInputPath usage ==="
rg -n 'SafeInputPath' --type=go

echo ""
echo "=== Check if localfileio exports SafeInputPath ==="
rg -n 'func SafeInputPath' internal/vfs/localfileio/

Repository: larksuite/cli

Length of output: 686


🏁 Script executed:

#!/bin/bash
# Check validate package structure
echo "=== List files in internal/validate ==="
ls -la internal/validate/

echo ""
echo "=== Search for SafeInputPath definition in validate package ==="
rg -n 'func SafeInputPath' internal/validate/

echo ""
echo "=== Check internal/validate/path.go for all exported functions ==="
rg -n '^func ' internal/validate/path.go

Repository: larksuite/cli

Length of output: 957


🏁 Script executed:

#!/bin/bash
# Check what's in input.go
echo "=== Content of internal/validate/input.go ==="
cat -n internal/validate/input.go

echo ""
echo "=== Search for SafeInputPath anywhere in codebase (including non-go files) ==="
rg -n 'SafeInputPath' --max-count=20

Repository: larksuite/cli

Length of output: 3679


🏁 Script executed:

#!/bin/bash
# Check AGENTS.md documentation
echo "=== AGENTS.md around line 72 ==="
sed -n '65,80p' AGENTS.md

echo ""
echo "=== drive_import.go around line 144 ==="
sed -n '140,155p' shortcuts/drive/drive_import.go

echo ""
echo "=== limits.go around line 35 ==="
sed -n '30,45p' shortcuts/mail/draft/limits.go

Repository: larksuite/cli

Length of output: 2060


Update documentation and stale comments to reflect FileIO-based path validation.

SafeInputPath has been intentionally removed as part of the refactoring to consolidate validation within FileIO abstractions. This is correct—callers now use fio.Stat(), fio.Open(), and similar methods which validate paths internally.

However, documentation and comments have not been updated to reflect this pattern:

  • AGENTS.md (line 72) still instructs: "Call validate.SafeInputPath before any file I/O"
  • shortcuts/mail/draft/limits.go (line 35) still references the old pattern: "Callers must validate the file path via validate.SafeInputPath"

Update these to document the new pattern: validation is performed automatically by FileIO operations (e.g., as shown in shortcuts/drive/drive_import.go:144).

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

In `@internal/validate/path.go` around lines 6 - 11, Update the stale
docs/comments that still instruct callers to use validate.SafeInputPath: change
AGENTS.md (around the previous line 72) and the comment in
shortcuts/mail/draft/limits.go (previously referencing "Callers must validate
the file path via validate.SafeInputPath") to instead state that path validation
is performed by FileIO operations and callers should use FileIO methods such as
fio.Stat(), fio.Open(), fio.Read(), etc.; reference the existing example in
shortcuts/drive/drive_import.go (around the usage at line 144) to show the new
pattern and remove any mention of SafeInputPath or validate.SafeInputPath.

Comment on lines +34 to +49
func (l *LocalFileIO) Open(name string) (fileio.File, error) {
safePath, err := safeInputPath(name)
if err != nil {
return nil, err
}
return os.Open(safePath)
}

// Stat returns file metadata after validating the path.
func (l *LocalFileIO) Stat(name string) (os.FileInfo, error) {
safePath, err := safeInputPath(name)
if err != nil {
return nil, err
}
return os.Stat(safePath)
}
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

Use internal/vfs for the remaining filesystem calls here.

os.Open, os.Stat, and os.MkdirAll bypass the repo's filesystem wrapper, so this implementation will not respect any vfs indirection consistently. Please route these through vfs.* as well, or add the missing wrappers before merging.

As per coding guidelines, Use vfs.* instead of os.* for all filesystem access.

Also applies to: 59-67

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

In `@internal/vfs/localfileio/localfileio.go` around lines 34 - 49, The Open and
Stat methods in LocalFileIO (and the other method around MkdirAll at lines
~59-67) call os.Open, os.Stat, and os.MkdirAll directly which bypasses the repo
VFS wrapper; change these to call the vfs package equivalents (e.g., vfs.Open,
vfs.Stat, vfs.MkdirAll or the project’s vfs.FS interface methods used elsewhere)
after validating the path with safeInputPath so all filesystem access
consistently goes through internal/vfs; update imports to include the vfs
package and ensure error propagation stays the same.

Comment on lines +299 to +316
// FileIO resolves the FileIO using the current execution context.
// Falls back to the globally registered provider when Factory or its
// FileIOProvider is nil (e.g. in lightweight test helpers).
func (ctx *RuntimeContext) FileIO() fileio.FileIO {
if ctx != nil && ctx.Factory != nil {
if fio := ctx.Factory.ResolveFileIO(ctx.ctx); fio != nil {
return fio
}
}
if p := fileio.GetProvider(); p != nil {
c := context.Background()
if ctx != nil {
c = ctx.ctx
}
return p.ResolveFileIO(c)
}
return 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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if any FileIO() caller handles nil return
echo "=== Checking FileIO() usage patterns ==="
rg -n '\.FileIO\(\)' --type=go -A2 | head -80

Repository: larksuite/cli

Length of output: 5586


🏁 Script executed:

#!/bin/bash
# Check the two specific callers mentioned in the original comment
echo "=== ValidatePath (line 323) ==="
sed -n '318,330p' shortcuts/common/runner.go

echo ""
echo "=== resolveInputFlags (line 608) ==="
sed -n '603,620p' shortcuts/common/runner.go

# Check if Factory/provider is always initialized
echo ""
echo "=== Check RuntimeContext initialization and Factory setup ==="
rg -n 'NewRuntimeContext|\.Factory\s*=' --type=go shortcuts/common/runner.go | head -20

Repository: larksuite/cli

Length of output: 1132


🏁 Script executed:

#!/bin/bash
# Check where RuntimeContext is created and if Factory is always set
echo "=== RuntimeContext initialization ==="
rg -n 'RuntimeContext\{|&RuntimeContext\{' --type=go | head -20

echo ""
echo "=== Check fileio.GetProvider setup ==="
rg -n 'GetProvider|RegisterProvider' --type=go | head -20

# Check imports at top of runner.go to understand dependencies
echo ""
echo "=== Top of runner.go for context ==="
head -30 shortcuts/common/runner.go

Repository: larksuite/cli

Length of output: 3861


🏁 Script executed:

#!/bin/bash
# Check how ValidatePath and resolveInputFlags are used/called
echo "=== ValidatePath usage ==="
rg -n 'ValidatePath' --type=go | head -20

echo ""
echo "=== Check factory_default.go for fileio provider setup ==="
sed -n '45,55p' internal/cmdutil/factory_default.go

echo ""
echo "=== Check if fileio.GetProvider() is initialized in main flow ==="
rg -n 'fileio.*Register|SetProvider' --type=go | head -15

Repository: larksuite/cli

Length of output: 1331


🏁 Script executed:

#!/bin/bash
# Check if ValidatePath callers might be called in tests or production only
echo "=== Check one ValidatePath caller (sheet_export.go:66) in context ==="
sed -n '60,75p' shortcuts/sheets/sheet_export.go

echo ""
echo "=== Check localfileio registration more thoroughly ==="
cat internal/vfs/localfileio/localfileio.go

echo ""
echo "=== Check fileio registry.go to understand GetProvider behavior ==="
cat extension/fileio/registry.go

Repository: larksuite/cli

Length of output: 3213


🏁 Script executed:

#!/bin/bash
# Check if localfileio is imported in main application flow
echo "=== Check main.go or cmd entrypoint ==="
find . -name "main.go" -type f | head -5 | xargs grep -l "localfileio\|fileio" 2>/dev/null

echo ""
echo "=== Check cmd package for imports ==="
fd -e go "cmd.*main" | head -3 | xargs head -30

echo ""
echo "=== Check if localfileio is imported anywhere in cmd/main flow ==="
rg 'import.*localfileio' --type=go

Repository: larksuite/cli

Length of output: 288


🏁 Script executed:

#!/bin/bash
# Find the actual main entry point
echo "=== Find main function ==="
rg -n 'func main\(' --type=go

echo ""
echo "=== Check validate/path.go which imports localfileio ==="
head -40 internal/validate/path.go

echo ""
echo "=== Check if validate.SafeInputPath is used (mentioned in learnings) ==="
rg -n 'validate.SafeInputPath\|SafeInputPath' --type=go | head -20

Repository: larksuite/cli

Length of output: 554


🏁 Script executed:

#!/bin/bash
# Check main.go for imports
echo "=== main.go full content ==="
cat main.go

echo ""
echo "=== Check if there's an init or import that brings in localfileio ==="
grep -r "_ \".*localfileio\"" --include="*.go" | head -10

Repository: larksuite/cli

Length of output: 770


🏁 Script executed:

#!/bin/bash
# Check if factory_default.go is used in cmd/Execute flow
echo "=== Check cmd package ==="
head -50 cmd/root.go 2>/dev/null || head -50 cmd/*.go | head -60

echo ""
echo "=== Look for NewFactory or factory creation in cmd ==="
rg -n 'NewFactory|factory_default\|Factory\(' cmd/ --type=go | head -15

Repository: larksuite/cli

Length of output: 1473


🏁 Script executed:

#!/bin/bash
# Check where RuntimeContext is created with Factory in main flow (line 545 in runner.go)
echo "=== Context around line 545 in runner.go ==="
sed -n '530,560p' shortcuts/common/runner.go

echo ""
echo "=== Look for callers of NewRuntimeContext or similar ==="
rg -n 'func.*NewRuntimeContext|executeShortcut\|RuntimeContext.*Factory' shortcuts/common/ --type=go | head -15

Repository: larksuite/cli

Length of output: 1726


🏁 Script executed:

#!/bin/bash
# Final verification: check if Factory is ALWAYS set in production path
echo "=== Trace Execute path to see if Factory is always provided ==="
rg -n 'func Execute' cmd/ --type=go -A 30 | head -50

echo ""
echo "=== Check if there's any code path that creates RuntimeContext without Factory ==="
rg -n 'newRuntimeContext' shortcuts/common/ --type=go -B 5 | head -30

echo ""
echo "=== Summary of FileIO() method contract ==="
sed -n '299,316p' shortcuts/common/runner.go

Repository: larksuite/cli

Length of output: 3324


Add nil check for FileIO() in ValidatePath and resolveInputFlags, or document the guarantee.

While FileIO() can theoretically return nil when both the Factory provider and global provider are unavailable, the production code path ensures this won't happen—Factory is always initialized in Execute() with a FileIOProvider, and localfileio is registered at init time. However, ValidatePath() (line 323) and resolveInputFlags() (line 612) both dereference the result without nil checks.

For defensive programming, either:

  1. Add nil checks before calling methods on FileIO()
  2. Document clearly that FileIO() is guaranteed non-nil in production contexts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/common/runner.go` around lines 299 - 316, ValidatePath and
resolveInputFlags currently call ctx.FileIO() and dereference it without
checking for nil; add defensive nil checks where FileIO() is used (in the
ValidatePath and resolveInputFlags functions) and return a clear error (or
fallback behavior) if ctx.FileIO() == nil. Specifically, in ValidatePath and in
resolveInputFlags locate calls like ctx.FileIO().Stat/Read/Open and guard them
with fio := ctx.FileIO(); if fio == nil { return fmt.Errorf("file IO provider
not available") } (or appropriate error handling), ensuring
RuntimeContext.FileIO() is safely used even if Factory/FileIOProvider or global
provider are absent; alternatively, if you prefer contract documentation, add a
comment on RuntimeContext.FileIO/Execute guaranteeing non-nil in production and
add a runtime check in Execute to panic or return an error if
Factory/FileIOProvider is unset.

Comment on lines 115 to 119
// Validate file
stat, err := vfs.Stat(filePath)
stat, err := runtime.FileIO().Stat(filePath)
if err != nil {
return output.ErrValidation("file not found: %s", filePath)
}
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

Preserve the original FileIO error instead of rewriting everything to "file not found".

Now that runtime.FileIO().Stat() owns path and permission checks, this branch turns unsafe-path and permission failures into the same missing-file message. That makes the error misleading and regresses the clearer validation feedback from the new FileIO layer.

Suggested fix
 		// Validate file
 		stat, err := runtime.FileIO().Stat(filePath)
 		if err != nil {
-			return output.ErrValidation("file not found: %s", filePath)
+			var exitErr *output.ExitError
+			if errors.As(err, &exitErr) {
+				return err
+			}
+			return output.ErrValidation("cannot read file %q: %v", filePath, err)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/doc_media_insert.go` around lines 115 - 119, The current branch
that handles errors from runtime.FileIO().Stat(filePath) hides the original
error by always returning output.ErrValidation("file not found: %s", filePath);
change it to preserve and return the underlying error (or wrap it) so
path/permission errors from runtime.FileIO().Stat are conveyed. Locate the Stat
call in doc_media_insert.go (runtime.FileIO().Stat(filePath)), and replace the
unconditional "file not found" return with a return that includes err (e.g.,
wrap err into output.ErrValidation or return err directly) while keeping the
filePath context.

"fmt"
"net/http"
"path/filepath"
"os"
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

Don’t derive FileIO semantics from os.IsNotExist.

This gate assumes every fileio.FileIO.Stat miss satisfies os.IsNotExist, which is not part of the FileIO contract. A wrapped or provider-specific miss will be reported here as unsafe output path, and this also bypasses the centralized runtime.ValidatePath(...) flow the rest of the refactor moved to.

Suggested adjustment
-	"os"
@@
-		// Early path validation + overwrite check via FileIO.Stat
-		if _, statErr := runtime.FileIO().Stat(outputPath); statErr != nil && !os.IsNotExist(statErr) {
-			return output.ErrValidation("unsafe output path: %s", statErr)
-		} else if statErr == nil && !overwrite {
-			return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)
-		}
+		if err := runtime.ValidatePath(outputPath); err != nil {
+			return output.ErrValidation("unsafe output path: %s", err)
+		}
+		if !overwrite {
+			if _, statErr := runtime.FileIO().Stat(outputPath); statErr == nil {
+				return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)
+			}
+		}

As per coding guidelines, "Validate paths using validate.SafeInputPath before any file I/O operations".

Also applies to: 55-60

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

In `@shortcuts/drive/drive_download.go` at line 10, The code currently treats any
fileio.FileIO.Stat error as an os.IsNotExist miss and labels it an "unsafe
output path"; instead, validate the path up-front and treat Stat errors
generically: call validate.SafeInputPath (or runtime.ValidatePath where used in
the codebase) on the path before invoking fileio.FileIO.Stat, remove any
os.IsNotExist-based branching, and on Stat errors return or propagate the
original error (or a clear not-found error) rather than assuming os.IsNotExist;
update the logic around the fileio.FileIO.Stat call in drive_download.go to use
validate.SafeInputPath/runtime.ValidatePath and handle Stat errors without
os.IsNotExist checks.

Comment on lines +214 to +221
func validateMediaFlagPath(fio fileio.FileIO, flagName, value string) error {
if value == "" || strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") || isMediaKey(value) {
return nil
}
if _, err := fio.Stat(value); err != nil && !os.IsNotExist(err) {
return output.ErrValidation("%s: %v", flagName, err)
}
return 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

Reject missing local media files here.

fio.Stat failures are ignored when the file does not exist, so values like --image ./missing.png now pass both Validate and the early Execute preflight and only fail later in the upload path. That regresses the old “bad local path” feedback and makes user errors surface much later.

🐛 Proposed fix
-import (
-	"context"
-	"encoding/json"
-	"net/http"
-	"os"
-	"strings"
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"strings"
@@
 func validateMediaFlagPath(fio fileio.FileIO, flagName, value string) error {
 	if value == "" || strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") || isMediaKey(value) {
 		return nil
 	}
-	if _, err := fio.Stat(value); err != nil && !os.IsNotExist(err) {
+	if _, err := fio.Stat(value); err != nil {
 		return output.ErrValidation("%s: %v", flagName, err)
 	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/im_messages_send.go` around lines 214 - 221,
validateMediaFlagPath currently ignores missing local files because it only
returns an error when Stat fails with something other than os.IsNotExist; update
validateMediaFlagPath to reject non-empty, non-URL, non-media-key paths that do
not exist by checking fio.Stat(value) and returning output.ErrValidation("%s:
%v", flagName, err) whenever err != nil (including os.IsNotExist), while
preserving the early return for URLs/media keys/empty values — change the
conditional around fio.Stat in validateMediaFlagPath to return a validation
error on any Stat error instead of skipping when os.IsNotExist(err).

Comment on lines +62 to 70
// readFile reads the named file and returns its contents via the Builder's FileIO.
func (b Builder) readFile(path string) ([]byte, error) {
f, err := b.fio.Open(path)
if err != nil {
return nil, fmt.Errorf("attachment %q: %w", path, err)
}
return vfs.ReadFile(safePath)
defer f.Close()
return io.ReadAll(f)
}
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

Potential nil pointer dereference if WithFileIO is not called.

The readFile method calls b.fio.Open(path) but fio could be nil if WithFileIO was never called on the builder. This would cause a panic.

Consider adding a nil check or documenting that WithFileIO must be called before using file-based methods.

🛡️ Proposed defensive check
 func (b Builder) readFile(path string) ([]byte, error) {
+	if b.fio == nil {
+		return nil, fmt.Errorf("attachment %q: FileIO not configured (call WithFileIO first)", path)
+	}
 	f, err := b.fio.Open(path)
 	if err != nil {
 		return nil, fmt.Errorf("attachment %q: %w", path, err)
 	}
 	defer f.Close()
 	return io.ReadAll(f)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/emlbuilder/builder.go` around lines 62 - 70, The readFile
method can panic if Builder.fio is nil (when WithFileIO was not called); update
Builder.readFile to check if b.fio == nil and return a clear error (e.g., "file
IO not initialized: call WithFileIO") instead of calling b.fio.Open, and
reference the Builder.readFile and WithFileIO symbols so the check is added near
the start of that method; ensure the error message is wrapped/returned
consistently with existing fmt.Errorf usage.

tuxedomm added 2 commits April 7, 2026 21:43
Add ResolvePath to FileIO interface so callers can obtain the validated
absolute path without re-importing internal path helpers. Update all
download shortcuts and SaveResponse to return resolved absolute paths
in saved_path output, fixing a regression where relative paths were
returned instead of absolute ones.

Also add nil guard in RuntimeContext.ValidatePath for defensive safety.

Change-Id: I8bb0e323f7e44149df5e6f558a030e505380a1ce
Change-Id: If8a0f8588dddf4dca8741a2646881c7729fdfa2e
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: 3

Caution

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

⚠️ Outside diff range comments (2)
shortcuts/common/runner_jq_test.go (1)

156-168: ⚠️ Potential issue | 🟠 Major

Use the repo test factory helper and isolate config env in this test helper.

Line 156 still builds cmdutil.Factory manually, and this helper path does not isolate LARKSUITE_CLI_CONFIG_DIR. Please switch to cmdutil.TestFactory(t, config) and set per-test config dir via t.Setenv(...) to keep tests hermetic.

As per coding guidelines, **/*_test.go: Use cmdutil.TestFactory(t, config) for creating test factories in Go tests and isolate config state with t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()).

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

In `@shortcuts/common/runner_jq_test.go` around lines 156 - 168, The test helper
newTestFactory() currently constructs a cmdutil.Factory manually and doesn't
isolate config state; replace its usage with the repository test helper by
switching to cmdutil.TestFactory(t, config) in tests and ensure you call
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) so each test gets a hermetic
per-test config dir; locate references to newTestFactory and update the test to
create a cfg (core.CliConfig with AppID/AppSecret/Brand) passed into
cmdutil.TestFactory(t, cfg) and remove the manual Factory construction and
direct LarkClient/IOStreams setup.
shortcuts/minutes/minutes_download.go (1)

264-287: ⚠️ Potential issue | 🟠 Major

Validate the finalized output path before writing.

outputPath is only known after this block, but the code goes straight to Stat/Save. An unsafe path currently comes back as a late cannot create file after the download request has already succeeded. Validate it once here and return a validation error before touching the body.

Suggested change
 	if outputPath == "" {
 		filename := resolveFilenameFromResponse(resp, minuteToken)
 		// Deduplicate filenames in batch mode: prefix with token on collision.
 		if opts.usedNames != nil {
@@
 		}
 		outputPath = filepath.Join(opts.outputDir, filename)
 	}
+	if _, err := opts.fio.ResolvePath(outputPath); err != nil {
+		return nil, output.ErrValidation("unsafe output path: %s", err)
+	}
 
 	if !opts.overwrite {
 		if _, statErr := opts.fio.Stat(outputPath); statErr == nil {
 			return nil, output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)
As per coding guidelines, `Validate paths using validate.SafeInputPath before any file I/O operations`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/minutes/minutes_download.go` around lines 264 - 287, After
computing outputPath (after the block that sets opts.outputPath / calls
resolveFilenameFromResponse and deduplicates via opts.usedNames), validate the
finalized path using validate.SafeInputPath(outputPath) and return an
output.ErrValidation if validation fails; do this before any file I/O such as
opts.fio.Stat or opts.fio.Save so you fail fast (before consuming resp.Body).
Update the code paths around opts.overwrite/opts.fio.Stat and the subsequent
opts.fio.Save call to assume a pre-validated outputPath, and reference the
existing symbols resolveFilenameFromResponse, minuteToken, opts.usedNames,
opts.outputDir, opts.overwrite, opts.fio.Stat, opts.fio.Save, and
validate.SafeInputPath when making the change.
♻️ Duplicate comments (3)
internal/vfs/localfileio/localfileio.go (1)

34-39: ⚠️ Potential issue | 🟠 Major

Route these filesystem calls through internal/vfs.

Open, Stat, and Save still hit os.Open, os.Stat, and os.MkdirAll directly, so the new FileIO layer bypasses the repo’s VFS abstraction at its core filesystem boundaries.

As per coding guidelines, Use vfs.* instead of os.* for all filesystem access.

Also applies to: 43-48, 64-72

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

In `@internal/vfs/localfileio/localfileio.go` around lines 34 - 39, The
LocalFileIO methods (Open, Stat, Save) call os.Open/os.Stat/os.MkdirAll directly
and must instead route filesystem access through the repo VFS layer; update
LocalFileIO.Open, LocalFileIO.Stat and LocalFileIO.Save (and any use of
safeInputPath) to call the corresponding internal/vfs functions (e.g., vfs.Open,
vfs.Stat, vfs.MkdirAll or their equivalents) and preserve existing error
handling/return values so the FileIO implementation uses the VFS abstraction
rather than the os package.
shortcuts/drive/drive_download.go (1)

55-60: ⚠️ Potential issue | 🟠 Major

Don’t key FileIO miss handling off os.IsNotExist.

fileio.FileIO.Stat does not promise os.IsNotExist semantics. A provider-specific validation/miss error is misclassified here, and the direct runtime.FileIO().Stat(...) call still panics when no provider is registered. Reuse runtime.ValidatePath(outputPath) for the safety check, then only treat statErr == nil as “already exists”. This also lets you drop the os import.

Suggested change
-		// Early path validation + overwrite check via FileIO.Stat
-		if _, statErr := runtime.FileIO().Stat(outputPath); statErr != nil && !os.IsNotExist(statErr) {
-			return output.ErrValidation("unsafe output path: %s", statErr)
-		} else if statErr == nil && !overwrite {
-			return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)
-		}
+		if err := runtime.ValidatePath(outputPath); err != nil {
+			return output.ErrValidation("unsafe output path: %s", err)
+		}
+		if !overwrite {
+			if _, statErr := runtime.FileIO().Stat(outputPath); statErr == nil {
+				return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)
+			}
+		}
As per coding guidelines, `Validate paths using validate.SafeInputPath before any file I/O operations`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/drive/drive_download.go` around lines 55 - 60, Replace the
runtime.FileIO().Stat-based validation with a call to
runtime.ValidatePath(outputPath) (or validate.SafeInputPath per guidelines) to
perform safety checks and avoid panics when no FileIO provider is registered,
then only treat a subsequent runtime.FileIO().Stat(outputPath) result of statErr
== nil as “already exists” (return the overwrite validation error when statErr
== nil and !overwrite); ignore provider-specific/stat errors instead of checking
os.IsNotExist and remove the os import. Ensure you update the code around the
existing runtime.FileIO().Stat usage and the output.ErrValidation checks
(referencing runtime.ValidatePath, runtime.FileIO().Stat, and overwrite).
internal/cmdutil/testing.go (1)

95-104: ⚠️ Potential issue | 🟠 Major

Keep TestChdir on the VFS abstraction.

This helper still uses process-wide os.Chdir directly, and the cleanup path drops any restore failure. Please add/use a VFS Chdir wrapper here so both the switch and restore stay inside the repo abstraction and can fail the test if cleanup breaks.

As per coding guidelines, Use vfs.* instead of os.* for all filesystem access.

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

In `@internal/cmdutil/testing.go` around lines 95 - 104, The TestChdir helper
currently calls os.Chdir and ignores errors on restore; change it to use the VFS
abstraction by calling vfs.Chdir(dir) for the switch and capture any error, and
in the t.Cleanup closure call vfs.Chdir(orig) and fail the test (t.Fatalf) if
that restore returns an error; keep the existing vfs.Getwd usage and preserve
the nolint comments only if still applicable.
🧹 Nitpick comments (1)
shortcuts/base/helpers.go (1)

86-99: Extract the repeated comma-splitting logic into one helper.

Line 90–Line 98 duplicates behavior already present in parseStringListFlexible (Line 74–Line 82), which can drift over time.

♻️ Proposed refactor
 func parseStringListFlexible(fio fileio.FileIO, raw string, flagName string) ([]string, error) {
@@
-	parts := strings.Split(raw, ",")
-	result := make([]string, 0, len(parts))
-	for _, part := range parts {
-		item := strings.TrimSpace(part)
-		if item != "" {
-			result = append(result, item)
-		}
-	}
-	return result, nil
+	return splitCommaList(raw), nil
 }
 
 func parseStringList(raw string) []string {
 	raw = strings.TrimSpace(raw)
 	if raw == "" {
 		return nil
 	}
+	return splitCommaList(raw)
+}
+
+func splitCommaList(raw string) []string {
 	parts := strings.Split(raw, ",")
 	result := make([]string, 0, len(parts))
 	for _, part := range parts {
 		item := strings.TrimSpace(part)
 		if item != "" {
 			result = append(result, item)
 		}
 	}
 	return result
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/base/helpers.go` around lines 86 - 99, Extract the duplicated
comma-splitting/trimming logic into a single helper (e.g.,
splitAndTrimCommaSeparated(raw string) []string) and replace the duplicated
block in the current function (the block shown) and parseStringListFlexible to
call this helper; the helper should TrimSpace(raw), return nil for empty input,
Split by ",", TrimSpace each part, skip empty items, and return the resulting
slice so nil-vs-empty semantics are preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/cmdutil/factory_default_test.go`:
- Around line 211-231: The test
TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization can pick up
machine-local config because NewDefault follows normal initialization; to
isolate state, set the config dir to a temp directory at the start of the test
by calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) before invoking
NewDefault(InvocationContext{}), so the factory initialization (NewDefault) will
use an isolated config directory for the duration of the test.

In `@shortcuts/common/runner_jq_test.go`:
- Around line 109-114: The testResolvedFileIO stub methods (Open, Stat,
ResolvePath, Save) have formatting that causes gofmt drift; run gofmt (or gofmt
-w) on shortcuts/common/runner_jq_test.go to reformat these method declarations
so they follow Go formatting rules (ensuring spacing and line breaks for the
return statements match gofmt output) and commit the resulting changes.

In `@shortcuts/minutes/minutes_download.go`:
- Around line 81-82: The code dereferences runtime.FileIO() (used in the Stat
call and later saved into downloadOpts used by downloadMediaFile) without
checking for nil; add an explicit nil check after calling runtime.FileIO() and
return a CLI error (e.g., via output.ErrValidation or a suitable error helper)
if no provider is registered so the code fails fast instead of panicking. Update
the two occurrences (the Stat check around outputPath and the later assignment
into downloadOpts) to validate fileIO != nil before using or storing it, and
return a clear error message like "no FileIO provider registered" when nil.

---

Outside diff comments:
In `@shortcuts/common/runner_jq_test.go`:
- Around line 156-168: The test helper newTestFactory() currently constructs a
cmdutil.Factory manually and doesn't isolate config state; replace its usage
with the repository test helper by switching to cmdutil.TestFactory(t, config)
in tests and ensure you call t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
so each test gets a hermetic per-test config dir; locate references to
newTestFactory and update the test to create a cfg (core.CliConfig with
AppID/AppSecret/Brand) passed into cmdutil.TestFactory(t, cfg) and remove the
manual Factory construction and direct LarkClient/IOStreams setup.

In `@shortcuts/minutes/minutes_download.go`:
- Around line 264-287: After computing outputPath (after the block that sets
opts.outputPath / calls resolveFilenameFromResponse and deduplicates via
opts.usedNames), validate the finalized path using
validate.SafeInputPath(outputPath) and return an output.ErrValidation if
validation fails; do this before any file I/O such as opts.fio.Stat or
opts.fio.Save so you fail fast (before consuming resp.Body). Update the code
paths around opts.overwrite/opts.fio.Stat and the subsequent opts.fio.Save call
to assume a pre-validated outputPath, and reference the existing symbols
resolveFilenameFromResponse, minuteToken, opts.usedNames, opts.outputDir,
opts.overwrite, opts.fio.Stat, opts.fio.Save, and validate.SafeInputPath when
making the change.

---

Duplicate comments:
In `@internal/cmdutil/testing.go`:
- Around line 95-104: The TestChdir helper currently calls os.Chdir and ignores
errors on restore; change it to use the VFS abstraction by calling
vfs.Chdir(dir) for the switch and capture any error, and in the t.Cleanup
closure call vfs.Chdir(orig) and fail the test (t.Fatalf) if that restore
returns an error; keep the existing vfs.Getwd usage and preserve the nolint
comments only if still applicable.

In `@internal/vfs/localfileio/localfileio.go`:
- Around line 34-39: The LocalFileIO methods (Open, Stat, Save) call
os.Open/os.Stat/os.MkdirAll directly and must instead route filesystem access
through the repo VFS layer; update LocalFileIO.Open, LocalFileIO.Stat and
LocalFileIO.Save (and any use of safeInputPath) to call the corresponding
internal/vfs functions (e.g., vfs.Open, vfs.Stat, vfs.MkdirAll or their
equivalents) and preserve existing error handling/return values so the FileIO
implementation uses the VFS abstraction rather than the os package.

In `@shortcuts/drive/drive_download.go`:
- Around line 55-60: Replace the runtime.FileIO().Stat-based validation with a
call to runtime.ValidatePath(outputPath) (or validate.SafeInputPath per
guidelines) to perform safety checks and avoid panics when no FileIO provider is
registered, then only treat a subsequent runtime.FileIO().Stat(outputPath)
result of statErr == nil as “already exists” (return the overwrite validation
error when statErr == nil and !overwrite); ignore provider-specific/stat errors
instead of checking os.IsNotExist and remove the os import. Ensure you update
the code around the existing runtime.FileIO().Stat usage and the
output.ErrValidation checks (referencing runtime.ValidatePath,
runtime.FileIO().Stat, and overwrite).

---

Nitpick comments:
In `@shortcuts/base/helpers.go`:
- Around line 86-99: Extract the duplicated comma-splitting/trimming logic into
a single helper (e.g., splitAndTrimCommaSeparated(raw string) []string) and
replace the duplicated block in the current function (the block shown) and
parseStringListFlexible to call this helper; the helper should TrimSpace(raw),
return nil for empty input, Split by ",", TrimSpace each part, skip empty items,
and return the resulting slice so nil-vs-empty semantics are preserved.
🪄 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: 5f0caf83-bf43-4d93-86b5-f17aa7b5ed34

📥 Commits

Reviewing files that changed from the base of the PR and between a4e0fcf and 8867dca.

📒 Files selected for processing (18)
  • extension/fileio/types.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/factory_default_test.go
  • internal/cmdutil/testing.go
  • internal/vfs/localfileio/localfileio.go
  • internal/vfs/localfileio/path.go
  • internal/vfs/localfileio/path_test.go
  • shortcuts/base/helpers.go
  • shortcuts/common/runner.go
  • shortcuts/common/runner_jq_test.go
  • shortcuts/doc/doc_media_download.go
  • shortcuts/drive/drive_download.go
  • shortcuts/drive/drive_export_common.go
  • shortcuts/drive/drive_export_test.go
  • shortcuts/im/im_messages_resources_download.go
  • shortcuts/minutes/minutes_download.go
  • shortcuts/sheets/sheet_export.go
✅ Files skipped from review due to trivial changes (2)
  • extension/fileio/types.go
  • shortcuts/common/runner.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • shortcuts/drive/drive_export_test.go
  • internal/client/response_test.go
  • internal/vfs/localfileio/path_test.go
  • internal/client/response.go
  • internal/vfs/localfileio/path.go
  • shortcuts/sheets/sheet_export.go
  • shortcuts/im/im_messages_resources_download.go
  • shortcuts/drive/drive_export_common.go

Comment on lines +211 to 231
func TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) {
prev := fileio.GetProvider()
provider := &countingFileIOProvider{}
fileio.Register(provider)
t.Cleanup(func() { fileio.Register(prev) })

mid := &extensionMiddleware{Base: capturer, Ext: tamperIC}

origCtx := context.WithValue(context.Background(), testKey, "original")
req, _ := http.NewRequestWithContext(origCtx, "GET", srv.URL, nil)
resp, err := mid.RoundTrip(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
resp.Body.Close()

// Built-in chain should see original context, not tampered
if ctxValue != "original" {
t.Fatalf("built-in chain saw context value %q, want %q", ctxValue, "original")
}
}

// interceptorFunc adapts a function to exttransport.Interceptor.
type interceptorFunc func(*http.Request) func(*http.Response, error)

func (f interceptorFunc) PreRoundTrip(req *http.Request) func(*http.Response, error) { return f(req) }

func TestBuildSDKTransport_WithExtension(t *testing.T) {
exttransport.Register(&stubTransportProvider{})
t.Cleanup(func() { exttransport.Register(nil) })

transport := buildSDKTransport()

// Chain: extensionMiddleware → SecurityPolicy → UserAgent → Retry → Base
mid, ok := transport.(*extensionMiddleware)
if !ok {
t.Fatalf("outer transport type = %T, want *extensionMiddleware", transport)
}
sec, ok := mid.Base.(*internalauth.SecurityPolicyTransport)
if !ok {
t.Fatalf("transport type = %T, want *auth.SecurityPolicyTransport", mid.Base)
}
ua, ok := sec.Base.(*UserAgentTransport)
if !ok {
t.Fatalf("transport type = %T, want *UserAgentTransport", sec.Base)
f := NewDefault(InvocationContext{})
if f.FileIOProvider != provider {
t.Fatalf("NewDefault() provider = %T, want %T", f.FileIOProvider, provider)
}
if _, ok := ua.Base.(*RetryTransport); !ok {
t.Fatalf("innermost transport type = %T, want *RetryTransport", ua.Base)
if provider.resolveCalls != 0 {
t.Fatalf("ResolveFileIO() calls after NewDefault() = %d, want 0", provider.resolveCalls)
}
}

func TestBuildSDKTransport_WithoutExtension(t *testing.T) {
exttransport.Register(nil)

transport := buildSDKTransport()

sec, ok := transport.(*internalauth.SecurityPolicyTransport)
if !ok {
t.Fatalf("outer transport type = %T, want *auth.SecurityPolicyTransport", transport)
}
ua, ok := sec.Base.(*UserAgentTransport)
if !ok {
t.Fatalf("middle transport type = %T, want *UserAgentTransport", sec.Base)
if got := f.ResolveFileIO(context.Background()); got == nil {
t.Fatal("ResolveFileIO() = nil, want non-nil")
}
if _, ok := ua.Base.(*RetryTransport); !ok {
t.Fatalf("inner transport type = %T, want *RetryTransport", ua.Base)
if provider.resolveCalls != 1 {
t.Fatalf("ResolveFileIO() calls after explicit resolve = %d, want 1", provider.resolveCalls)
}
}
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

Isolate config state in this test.

NewDefault still walks the normal factory initialization path, so this test can pick up machine-local config unless it sets a temp config dir first.

Suggested change
 func TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	prev := fileio.GetProvider()
 	provider := &countingFileIOProvider{}
 	fileio.Register(provider)
As per coding guidelines, `Isolate config state in Go tests by using t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())`.
📝 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 TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) {
prev := fileio.GetProvider()
provider := &countingFileIOProvider{}
fileio.Register(provider)
t.Cleanup(func() { fileio.Register(prev) })
mid := &extensionMiddleware{Base: capturer, Ext: tamperIC}
origCtx := context.WithValue(context.Background(), testKey, "original")
req, _ := http.NewRequestWithContext(origCtx, "GET", srv.URL, nil)
resp, err := mid.RoundTrip(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
resp.Body.Close()
// Built-in chain should see original context, not tampered
if ctxValue != "original" {
t.Fatalf("built-in chain saw context value %q, want %q", ctxValue, "original")
}
}
// interceptorFunc adapts a function to exttransport.Interceptor.
type interceptorFunc func(*http.Request) func(*http.Response, error)
func (f interceptorFunc) PreRoundTrip(req *http.Request) func(*http.Response, error) { return f(req) }
func TestBuildSDKTransport_WithExtension(t *testing.T) {
exttransport.Register(&stubTransportProvider{})
t.Cleanup(func() { exttransport.Register(nil) })
transport := buildSDKTransport()
// Chain: extensionMiddleware → SecurityPolicy → UserAgent → Retry → Base
mid, ok := transport.(*extensionMiddleware)
if !ok {
t.Fatalf("outer transport type = %T, want *extensionMiddleware", transport)
}
sec, ok := mid.Base.(*internalauth.SecurityPolicyTransport)
if !ok {
t.Fatalf("transport type = %T, want *auth.SecurityPolicyTransport", mid.Base)
}
ua, ok := sec.Base.(*UserAgentTransport)
if !ok {
t.Fatalf("transport type = %T, want *UserAgentTransport", sec.Base)
f := NewDefault(InvocationContext{})
if f.FileIOProvider != provider {
t.Fatalf("NewDefault() provider = %T, want %T", f.FileIOProvider, provider)
}
if _, ok := ua.Base.(*RetryTransport); !ok {
t.Fatalf("innermost transport type = %T, want *RetryTransport", ua.Base)
if provider.resolveCalls != 0 {
t.Fatalf("ResolveFileIO() calls after NewDefault() = %d, want 0", provider.resolveCalls)
}
}
func TestBuildSDKTransport_WithoutExtension(t *testing.T) {
exttransport.Register(nil)
transport := buildSDKTransport()
sec, ok := transport.(*internalauth.SecurityPolicyTransport)
if !ok {
t.Fatalf("outer transport type = %T, want *auth.SecurityPolicyTransport", transport)
}
ua, ok := sec.Base.(*UserAgentTransport)
if !ok {
t.Fatalf("middle transport type = %T, want *UserAgentTransport", sec.Base)
if got := f.ResolveFileIO(context.Background()); got == nil {
t.Fatal("ResolveFileIO() = nil, want non-nil")
}
if _, ok := ua.Base.(*RetryTransport); !ok {
t.Fatalf("inner transport type = %T, want *RetryTransport", ua.Base)
if provider.resolveCalls != 1 {
t.Fatalf("ResolveFileIO() calls after explicit resolve = %d, want 1", provider.resolveCalls)
}
}
func TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
prev := fileio.GetProvider()
provider := &countingFileIOProvider{}
fileio.Register(provider)
t.Cleanup(func() { fileio.Register(prev) })
f := NewDefault(InvocationContext{})
if f.FileIOProvider != provider {
t.Fatalf("NewDefault() provider = %T, want %T", f.FileIOProvider, provider)
}
if provider.resolveCalls != 0 {
t.Fatalf("ResolveFileIO() calls after NewDefault() = %d, want 0", provider.resolveCalls)
}
if got := f.ResolveFileIO(context.Background()); got == nil {
t.Fatal("ResolveFileIO() = nil, want non-nil")
}
if provider.resolveCalls != 1 {
t.Fatalf("ResolveFileIO() calls after explicit resolve = %d, want 1", provider.resolveCalls)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutil/factory_default_test.go` around lines 211 - 231, The test
TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization can pick up
machine-local config because NewDefault follows normal initialization; to
isolate state, set the config dir to a temp directory at the start of the test
by calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) before invoking
NewDefault(InvocationContext{}), so the factory initialization (NewDefault) will
use an isolated config directory for the duration of the test.

Comment on lines +81 to 82
if fi, err := runtime.FileIO().Stat(outputPath); err == nil && !fi.IsDir() {
return output.ErrValidation("--output %q is a file; batch mode expects a directory path", outputPath)
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

Fail fast when no FileIO provider is available.

runtime.FileIO() is dereferenced here and then stored into downloadOpts without a nil check. If a test or custom factory forgets to register a provider, downloadMediaFile will panic on Stat/Save instead of returning a normal CLI error.

Suggested change
 		errOut := runtime.IO().ErrOut
 		single := len(tokens) == 1
+		fio := runtime.FileIO()
+		if fio == nil {
+			return output.Errorf(output.ExitInternal, "io", "no file I/O provider registered")
+		}
 
 		// Batch mode: --output must be a directory, not an existing file.
 		if !single && outputPath != "" {
-			if fi, err := runtime.FileIO().Stat(outputPath); err == nil && !fi.IsDir() {
+			if fi, err := fio.Stat(outputPath); err == nil && !fi.IsDir() {
 				return output.ErrValidation("--output %q is a file; batch mode expects a directory path", outputPath)
 			}
 		}
@@
-			opts := downloadOpts{fio: runtime.FileIO(), overwrite: overwrite, usedNames: usedNames}
+			opts := downloadOpts{fio: fio, overwrite: overwrite, usedNames: usedNames}

Also applies to: 164-165

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

In `@shortcuts/minutes/minutes_download.go` around lines 81 - 82, The code
dereferences runtime.FileIO() (used in the Stat call and later saved into
downloadOpts used by downloadMediaFile) without checking for nil; add an
explicit nil check after calling runtime.FileIO() and return a CLI error (e.g.,
via output.ErrValidation or a suitable error helper) if no provider is
registered so the code fails fast instead of panicking. Update the two
occurrences (the Stat check around outputPath and the later assignment into
downloadOpts) to validate fileIO != nil before using or storing it, and return a
clear error message like "no FileIO provider registered" when nil.

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

Labels

domain/base PR touches the base domain domain/ccm PR touches the ccm domain domain/im PR touches the im domain domain/mail PR touches the mail domain domain/vc PR touches the vc domain size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant