fix(utils): allow nested output directories (check nearest existing ancestor for writeability)#92
Conversation
`build_output_path` creates the output directory with `mkdir(parents=True, exist_ok=True)`, which may create several missing levels. But its `is_file_writeable` guard only checked the *immediate* parent: for a multi-level new path (e.g. `<base>/a/b/c` where `<base>/a` does not exist), `os.access(parent, W_OK)` is False, so it raised "Directory (...) is not writeable" even though `mkdir(parents=True)` would create it fine. Check the writeability of the nearest *existing* ancestor instead -- the level `mkdir(parents=True)` actually starts writing into. Single-level new paths (immediate parent exists) behave exactly as before. Adds tests for a multi-level `is_file_writeable` path and for `build_output_path` creating a nested directory.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds regression coverage and fixes path writeability detection so nested, non-existent output directories are treated as writable when an existing ancestor is writable.
Changes:
- Update
is_file_writeable()to check the nearest existing ancestor instead of only the immediate parent. - Add tests covering multi-level non-existent paths and verifying
build_output_path()creates nested directories.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/test_utils.py | Adds regression tests for multi-level path writeability and nested directory creation via build_output_path(). |
| minimax_mcp/utils.py | Fixes writeability checks for non-existent nested paths by walking up to the nearest existing ancestor. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if path.exists(): | ||
| return os.access(path, os.W_OK) | ||
| parent_dir = path.parent | ||
| return os.access(parent_dir, os.W_OK) | ||
| # The path does not exist yet. Callers create it with | ||
| # ``mkdir(parents=True)``, which may create several missing levels, so | ||
| # check writeability of the nearest existing ancestor rather than only the | ||
| # immediate parent (which is itself missing for a multi-level new path). | ||
| for ancestor in path.parents: | ||
| if ancestor.exists(): | ||
| return os.access(ancestor, os.W_OK) |
| # Multi-level new path: the immediate parent does not exist yet, but | ||
| # mkdir(parents=True) can create it, so it must be reported writeable | ||
| # (regression: only the immediate parent used to be checked). |
| # A nested, not-yet-existing output directory must be created rather than | ||
| # rejected as "not writeable". |
Address review: creating a child entry in a directory needs both write and search (execute) permission, so check os.W_OK | os.X_OK on the nearest existing ancestor (always a directory) rather than os.W_OK alone. No change on platforms where directories are always searchable; on POSIX it avoids a false positive for a write-but-not-executable directory.
|
Thanks @copilot —
2 & 3. "writeable" vs "writable" in comments. I kept "writeable" deliberately, to match this module's existing public API — the function is named |
Problem
build_output_pathcreates the output directory withmkdir(parents=True, exist_ok=True)— which can create several missing levels at once — but guards it withis_file_writeable, which only checks the immediate parent:For a multi-level new output directory (e.g.
<base>/a/b/cwhere<base>/adoesn't exist yet), the immediate parent<base>/a/bis itself missing, soos.access(<base>/a/b, W_OK)returnsFalseandbuild_output_pathraises:…even though
mkdir(parents=True)would create the whole chain without any problem. So passing a nestedoutput_directory(e.g.audio/2026/06) to any audio/image/video/music tool fails before the result can be saved.Fix
Check the writeability of the nearest existing ancestor — the level
mkdir(parents=True)actually starts writing into — instead of only the immediate parent:Single-level new paths (immediate parent exists) behave exactly as before — no regression.
Tests
Extends
test_is_file_writeablewith a multi-level new path and addstest_build_output_path_creates_nested_dir(createsa/b/cunder a temp dir). The multi-level branch was previously untested (test_make_output_pathusesis_test=True, which bypasses the writeability check).pytest tests/test_utils.pypasses 7/7.(Distinct from the open PRs — this is in
is_file_writeable/build_output_path, not the validation/timeout/play_audio areas.)