diff --git a/openviking/storage/viking_fs.py b/openviking/storage/viking_fs.py index 37ceec16..7821ce68 100644 --- a/openviking/storage/viking_fs.py +++ b/openviking/storage/viking_fs.py @@ -150,7 +150,8 @@ async def mkdir(self, uri: str, mode: str = "755", exist_ok: bool = False) -> No return None except Exception: pass - return + + await asyncio.to_thread(self.agfs.mkdir, path) async def rm(self, uri: str, recursive: bool = False) -> Dict[str, Any]: """Delete file/directory + recursively update vector index.""" diff --git a/tests/README.md b/tests/README.md index d2fb0c9e..cd4d03c6 100644 --- a/tests/README.md +++ b/tests/README.md @@ -157,6 +157,7 @@ Miscellaneous tests. | `test_config_validation.py` | Configuration validation | Config schema validation, required fields, type checking | | `test_debug_service.py` | Debug service | Debug endpoint tests, service diagnostics | | `test_extract_zip.py` | Zip extraction security (Zip Slip) | Path traversal prevention (`../`), absolute path rejection, symlink entry filtering, backslash traversal, UNC path rejection, directory entry skipping, normal extraction | +| `test_mkdir.py` | VikingFS.mkdir() fix verification | mkdir calls agfs.mkdir, exist_ok=True skips existing, exist_ok=True creates missing, default creation, parent-before-target ordering | | `test_port_check.py` | AGFS port check socket leak fix | Available port no leak, occupied port raises RuntimeError, occupied port no ResourceWarning | ### engine/ diff --git a/tests/misc/test_mkdir.py b/tests/misc/test_mkdir.py new file mode 100644 index 00000000..5c559971 --- /dev/null +++ b/tests/misc/test_mkdir.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 +"""Tests for VikingFS.mkdir() — verifies the target directory is actually created.""" + +import os +import sys +from unittest.mock import AsyncMock, MagicMock + +import pytest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + + +def _make_viking_fs(): + """Create a VikingFS instance with mocked AGFS backend.""" + from openviking.storage.viking_fs import VikingFS + + fs = VikingFS.__new__(VikingFS) + fs.agfs = MagicMock() + fs.agfs.mkdir = MagicMock(return_value=None) + fs.query_embedder = None + fs.vector_store = None + fs._uri_prefix = "viking://" + return fs + + +class TestMkdir: + """Test that mkdir() actually creates the target directory.""" + + @pytest.mark.asyncio + async def test_mkdir_calls_agfs_mkdir(self): + """mkdir() must call agfs.mkdir with the target path.""" + fs = _make_viking_fs() + fs._ensure_parent_dirs = AsyncMock() + fs.stat = AsyncMock(side_effect=Exception("not found")) + + await fs.mkdir("viking://resources/new_dir") + + fs.agfs.mkdir.assert_called_once() + call_path = fs.agfs.mkdir.call_args[0][0] + assert call_path.endswith("resources/new_dir") + + @pytest.mark.asyncio + async def test_mkdir_exist_ok_true_existing(self): + """mkdir(exist_ok=True) should return early if directory exists.""" + fs = _make_viking_fs() + fs._ensure_parent_dirs = AsyncMock() + fs.stat = AsyncMock(return_value={"type": "directory"}) + + await fs.mkdir("viking://resources/existing_dir", exist_ok=True) + + # Should NOT call agfs.mkdir because directory already exists + fs.agfs.mkdir.assert_not_called() + + @pytest.mark.asyncio + async def test_mkdir_exist_ok_true_not_existing(self): + """mkdir(exist_ok=True) should create dir if it does not exist.""" + fs = _make_viking_fs() + fs._ensure_parent_dirs = AsyncMock() + fs.stat = AsyncMock(side_effect=Exception("not found")) + + await fs.mkdir("viking://resources/new_dir", exist_ok=True) + + fs.agfs.mkdir.assert_called_once() + call_path = fs.agfs.mkdir.call_args[0][0] + assert call_path.endswith("resources/new_dir") + + @pytest.mark.asyncio + async def test_mkdir_exist_ok_false_default(self): + """mkdir(exist_ok=False) should always attempt to create.""" + fs = _make_viking_fs() + fs._ensure_parent_dirs = AsyncMock() + + await fs.mkdir("viking://resources/another_dir") + + fs.agfs.mkdir.assert_called_once() + + @pytest.mark.asyncio + async def test_mkdir_ensures_parents_first(self): + """mkdir() must call _ensure_parent_dirs before creating target.""" + fs = _make_viking_fs() + call_order = [] + fs._ensure_parent_dirs = AsyncMock(side_effect=lambda p: call_order.append("parents")) + fs.agfs.mkdir = MagicMock(side_effect=lambda p: call_order.append("mkdir")) + + await fs.mkdir("viking://a/b/c") + + assert call_order == ["parents", "mkdir"]