Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Self,
)

from nemo_platform_plugin.config import nmp_user_data_dir
from nemo_platform_plugin.schema import SecretRef
from pydantic import BaseModel, Field, field_validator, model_validator

Expand Down Expand Up @@ -91,7 +92,16 @@ def make_path_relative_to_program(cls, v: str) -> str:
This allows the config to pass in absolute paths, ``~``-prefixed
paths (expanded against the running user's home dir), or relative
paths like ``./files_storage`` (joined against cwd).

An **empty** path means the platform's user-data directory, so blobs follow
``NMP_DATA_DIR`` alongside the entity-store database.

Resolved here rather than as a field default because a machine-dependent default is
rendered into the committed config reference — the same reason the SQLite path is
computed in ``get_database_url``.
"""
if not v:
return str(nmp_user_data_dir() / "files")
return str(Path.cwd() / Path(v).expanduser())

@property
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""How a local storage path is resolved, and which state it keeps together.

Regression: ``NMP_DATA_DIR`` relocates the entity-store database, but the bundled local
configurations pinned the literal blob location, so a run with it set produced a *half*
isolated instance — database in the chosen directory, blobs in the default one. That is worse
than an un-isolated instance: it looks isolated, and wiping the chosen directory silently
leaves the blobs behind.
"""

import pytest
from nemo_platform_plugin.files.storage_config import LocalStorageConfig


def test_an_empty_path_follows_the_platform_data_dir(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
"""The fix: blobs land under the chosen data directory, beside the entity-store database."""
monkeypatch.setenv("NMP_DATA_DIR", str(tmp_path))
assert LocalStorageConfig(path="").path == str(tmp_path / "files")


def test_an_empty_path_without_a_data_dir_keeps_the_previous_location(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The value the bundled configs used to hard-code. Anyone who has not opted into a data
directory must see no change at all."""
monkeypatch.delenv("NMP_DATA_DIR", raising=False)
monkeypatch.setenv("HOME", "/home/someone")
monkeypatch.delenv("XDG_DATA_HOME", raising=False)
assert LocalStorageConfig(path="").path == "/home/someone/.local/share/nemo/files"


def test_an_explicit_path_is_never_overridden(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
"""The container image, the Helm chart and the agentic runners all set this explicitly."""
monkeypatch.setenv("NMP_DATA_DIR", str(tmp_path))
assert LocalStorageConfig(path="/data/files_storage").path == "/data/files_storage"


def test_home_relative_paths_still_expand(monkeypatch: pytest.MonkeyPatch) -> None:
"""Only the empty path gains meaning; every other form resolves exactly as before."""
monkeypatch.setenv("HOME", "/home/someone")
assert LocalStorageConfig(path="~/blobs").path == "/home/someone/blobs"


def test_the_data_dir_is_read_per_construction(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
"""Resolution happens in the validator, not as a field default, so it tracks the
environment at construction — and, just as importantly, keeps a machine-dependent path out
of the field default that generates the committed config reference."""
monkeypatch.setenv("NMP_DATA_DIR", str(tmp_path / "one"))
assert LocalStorageConfig(path="").path == str(tmp_path / "one" / "files")
monkeypatch.setenv("NMP_DATA_DIR", str(tmp_path / "two"))
assert LocalStorageConfig(path="").path == str(tmp_path / "two" / "files")
4 changes: 3 additions & 1 deletion packages/nmp_platform/config/local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ secrets:
files:
default_storage_config:
type: local
path: ~/.local/share/nemo/files
# Empty means the platform user-data directory, so blobs follow NMP_DATA_DIR
# alongside the entity-store database.
path: ""

studio:
static_files_path: web/packages/studio/dist
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,6 @@ secrets:
files:
default_storage_config:
type: local
path: ~/.local/share/nemo/files
# Empty means the platform user-data directory, so blobs follow NMP_DATA_DIR
# alongside the entity-store database.
path: ""