Skip to content

Add solution#325

Open
dminikulin wants to merge 3 commits intomate-academy:masterfrom
dminikulin:develop
Open

Add solution#325
dminikulin wants to merge 3 commits intomate-academy:masterfrom
dminikulin:develop

Conversation

@dminikulin
Copy link

No description provided.

Copy link

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

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

Review Summary

I’m requesting changes because there are a couple of critical blockers that violate explicit requirements and can break CLI behavior.

  • Critical issues to fix:

    1. Enforce exactly two CLI arguments (Checklist 1.2/2.2). In src/app.js, when args.length !== 2 you log an error but execution continues and move(oldPath, newPath) may receive undefined. After logging, immediately process.exit(1) or return so the program does not proceed. Also ensure the CLI invocation wraps move(...).catch(err => { console.error(err.message); process.exit(1); }) to exit non-zero on errors.
    2. Trailing-slash semantics (Checklist 1.3, 3.1–3.3). Add an explicit check before fs.stat(newPath): const trailing = newPath.endsWith(path.sep) || newPath.endsWith('/'). If trailing is true, treat newPath as a directory. Verify the directory exists and is a directory; if not, throw an error. If it exists, set the destination to path.join(newPath, path.basename(oldPath)).
    3. Error signaling for missing destination directory. Where the destination directory does not exist, the spec expects an error; currently you console.error and return. Replace these with throw new Error('Destination directory does not exist') (and similar for other invalid cases). Keep printing to stderr and exiting non-zero only in the CLI wrapper.
  • What you did well:

    • Validates source is a file and uses fs.rename, correctly removing the source on success, and handles existing destination directories by joining the basename.
  • Minor/optional:

    • When oldPath === newPath, your no-op return is acceptable; optionally print a brief message or keep it silent for minimalism.

Make these targeted updates and you should meet the spec and pass the related tests.


✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨

Important

Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.

Tip

If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.

Caution

Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.

Footnotes

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

Comment on lines +67 to +69
if (args.length !== 2) {
console.error('Exactly two arguments are required');
}

Choose a reason for hiding this comment

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

When the CLI is invoked with a number of args different than two, the code logs an error but does not stop execution. After the error message the script continues, destructures args and calls move with possibly undefined values. Requirement: the app must be executable via node index with exactly two arguments (checklist items 1.2 and 2.2). Change: after logging the error you should exit/return immediately (for example, call process.exit(1) or return) so the program does not continue. This enforces the "exactly two arguments" rule and avoids confusing follow-up errors.

src/app.js Outdated
Comment on lines 8 to 13
async function move(oldPath, newPath) {
if (!oldPath || !newPath) {
console.error('Source and destination are required.');

return;
}

Choose a reason for hiding this comment

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

The move() function does an early check for missing oldPath/newPath and returns — that's good defensive code. However, the top-level CLI should enforce argument count and not rely on move() to handle bad argument counts. Enforcing the check at the entry-point keeps behavior clear and consistent with the requirement that the executable accepts exactly two positional arguments.

src/app.js Outdated
Comment on lines 30 to 37
try {
const destStats = await fs.stat(newPath);

if (destStats.isDirectory()) {
const baseName = path.basename(src);

dest = path.join(newPath, baseName);
}

Choose a reason for hiding this comment

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

The code detects existing destination directories via fs.stat(newPath) and handles that case. But it does not explicitly handle the rule: "if destination path string ends with a slash ('/') it must be considered as a directory" (requirement 3.1). Add an explicit check for trailing slash (for example const destHasTrailingSlash = newPath.endsWith(path.sep) || newPath.endsWith('/')) and, if true, treat newPath as a directory: verify it exists and is a directory, then set dest = path.join(newPath, path.basename(src)). If the directory does not exist, throw/return an error (do not attempt to rename to a path with a trailing slash). This will make the behavior match the spec exactly.

src/app.js Outdated
Comment on lines 38 to 56
} catch (err) {
const destDir = path.dirname(newPath);

try {
const dirStats = await fs.stat(destDir);

if (!dirStats.isDirectory()) {
console.error('Destination directory does not exist');

return;
}
} catch {
console.error('Destination directory does not exist');

return;
}

dest = newPath;
}

Choose a reason for hiding this comment

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

The catch branch that handles fs.stat(newPath) failures currently computes destDir = path.dirname(newPath) and checks that destDir exists. That works for the case where newPath is intended as a filename in an existing directory. It does not, however, properly cover cases where the original newPath ends with a trailing slash (e.g. dir/). If newPath ends with a slash and the directory does not exist your code must throw an error per spec. Update this branch to consult the trailing-slash check and either build path.join(destDir, basename(src)) (if directory exists) or throw an error (if it doesn't).

src/app.js Outdated
Comment on lines 20 to 24
const src = oldPath;
let dest = newPath;
const srcStats = await fs.stat(src);

if (!srcStats.isFile()) {

Choose a reason for hiding this comment

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

Good: the code validates that the source exists and is a file using fs.stat(src) and srcStats.isFile(), and uses fs.rename which satisfies the requirement that the source is removed after a successful move (checklist items 3.4 and 3.5). Keep this behavior.

src/app.js Outdated
Comment on lines 15 to 17
if (oldPath === newPath) {
return;
}

Choose a reason for hiding this comment

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

Minor: when oldPath === newPath the function silently returns. That's acceptable, but consider whether you want to print a message or exit with a non-zero code depending on how strict you need to be. The task description doesn't require output in this case, so this is optional.

Copy link

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

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

Review Summary

Decision: Request changes

Your implementation is close, but several critical blockers prevent it from meeting the assignment’s requirements.

Critical issues to fix:

  • Error propagation in move (src/app.js: around lines 9–13, 62–63): the top-level try/catch swallows errors (only logs). This prevents the CLI’s .catch(...) from running and the process from exiting non-zero. Remove the outer catch or rethrow after logging so callers see rejections.
  • CLI arg enforcement (src/app.js: ~70–72): when args.length !== 2 you log but continue. Per spec, immediately process.exit(1) (and don’t call move). This is required so the CLI exits non-zero on invalid usage.
  • Ensure CLI catch executes (src/app.js: ~76–79): once move propagates errors, the existing .catch(err => { console.error(err.message); process.exit(1); }) will work as intended.
  • Cross-device fallback (src/app.js: rename call ~59–61): implement EXDEV handling. On err.code === 'EXDEV', copy to dest (e.g., fs.copyFile or streaming), then unlink(oldPath) only after a successful copy. If the copy fails, remove the partially written dest.
  • Overwrite safety (same block ~59–61): before moving, check if dest already exists (fs.stat). If it does, throw a clear error (e.g., Destination already exists) instead of relying on platform-dependent rename behavior.

Minor but required by spec:

  • Silence on success (src/app.js: line ~61): remove the success console.log. The tool should be quiet on success.

What you did well:

  • Source validation with fs.stat and isFile() is correct, and your directory-detection/trailing-slash logic is on the right track. Once you let errors propagate, these checks will cause proper non-zero exits when invalid.

Make these changes and you should meet the checklist and expected CLI behavior. Keep going—you’re very close.


✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨

Important

Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.

Tip

If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.

Caution

Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.

Footnotes

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

Comment on lines +9 to +13
try {
if (!oldPath || !newPath) {
throw new Error('Exactly two arguments are required');
}

Choose a reason for hiding this comment

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

Critical: The move function wraps its entire body in a try/catch and the catch only logs the error. This swallows errors and prevents the CLI wrapper from receiving a rejected promise. Remove the top-level try/catch here (or rethrow the error after logging) so callers can handle errors and the process can exit with non-zero. This is required so tests and the CLI detect failures (see checklist items about exiting non-zero on errors).

Comment on lines +62 to +63
} catch (err) {
console.error('Move error:', err.message);

Choose a reason for hiding this comment

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

Because of the catch above, thrown errors (e.g., missing source, missing destination directory) are printed but not propagated. The catch block should either rethrow the error or be removed. Currently this behavior prevents the CLI wrapper's .catch from executing. Let errors propagate so the process can exit 1 when move fails.

Comment on lines +70 to +72
if (args.length !== 2) {
console.error('Exactly two arguments are required');
}

Choose a reason for hiding this comment

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

CLI argument enforcement: you log an error when args.length !== 2 but do not stop execution. Per the spec you must enforce exactly two positional arguments and exit non-zero if the count is incorrect. Call process.exit(1) (or return after exiting) here and do not call move when the arguments are invalid.

Comment on lines +76 to +79
move(oldPath, newPath).catch((err) => {
console.error(err);
process.exit(1);
});

Choose a reason for hiding this comment

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

The CLI wrapper attaches a .catch to move(...) expecting rejections to be handled, but because move currently swallows errors this catch will rarely fire. Ensure move rejects on error (remove swallowing catch) so this .catch can call process.exit(1) and signal failure to the caller/test harness.

Comment on lines +59 to +61

await fs.rename(oldPath, dest);
console.log(`Moved ${oldPath} to ${dest}`);

Choose a reason for hiding this comment

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

Functional gap: fs.rename is invoked directly. On cross-device moves rename can fail with code === 'EXDEV'. Implement a fallback: on EXDEV do a copy (fs.copyFile or streaming copy) from oldPath to dest, then unlink(oldPath) only after a successful copy. Also ensure you remove any partially copied dest if the copy fails. This is required to match mv behavior across filesystems.

Comment on lines +59 to +61

await fs.rename(oldPath, dest);
console.log(`Moved ${oldPath} to ${dest}`);

Choose a reason for hiding this comment

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

Overwrite safety: before renaming/copying, check whether the computed dest already exists. The assignment expects you not to silently overwrite; either fail with an error or rely on rename to error consistently. It's safer to stat dest and throw if it exists rather than risking platform-dependent overwrite behavior.

}

await fs.rename(oldPath, dest);
console.log(`Moved ${oldPath} to ${dest}`);

Choose a reason for hiding this comment

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

The success console.log(Moved ${oldPath} to ${dest}) prints output on successful moves. The specification states no extra output is required on success. Remove this line to keep the utility silent on success.

Comment on lines +18 to +22
const srcStats = await fs.stat(oldPath);

if (!srcStats.isFile()) {
throw new Error('Source is not a file');
}

Choose a reason for hiding this comment

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

Validation: your source validation (stat + isFile()) is correct. However, if fs.stat(oldPath) throws (source missing), that error must propagate to the CLI wrapper so the process can exit non-zero. After removing the swallow-catch the existing logic will correctly cause a rejection on missing source.

Comment on lines +24 to +35
const trailing = newPath.endsWith(path.sep) || newPath.endsWith('/');
let dest;

if (trailing) {
const destStats = await fs.stat(newPath);

if (!destStats.isDirectory()) {
throw new Error('Destination is not a directory');
}

dest = path.join(newPath, path.basename(oldPath));
} else {

Choose a reason for hiding this comment

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

Trailing-slash handling: you correctly detect trailing slash with newPath.endsWith(path.sep) || newPath.endsWith('/'). When trailing is true you stat newPath; ensure that if stat throws because the directory does not exist you allow that error to propagate (and cause a non-zero exit) rather than only logging it. With the top-level catch removed this will behave as required by the spec.

Copy link

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

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

Review Summary

Educational approval: You’ve implemented many core behaviors correctly, and this pass is to help you progress while you address remaining edge cases and error handling.

What you did well:

  • Solid validation of the source file with fs.stat(oldPath) and isFile() (lines 18–22).
  • Correct handling of trailing slash and existing destination directory logic to derive dest (lines 27–34 and 36–43).
  • Proper destination parent-directory check when newPath doesn’t exist (lines 44–53), and clean export module.exports = { move }.

What needs improvement (why and how):

  • Error propagation: The top-level try/catch in move (lines 9–64) swallows errors, preventing required rejections and non-zero CLI exits. Remove the outer catch or rethrow after logging so callers observe failures.
  • CLI arg validation: When args.length !== 2 (lines 70–72), exit immediately with process.exit(1) and don’t call move to meet the CLI requirement.
  • Cross-device moves: Wrap await fs.rename(oldPath, dest) (around line 60) in try/catch; on err.code === 'EXDEV', perform await fs.copyFile(oldPath, dest) then await fs.unlink(oldPath). If copy fails, remove any partial dest.
  • Deterministic overwrite behavior: Before renaming/copying, check if dest already exists; if so, throw a clear error like Destination already exists to avoid platform-dependent overwrites.
  • Silence on success: Remove the success console.log (line 61) to keep the tool quiet when everything works.

You’re very close—your path resolution and validation logic are on the right track. Tightening error propagation and adding the EXDEV fallback will align the tool with mv-like semantics and the checklist. Keep going—you’ve built a solid foundation!


✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨

Footnotes

  1. Rate AI review example

Comment on lines +9 to +64
try {
if (!oldPath || !newPath) {
throw new Error('Exactly two arguments are required');
}

if (oldPath === newPath) {
return;
}

const srcStats = await fs.stat(oldPath);

if (!srcStats.isFile()) {
throw new Error('Source is not a file');
}

const trailing = newPath.endsWith(path.sep) || newPath.endsWith('/');
let dest;

if (trailing) {
const destStats = await fs.stat(newPath);

if (!destStats.isDirectory()) {
throw new Error('Destination is not a directory');
}

dest = path.join(newPath, path.basename(oldPath));
} else {
try {
const destStats = await fs.stat(newPath);

if (destStats.isDirectory()) {
dest = path.join(newPath, path.basename(oldPath));
} else {
dest = newPath;
}
} catch (err) {
if (err.code === 'ENOENT') {
const destDir = path.dirname(newPath);
const dirStats = await fs.stat(destDir);

if (!dirStats.isDirectory()) {
throw new Error('Destination directory does not exist');
}

dest = newPath;
} else {
throw err;
}
}
}

await fs.rename(oldPath, dest);
console.log(`Moved ${oldPath} to ${dest}`);
} catch (err) {
console.error('Move error:', err.message);
}

Choose a reason for hiding this comment

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

Top-level error swallowing in move() — prevents callers from observing failures.

You wrap the entire move body in a try/catch that only logs the error and does not rethrow (lines 9–64). Per the specification the function must reject on error so the CLI and tests can observe a failure and exit non-zero. Remove this outer try/catch or rethrow the error after logging so callers receive the rejection. This addresses checklist items about error handling and CLI exit behavior.

Comment on lines +70 to +72
if (args.length !== 2) {
console.error('Exactly two arguments are required');
}

Choose a reason for hiding this comment

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

CLI argument validation must exit immediately on invalid usage.

At lines 70–72 you log when args.length !== 2 but then continue to call move. The program should not proceed — call process.exit(1) (or throw) immediately after logging to ensure a non-zero exit on invalid usage, as required by the task.

Comment on lines +58 to +61
}

await fs.rename(oldPath, dest);
console.log(`Moved ${oldPath} to ${dest}`);

Choose a reason for hiding this comment

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

fs.rename has no EXDEV fallback — add copy+unlink on cross-device errors.

You call await fs.rename(oldPath, dest) (line 60) without handling err.code === 'EXDEV'. Implement a try/catch around fs.rename; if the error code is EXDEV, perform a safe copy (e.g., fs.copyFile(oldPath, dest) or stream copy) and only await fs.unlink(oldPath) after a successful copy. Also ensure that if copy fails you remove any partially-created dest file to avoid leaving partial results. This satisfies the cross-device handling requirement in the checklist.

}

await fs.rename(oldPath, dest);
console.log(`Moved ${oldPath} to ${dest}`);

Choose a reason for hiding this comment

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

Do not log success — tool should be silent on success.

Remove the console.log('Moved ...') on line 61. The program should produce no output when the operation succeeds (per project expectations).

Comment on lines +36 to +43
try {
const destStats = await fs.stat(newPath);

if (destStats.isDirectory()) {
dest = path.join(newPath, path.basename(oldPath));
} else {
dest = newPath;
}

Choose a reason for hiding this comment

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

Check for existing destination to avoid platform-dependent overwrite.

Before performing the rename/copy you should explicitly check whether dest exists and fail with a clear error (for example: Destination already exists). The current code calculates dest but does not check for its existence prior to fs.rename (see block where dest is determined, lines 36–43). Add a fs.stat(dest) or fs.access(dest) check and throw if it exists so behavior is deterministic and tests won't depend on platform rename semantics.

Comment on lines +27 to +34
if (trailing) {
const destStats = await fs.stat(newPath);

if (!destStats.isDirectory()) {
throw new Error('Destination is not a directory');
}

dest = path.join(newPath, path.basename(oldPath));

Choose a reason for hiding this comment

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

Trailing-slash directory handling is implemented but errors must propagate.

The trailing-slash branch (lines 27–34) correctly attempts fs.stat(newPath) and verifies it's a directory. However, because the outer catch swallows errors, a missing directory will only be logged, not cause the process to fail. Once you remove/rework the swallow-catch, the ENOENT will correctly surface as required by the spec.

Comment on lines +18 to +22
const srcStats = await fs.stat(oldPath);

if (!srcStats.isFile()) {
throw new Error('Source is not a file');
}

Choose a reason for hiding this comment

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

Source validation is correct — ensure errors are not swallowed.

You call fs.stat(oldPath) and verify isFile() (lines 18–22), which matches the requirement to only support files as sources. Ensure that ENOENT or the Source is not a file error is allowed to propagate to the caller (remove the top-level suppression) so the CLI exits non-zero on invalid sources.

Comment on lines +44 to +53
} catch (err) {
if (err.code === 'ENOENT') {
const destDir = path.dirname(newPath);
const dirStats = await fs.stat(destDir);

if (!dirStats.isDirectory()) {
throw new Error('Destination directory does not exist');
}

dest = newPath;

Choose a reason for hiding this comment

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

Destination parent-directory check is correct but must propagate.

When fs.stat(newPath) throws ENOENT you check the parent directory and throw Destination directory does not exist (lines 44–53). This matches checklist requirements — keep this logic but ensure the error reaches the caller (remove the outer swallow-catch).

});
}

module.exports = { move };

Choose a reason for hiding this comment

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

module.exports is fine — keep move async and rejecting on error for tests.

module.exports = { move } (line 82) is correct. Tests or consumers will expect move() to reject on error; ensure move does not swallow errors so exported function behaves as expected.

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.

2 participants