Fix: let removeFolder reject with the original fs.rm error - #815
Open
AmaadMartin wants to merge 3 commits into
Open
Fix: let removeFolder reject with the original fs.rm error#815AmaadMartin wants to merge 3 commits into
AmaadMartin wants to merge 3 commits into
Conversation
added 3 commits
August 8, 2026 09:51
removeFolder logged the fs.rm failure and resolved, so `adk create` on an existing folder continued into createFolder and generateFiles and merged the new scaffold over the stale directory. removeFolder now propagates, and AgentFile.dispose() -- the one caller that wants cleanup to be best-effort -- swallows the failure at the call site and logs it at debug level. listFiles keeps its empty-array fallback. Its JSDoc now states that an empty result is indistinguishable from an unreadable directory.
Covers the propagated fs.rm error, the surviving {recursive: true} option, the
listFiles empty-array fallback, both dispose() cleanup failures (rm and
unlink), and that `adk create` stops on a failed overwrite.
load() assigns cleanupDirPath and cleanupFilePath together, and the compiled file is created inside that directory, so the recursive removeFolder call already deletes it. The separate unlink was redundant and the inner cleanupDirPath guard could never be false. Removing the unlink also keeps the catch scoped to removeFolder, the only call whose contract this branch changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
No public issue.
Problem:
removeFolderindev/src/utils/file_utils.tslogged anfs.rmfailure and then resolved.
adk createuses it to clear an existing agentfolder before it recreates one, so a failed removal let the command continue and
write the new scaffold over the stale directory. The user saw a
Failed to remove folder ...line scroll past above the success banner, and the commandexited 0.
listFilesreturns[]on a read failure, but its one-line doccomment did not say so.
Solution:
removeFoldernow returnsfs.rm(...)and rejects with theoriginal error object, matching
loadFileDataandsaveToFilein the samemodule.
AgentFile.dispose()is the one caller that wants cleanup to bebest-effort, so it catches the failure at the call site and logs it through the
AdkLoggerit already owns atdebuglevel.listFileskeeps its fallback andits JSDoc now says an empty result is indistinguishable from an unreadable
directory.
Two decisions a reviewer will want stated:
dispose()also drops itsfsPromises.unlink(cleanupFilePath)call.load()assignscleanupDirPathandcleanupFilePathtogether atagent_loader.ts:212-213, and it creates the compiled file inside thatdirectory, so the recursive
removeFolderalready deletes the file. Theinner
if (this.cleanupDirPath)guard could never be false either. Removingboth keeps the new
catchscoped toremoveFolder, the only call whosecontract this PR changes, so no other error path is downgraded.
removeFolderOrThrow,gives identical behaviour but costs an extra symbol whose only difference is
its error handling. Calling
fs.rmdirectly fromcli_create.tsworks, butthat file routes every filesystem call through
file_utilstoday. WithremoveFolderpropagating,cli_create.tsneeds no change at all.Collision check:
gh pr list --repo AmaadMartin/adk-js --state open --limit 400plusgh pr diff --name-onlyon every adjacent PR. No open PR makesremoveFolderpropagate. Two PRs touch lines next to mine and neither is aduplicate: #708 routes the
file_utilsdiagnostics throughAdkLogger(itrewrites the
console.errorinside theremoveFoldercatch that this PRdeletes), and #725 moves
this.disposed = trueabove thecleanupFilePathguard in
dispose(). I branched frommainrather than stacking, becausestacking on #708 would put a deletion of that PR's new
removeFolder logs the failure and resolvestest into this diff. Whichever lands second resolves aone-hunk conflict. #704 makes the sibling
createFolderpropagate; it is adifferent function and merges cleanly.
Testing Plan
Unit Tests:
62 passed, 1 failed. The failure is pre-existing and unrelated:
should handle Vertex AI selection with gcloud defaultsreads the host's realgcloud config,so it fails on any machine with gcloud configured. It fails the same way on a
clean checkout of
main.Coverage of the changed source lines is 100% line and branch, measured with
--coverage.includeon the two changed files. The residual gaps infile_utils.tsarecreateFolder(untouched here) andcreateTempDir.Proof each new test can fail. I ran every test against mutated source and
recorded the failure:
removeFolder removes the folder recursively{recursive: true}expected "spy" to be called with arguments: [ '/some/dir', { recursive: true } ]removeFolder rejects with the original fs error instead of swallowing ittry/catcharoundfs.rmpromise resolved "undefined" instead of rejectinglistFiles returns an empty array when the directory cannot be readthrow epromise rejected "Error: EACCES: permission denied, scandir" instead of resolvingdispose resolves when the temp folder cannot be removedtry/catchfromdispose()promise rejected "Error: EACCES: permission denied, rm" instead of resolvingdispose resolves when the compiled artifact is already goneunlinkcall ahead ofremoveFolderexpected "spy" to be called with arguments: [ Array(1) ] / Number of calls: 0should surface a failed overwrite instead of writing into the stale folderremoveFoldercall incli_create.tsin atry/catchpromise resolved "undefined" instead of rejectingshould still finish when the post-create file listing comes back emptyfiles.length === 0promise rejected "Error: No files were created" instead of resolvingBe aware of what the sixth test does and does not prove.
cli_create_test.tsmocks the whole
file_utilsmodule, so that test passes both before and afterthe source change. It locks the contract that
cli_createmust not swallow aremoval failure. The regression test for the fix itself is
removeFolder rejects with the original fs error instead of swallowing it.Manual End-to-End (E2E) Tests:
I ran the real CLI against a real filesystem, as a non-root user.
Before this change:
Failed to remove folder ...and a secondFailed to create folder ... EEXISTon stderr, then the scaffold merged into the stale folder(
package.jsonandtsconfig.jsonappeared next toSTALE.txt, andagent.tswas overwritten).
After this change: one line,
[ADK CLI] Error creating agent: EACCES: permission denied, rmdir '/tmp/adk-e2e/demo'.STALE.txtand the originalagent.tssurvive untouched and no
Created the following files in ...banner prints.I also ran
adk run agent.tson a scaffolded agent and typedexit, whichexercises
AgentFile.dispose()through theawait usingbinding incli_run.ts. The count of/tmp/adk_agent_loader-*directories was the samebefore and after, so the temp directory is still cleaned up.
npm run lintandnpx prettier --checkare clean on the touched files.npm run ts:checkreports the same 288 pre-existing errors before and afterthis change, none of them in the files I touched.
CI note: the macOS leg failed once on two
tests/integration/app_loader/app_loader_test.tscases timing out at 40s. Neither test calls
dispose(). A re-run of the samecommit passed on all three operating systems, and #506, #545, #652 and #664
already track that flake, so I left it alone.
Checklist