From 3ed6d5dfc6dd0b216e1ba8c5365c9a4bf30a8f98 Mon Sep 17 00:00:00 2001 From: anti Date: Thu, 18 Jun 2026 21:27:36 -0400 Subject: [PATCH] refactor: consolidate writable-dir probe into decnet/paths.py bus.factory and vectorstore.factory carried byte-identical copies of the 'env override -> writable runtime dir -> ~/.decnet fallback' probe. Move it to decnet.paths.resolve_runtime_path and call it from both. The mkdir-create variants (deployer topologies dir, _pid_dir candidate iteration, personas_pool existence-precedence) are deliberately left inline: they're different policies, not the same probe. --- decnet/bus/factory.py | 15 ++++++------- decnet/paths.py | 41 +++++++++++++++++++++++++++++++++++ decnet/vectorstore/factory.py | 14 ++++++------ tests/test_paths.py | 32 +++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 decnet/paths.py create mode 100644 tests/test_paths.py diff --git a/decnet/bus/factory.py b/decnet/bus/factory.py index 4dc3ffb0..a8cc90ae 100644 --- a/decnet/bus/factory.py +++ b/decnet/bus/factory.py @@ -20,6 +20,7 @@ import os from typing import Any, cast from decnet.bus.base import BaseBus +from decnet.paths import resolve_runtime_path def get_bus(**kwargs: Any) -> BaseBus: @@ -58,14 +59,12 @@ def _default_socket_path() -> str: ``RuntimeDirectory=decnet`` sets it up with the right perms; the home fallback keeps dev boxes usable without systemd. """ - explicit = os.environ.get("DECNET_BUS_SOCKET") - if explicit: - return explicit - - runtime_dir = "/run/decnet" - if os.path.isdir(runtime_dir) and os.access(runtime_dir, os.W_OK): - return f"{runtime_dir}/bus.sock" - return os.path.expanduser("~/.decnet/bus.sock") + return resolve_runtime_path( + "bus.sock", + env_var="DECNET_BUS_SOCKET", + runtime_dir="/run/decnet", + user_fallback="~/.decnet/bus.sock", + ) def _maybe_wrap_telemetry(bus: BaseBus) -> BaseBus: diff --git a/decnet/paths.py b/decnet/paths.py new file mode 100644 index 00000000..019f8fd7 --- /dev/null +++ b/decnet/paths.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +"""Shared runtime filesystem path resolution. + +DECNET writes runtime state under a system dir provisioned by ``decnet +init`` / systemd (``/var/lib/decnet`` for state, ``/run/decnet`` for +sockets). On dev boxes without systemd, or in CI, that dir may be absent +or read-only, so callers fall back to a per-user location. + +:func:`resolve_runtime_path` centralises the writable-dir probe that the +vectorstore and bus backends previously copy-pasted verbatim. +""" +from __future__ import annotations + +import os + + +def resolve_runtime_path( + filename: str, + *, + env_var: str, + runtime_dir: str, + user_fallback: str, +) -> str: + """Resolve a runtime file path. Creates nothing. + + Precedence: + 1. ``$env_var`` if set (used verbatim). + 2. ``runtime_dir/filename`` if ``runtime_dir`` exists and is writable. + 3. ``user_fallback`` (``~`` expanded). + + ``runtime_dir`` is *probed*, never created: it is meant to be + provisioned with the right ownership and perms by init/systemd, so + creating it here with whatever perms the current process happens to + have would be worse than falling back to the user path. + """ + explicit = os.environ.get(env_var) + if explicit: + return explicit + if os.path.isdir(runtime_dir) and os.access(runtime_dir, os.W_OK): + return os.path.join(runtime_dir, filename) + return os.path.expanduser(user_fallback) diff --git a/decnet/vectorstore/factory.py b/decnet/vectorstore/factory.py index c2e754d3..b62580ef 100644 --- a/decnet/vectorstore/factory.py +++ b/decnet/vectorstore/factory.py @@ -26,6 +26,7 @@ import logging import os from typing import Any +from decnet.paths import resolve_runtime_path from decnet.vectorstore.base import BaseVectorStore LOG = logging.getLogger(__name__) @@ -65,10 +66,9 @@ def get_vectorstore(**kwargs: Any) -> BaseVectorStore: def _default_db_path() -> str: - explicit = os.environ.get("DECNET_VECTORSTORE_PATH") - if explicit: - return explicit - runtime_dir = "/var/lib/decnet" - if os.path.isdir(runtime_dir) and os.access(runtime_dir, os.W_OK): - return f"{runtime_dir}/vectors.sqlite" - return os.path.expanduser("~/.decnet/vectors.sqlite") + return resolve_runtime_path( + "vectors.sqlite", + env_var="DECNET_VECTORSTORE_PATH", + runtime_dir="/var/lib/decnet", + user_fallback="~/.decnet/vectors.sqlite", + ) diff --git a/tests/test_paths.py b/tests/test_paths.py new file mode 100644 index 00000000..4bf81273 --- /dev/null +++ b/tests/test_paths.py @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +"""Tests for the shared runtime-path probe.""" +from __future__ import annotations + +from decnet.paths import resolve_runtime_path + + +def _resolve(tmp_path, env=None, runtime_dir=None): + return resolve_runtime_path( + "x.sock", + env_var="DECNET_TEST_PATH", + runtime_dir=str(runtime_dir if runtime_dir is not None else tmp_path), + user_fallback="~/.decnet/x.sock", + ) + + +def test_env_override_wins(tmp_path, monkeypatch): + monkeypatch.setenv("DECNET_TEST_PATH", "/explicit/here.sock") + assert _resolve(tmp_path) == "/explicit/here.sock" + + +def test_writable_runtime_dir(tmp_path, monkeypatch): + monkeypatch.delenv("DECNET_TEST_PATH", raising=False) + assert _resolve(tmp_path) == str(tmp_path / "x.sock") + + +def test_falls_back_when_runtime_dir_absent(tmp_path, monkeypatch): + monkeypatch.delenv("DECNET_TEST_PATH", raising=False) + missing = tmp_path / "nope" # does not exist → not a writable dir + result = _resolve(tmp_path, runtime_dir=missing) + assert result.endswith("/.decnet/x.sock") + assert "~" not in result # tilde expanded