From 31981db1a8e01f7d755c8ace2c6b8388eb091250 Mon Sep 17 00:00:00 2001 From: gujishh Date: Sun, 12 Apr 2026 18:34:33 +0900 Subject: [PATCH] fix(storage): treat append_file missing path as not_found --- openviking/storage/viking_fs.py | 8 +++- tests/misc/test_append_file_missing.py | 65 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/misc/test_append_file_missing.py diff --git a/openviking/storage/viking_fs.py b/openviking/storage/viking_fs.py index 2d2360dcd..cd12e2b98 100644 --- a/openviking/storage/viking_fs.py +++ b/openviking/storage/viking_fs.py @@ -1728,8 +1728,12 @@ async def append_file( except AGFSHTTPError as e: if e.status_code != 404: raise - except AGFSClientError: - raise + except AGFSClientError as e: + if "not found" not in (str(e) or "").lower(): + raise + except RuntimeError as e: + if "not found" not in (str(e) or "").lower(): + raise await self._ensure_parent_dirs(path) final_content = (existing + content).encode("utf-8") diff --git a/tests/misc/test_append_file_missing.py b/tests/misc/test_append_file_missing.py new file mode 100644 index 000000000..e9d9f77a8 --- /dev/null +++ b/tests/misc/test_append_file_missing.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. +# SPDX-License-Identifier: AGPL-3.0 +"""Ensures VikingFS.append_file treats missing files as empty content Before writing.""" + +import contextvars +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from openviking.pyagfs.exceptions import AGFSClientError + + +def _make_viking_fs(): + """Create a VikingFS instance with all required hooks mocked.""" + from openviking.storage.viking_fs import VikingFS + + fs = VikingFS.__new__(VikingFS) + fs.agfs = MagicMock() + fs.query_embedder = None + fs.vector_store = None + fs._bound_ctx = contextvars.ContextVar("vikingfs_bound_ctx", default=None) + return fs + + +@pytest.mark.asyncio +async def test_append_file_missing_runtime_error(): + """Missing file should not crash when append_file reads a RuntimeError 'not found'.""" + fs = _make_viking_fs() + fs._ensure_parent_dirs = AsyncMock() + fs.agfs.read.side_effect = RuntimeError("not found: /default/session/.../messages.jsonl") + fs.agfs.write = MagicMock() + + await fs.append_file("viking://session/default/foo/messages.jsonl", "hello\n") + + fs.agfs.write.assert_called_once() + path, payload = fs.agfs.write.call_args[0] + assert "messages.jsonl" in path + assert payload == b"hello\n" + + +@pytest.mark.asyncio +async def test_append_file_missing_client_error(): + """AGFSClientError carrying 'not found' should also be treated as empty existing content.""" + fs = _make_viking_fs() + fs._ensure_parent_dirs = AsyncMock() + fs.agfs.read.side_effect = AGFSClientError("not found: /default/session/default/messages.jsonl") + fs.agfs.write = MagicMock() + + await fs.append_file("viking://session/default/bar/messages.jsonl", "line\n") + + fs.agfs.write.assert_called_once() + _, payload = fs.agfs.write.call_args[0] + assert payload.endswith(b"line\n") + + +@pytest.mark.asyncio +async def test_append_file_other_runtime_error_bubbles(): + """RuntimeErrors without 'not found' should still propagate.""" + fs = _make_viking_fs() + fs._ensure_parent_dirs = AsyncMock() + fs.agfs.read.side_effect = RuntimeError("permission denied") + + with pytest.raises(RuntimeError): + await fs.append_file("viking://session/default/bad/messages.jsonl", "x\n")