Skip to content

refactor(file): split monolithic download module into focused submodules#26

Merged
BlackHole1 merged 1 commit into
mainfrom
imporve-test-2
Mar 27, 2026
Merged

refactor(file): split monolithic download module into focused submodules#26
BlackHole1 merged 1 commit into
mainfrom
imporve-test-2

Conversation

@BlackHole1
Copy link
Copy Markdown
Member

Extract the file download implementation into separate modules under download/ for better testability and maintainability:

  • errors.ts: download-specific error definitions
  • file-system.ts: temporary file and finalization logic
  • input.ts: URL, name, and extension option parsing
  • plan.ts: download session key creation and plan resolution
  • progress.ts: download progress reporting
  • request.ts: HTTP request handling with range support
  • session.ts: download session persistence
  • types.ts: shared type definitions

Each module has its own dedicated test file. The top-level download.test.ts now covers integration-level scenarios that exercise the full command handler.

Extract the file download implementation into separate modules under
download/ for better testability and maintainability:

- errors.ts: download-specific error definitions
- file-system.ts: temporary file and finalization logic
- input.ts: URL, name, and extension option parsing
- plan.ts: download session key creation and plan resolution
- progress.ts: download progress reporting
- request.ts: HTTP request handling with range support
- session.ts: download session persistence
- types.ts: shared type definitions

Each module has its own dedicated test file. The top-level
download.test.ts now covers integration-level scenarios that exercise
the full command handler.

Signed-off-by: Kevin Cui <bh@bugs.cc>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

Summary by CodeRabbit

  • Tests

    • Expanded test coverage for file download command with comprehensive end-to-end scenarios
    • Added tests for filename resolution, session management, and download resumption
    • Improved progress reporting and error handling verification
  • Refactor

    • Reorganized download command logic into focused, testable modules
    • Improved error handling and validation throughout the download workflow

Walkthrough

The pull request refactors the file download command handler by extracting large in-file implementations into dedicated modules. The monolithic download.ts is delegated to specialized handlers for input parsing (input.ts), download planning (plan.ts), session management (session.ts), HTTP requests (request.ts), filesystem operations (file-system.ts), and progress reporting (progress.ts). New modules for error handling (errors.ts) and type definitions (types.ts) provide shared utilities. Exported utility functions (parseFileDownloadNameOption, parseFileDownloadExtensionOption, formatByteCount) are removed from the main handler and relocated. Test coverage is refactored from local unit tests to integration-style tests exercising the full CliExecutionContext, along with new test suites for each new module.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Details

Complexity factors:

  • Scale & heterogeneity: Seven new modules with distinct responsibilities (input validation, session state, HTTP requests, filesystem safety, resumption logic) plus refactored handler and comprehensive new test suites across multiple files introduce substantial structural changes requiring separate reasoning for each module and integration points.
  • Logic density: Download session management (resumption, state persistence, cleanup), HTTP range-request coordination, temporary file lifecycle, and content-length verification involve interconnected state transitions and error conditions that demand careful tracing.
  • Test refactoring: Shift from isolated unit tests to integration-style tests with mocked dependencies (fetcher, session store) and temporary directories requires understanding both old and new assertion patterns and fixture construction.

Review focus areas:

  • Session state transitions (fresh, resumed, resumed-failed) and artifact cleanup
  • HTTP resume logic (206, 416 responses) and range-header construction
  • Temporary file safety (append vs. fresh mode, size validation, content-length mismatch)
  • Error propagation and recovery between modules
  • Test coverage adequacy for session/resumption/failure scenarios
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title follows the required format and clearly describes the main refactoring work of splitting a monolithic download module.
Description check ✅ Passed The pull request description is directly related to the changeset, detailing the extracted modules and their purposes.

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


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

❤️ Share

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

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

🧹 Nitpick comments (2)
src/application/commands/file/download/input.test.ts (1)

9-28: Add direct cases for the other exported input helpers.

src/application/commands/file/download/input.ts also introduces parseFileDownloadUrl() and ensureOutputDirectory(), but this suite only locks down the name/extension validators. A few focused cases for malformed URLs, non-HTTP schemes, existing-file out-dir paths, and mkdir/stat failures would make regressions fail much closer to the source.

Also applies to: 30-56

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

In `@src/application/commands/file/download/input.test.ts` around lines 9 - 28,
Add focused unit tests for the other exported input helpers in
src/application/commands/file/download/input.ts: cover parseFileDownloadUrl()
with malformed URL strings and non-HTTP/HTTPS schemes (assert it throws the
expected CLI user error/key), and cover ensureOutputDirectory() for cases where
the target path is an existing file (not a directory), for when mkdir fails, and
when stat fails (simulate errors via stubbing fs methods) to assert the correct
error keys are thrown; reference the functions parseFileDownloadUrl and
ensureOutputDirectory when adding tests so malformed URLs, non-http schemes,
existing-file out-dir paths, and mkdir/stat failure paths are exercised.
src/application/commands/file/download/file-system.ts (1)

146-170: Consider fallback for cross-filesystem scenarios.

The link() + unlink() approach provides atomicity on the same filesystem, but will fail with EXDEV if temporaryFilePath and directoryPath are on different mount points. Since the temp file is created in session.outDirPath which equals the output directory, this should be fine in practice—but if the design ever allows separate temp directories, a fallback to rename() or copy-and-delete would be needed.

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

In `@src/application/commands/file/download/file-system.ts` around lines 146 -
170, The finalizeDownloadedFile function currently uses link() followed by
unlink(), which will throw on cross-filesystem moves (EXDEV); update the catch
to detect error.code === 'EXDEV' (or equivalent) and in that case attempt a
fallback: try fs.rename(temporaryFilePath, candidateFilePath) first, and if
rename fails or is not permitted, fall back to fs.copyFile(temporaryFilePath,
candidateFilePath) then fs.unlink(temporaryFilePath); ensure all attempts still
return candidateFilePath on success and otherwise throw
createDownloadFailedError(candidateFilePath, ...) as before; reference functions
and symbols: finalizeDownloadedFile, link, unlink, rename, copyFile,
resolveAvailableFileName, createDownloadFailedError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/application/commands/file/download/request.ts`:
- Around line 42-48: The debug logs are currently emitting full URLs and query
strings (requestUrl, finalUrl, query) which can contain presigned tokens or
user:pass credentials; update the logging in the context.logger.debug calls in
src/application/commands/file/download/request.ts to log only the host and
pathname (use withRequestTarget(requestUrl.host, requestUrl.pathname) as-is) and
replace the full query/url/finalUrl fields with redacted values (e.g., log a
boolean or "REDACTED" for query presence or a masked URL that strips credentials
and query string). Apply the same change to the other context.logger.debug/warn
sites referenced (the blocks around the other occurrences at lines 57-64, 72-80,
91-98) and use requestUrl and finalUrl identifiers so the maintainers can find
and change those exact fields.
- Around line 119-127: The code currently sets a "Range" header regardless of
If-Range validation; change the logic in the download request flow (around
buildRequestHeaders, resolveIfRangeHeader, and headers usage) to refuse/rescind
resume attempts when resolveIfRangeHeader(session) returns undefined while there
is a non-zero localBytes (i.e., session.entityTag and session.lastModified are
both empty). Specifically: do NOT set the "Range" header or issue a partial
request when ifRangeValue is undefined and localBytes > 0 — instead reject the
resume (throw or return an error/flag so the caller restarts a fresh download)
so partial-append corruption cannot occur. Ensure any downstream callers handle
the new rejection path.

---

Nitpick comments:
In `@src/application/commands/file/download/file-system.ts`:
- Around line 146-170: The finalizeDownloadedFile function currently uses link()
followed by unlink(), which will throw on cross-filesystem moves (EXDEV); update
the catch to detect error.code === 'EXDEV' (or equivalent) and in that case
attempt a fallback: try fs.rename(temporaryFilePath, candidateFilePath) first,
and if rename fails or is not permitted, fall back to
fs.copyFile(temporaryFilePath, candidateFilePath) then
fs.unlink(temporaryFilePath); ensure all attempts still return candidateFilePath
on success and otherwise throw createDownloadFailedError(candidateFilePath, ...)
as before; reference functions and symbols: finalizeDownloadedFile, link,
unlink, rename, copyFile, resolveAvailableFileName, createDownloadFailedError.

In `@src/application/commands/file/download/input.test.ts`:
- Around line 9-28: Add focused unit tests for the other exported input helpers
in src/application/commands/file/download/input.ts: cover parseFileDownloadUrl()
with malformed URL strings and non-HTTP/HTTPS schemes (assert it throws the
expected CLI user error/key), and cover ensureOutputDirectory() for cases where
the target path is an existing file (not a directory), for when mkdir fails, and
when stat fails (simulate errors via stubbing fs methods) to assert the correct
error keys are thrown; reference the functions parseFileDownloadUrl and
ensureOutputDirectory when adding tests so malformed URLs, non-http schemes,
existing-file out-dir paths, and mkdir/stat failure paths are exercised.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9bc81ab-1a05-4cd3-a4b8-06a50f8249b7

📥 Commits

Reviewing files that changed from the base of the PR and between 099c006 and dbd0701.

📒 Files selected for processing (16)
  • src/application/commands/file/download.test.ts
  • src/application/commands/file/download.ts
  • src/application/commands/file/download/__tests__/helpers.ts
  • src/application/commands/file/download/errors.ts
  • src/application/commands/file/download/file-system.test.ts
  • src/application/commands/file/download/file-system.ts
  • src/application/commands/file/download/input.test.ts
  • src/application/commands/file/download/input.ts
  • src/application/commands/file/download/plan.ts
  • src/application/commands/file/download/progress.test.ts
  • src/application/commands/file/download/progress.ts
  • src/application/commands/file/download/request.test.ts
  • src/application/commands/file/download/request.ts
  • src/application/commands/file/download/session.test.ts
  • src/application/commands/file/download/session.ts
  • src/application/commands/file/download/types.ts

Comment on lines +42 to +48
context.logger.debug(
{
method: "GET",
...withRequestTarget(requestUrl.host, requestUrl.pathname),
query: requestUrl.searchParams.toString(),
url: requestUrl.toString(),
},
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

Redact full URLs and query strings before logging.

query, url, and finalUrl will record pre-signed tokens or user:pass@host credentials whenever users download from signed/private endpoints. That leaks sensitive data into routine debug/warn logs; keep the host/path fields and log only redacted URL details or query presence instead.

Also applies to: 57-64, 72-80, 91-98

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

In `@src/application/commands/file/download/request.ts` around lines 42 - 48, The
debug logs are currently emitting full URLs and query strings (requestUrl,
finalUrl, query) which can contain presigned tokens or user:pass credentials;
update the logging in the context.logger.debug calls in
src/application/commands/file/download/request.ts to log only the host and
pathname (use withRequestTarget(requestUrl.host, requestUrl.pathname) as-is) and
replace the full query/url/finalUrl fields with redacted values (e.g., log a
boolean or "REDACTED" for query presence or a masked URL that strips credentials
and query string). Apply the same change to the other context.logger.debug/warn
sites referenced (the blocks around the other occurrences at lines 57-64, 72-80,
91-98) and use requestUrl and finalUrl identifiers so the maintainers can find
and change those exact fields.

Comment on lines +119 to +127
const headers = buildRequestHeaders();

headers.set("Range", `bytes=${localBytes}-`);

const ifRangeValue = resolveIfRangeHeader(session);

if (ifRangeValue !== undefined) {
headers.set("If-Range", ifRangeValue);
}
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

Do not issue resume requests without an If-Range validator.

When both session.entityTag and session.lastModified are empty, this still sends a bare Range request. A changed upstream object can then return 206 for a different representation, and the later append will silently corrupt the partial download. Reject resume here, or make the caller restart fresh instead.

Also applies to: 132-146

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

In `@src/application/commands/file/download/request.ts` around lines 119 - 127,
The code currently sets a "Range" header regardless of If-Range validation;
change the logic in the download request flow (around buildRequestHeaders,
resolveIfRangeHeader, and headers usage) to refuse/rescind resume attempts when
resolveIfRangeHeader(session) returns undefined while there is a non-zero
localBytes (i.e., session.entityTag and session.lastModified are both empty).
Specifically: do NOT set the "Range" header or issue a partial request when
ifRangeValue is undefined and localBytes > 0 — instead reject the resume (throw
or return an error/flag so the caller restarts a fresh download) so
partial-append corruption cannot occur. Ensure any downstream callers handle the
new rejection path.

@BlackHole1 BlackHole1 merged commit 2b4301d into main Mar 27, 2026
2 checks passed
@BlackHole1 BlackHole1 deleted the imporve-test-2 branch March 27, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant