mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-24 18:50:15 +00:00
Merge pull request #2138 into stage-344
fix: recover from stale deleted workspaces
This commit is contained in:
+19
-1
@@ -6967,7 +6967,7 @@ def _handle_chat_start(handler, body, diag=None):
|
||||
attachments = _normalize_chat_attachments(body.get("attachments") or [])[:20]
|
||||
diag.stage("resolve_workspace") if diag else None
|
||||
try:
|
||||
workspace = str(resolve_trusted_workspace(body.get("workspace") or s.workspace))
|
||||
workspace = _resolve_chat_workspace_with_recovery(s, body.get("workspace"))
|
||||
except ValueError as e:
|
||||
return bad(handler, str(e))
|
||||
requested_model = body.get("model") or s.model
|
||||
@@ -7000,6 +7000,24 @@ def _handle_chat_start(handler, body, diag=None):
|
||||
|
||||
|
||||
|
||||
def _resolve_chat_workspace_with_recovery(s, requested_workspace) -> str:
|
||||
"""Recover stale implicit session workspaces without hiding explicit errors."""
|
||||
explicit = requested_workspace not in (None, "")
|
||||
candidate = requested_workspace if explicit else getattr(s, "workspace", None)
|
||||
try:
|
||||
return str(resolve_trusted_workspace(candidate))
|
||||
except ValueError:
|
||||
if explicit:
|
||||
raise
|
||||
fallback = str(resolve_trusted_workspace(get_last_workspace()))
|
||||
s.workspace = fallback
|
||||
try:
|
||||
s.save()
|
||||
except Exception:
|
||||
pass
|
||||
return fallback
|
||||
|
||||
|
||||
def _normalize_chat_attachments(raw_attachments):
|
||||
"""Normalize attachment payloads from the browser.
|
||||
|
||||
|
||||
+7
-2
@@ -64,7 +64,7 @@ def _profile_default_workspace() -> str:
|
||||
2. 'default_workspace' — alternate explicit key
|
||||
3. 'terminal.cwd' — hermes-agent terminal working dir (most common)
|
||||
|
||||
Falls back to the boot-time DEFAULT_WORKSPACE constant.
|
||||
Falls back to the live DEFAULT_WORKSPACE from api.config.
|
||||
"""
|
||||
try:
|
||||
from api.config import get_config
|
||||
@@ -86,7 +86,12 @@ def _profile_default_workspace() -> str:
|
||||
return str(p)
|
||||
except (ImportError, Exception):
|
||||
logger.debug("Failed to load profile default workspace config")
|
||||
return str(_BOOT_DEFAULT_WORKSPACE)
|
||||
try:
|
||||
from api.config import DEFAULT_WORKSPACE as _LIVE_DEFAULT_WORKSPACE
|
||||
|
||||
return str(Path(_LIVE_DEFAULT_WORKSPACE).expanduser().resolve())
|
||||
except Exception:
|
||||
return str(Path(_BOOT_DEFAULT_WORKSPACE).expanduser().resolve())
|
||||
|
||||
|
||||
# ── Public API ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from api import config as api_config
|
||||
from api import routes, workspace
|
||||
|
||||
|
||||
def test_profile_default_workspace_uses_live_config_default(monkeypatch, tmp_path):
|
||||
live_default = tmp_path / "live-default"
|
||||
live_default.mkdir()
|
||||
|
||||
monkeypatch.setattr(api_config, "DEFAULT_WORKSPACE", live_default)
|
||||
monkeypatch.setattr(api_config, "get_config", lambda: {})
|
||||
|
||||
assert workspace._profile_default_workspace() == str(live_default.resolve())
|
||||
|
||||
|
||||
def test_resolve_chat_workspace_with_recovery_repairs_missing_implicit_workspace(monkeypatch, tmp_path):
|
||||
fallback = tmp_path / "fallback"
|
||||
fallback.mkdir()
|
||||
stale = tmp_path / "deleted-workspace"
|
||||
|
||||
def fake_resolve(value):
|
||||
if value == str(stale):
|
||||
raise ValueError(f"Path does not exist: {stale}")
|
||||
return Path(value).resolve()
|
||||
|
||||
saved = {"count": 0}
|
||||
|
||||
def fake_save():
|
||||
saved["count"] += 1
|
||||
|
||||
session = SimpleNamespace(session_id="sess-1", workspace=str(stale), save=fake_save)
|
||||
|
||||
monkeypatch.setattr(routes, "resolve_trusted_workspace", fake_resolve)
|
||||
monkeypatch.setattr(routes, "get_last_workspace", lambda: str(fallback))
|
||||
|
||||
resolved = routes._resolve_chat_workspace_with_recovery(session, None)
|
||||
|
||||
assert resolved == str(fallback.resolve())
|
||||
assert session.workspace == str(fallback.resolve())
|
||||
assert saved["count"] == 1
|
||||
|
||||
|
||||
def test_resolve_chat_workspace_with_recovery_preserves_explicit_errors(monkeypatch, tmp_path):
|
||||
fallback = tmp_path / "fallback"
|
||||
fallback.mkdir()
|
||||
stale = tmp_path / "deleted-workspace"
|
||||
|
||||
def fake_resolve(value):
|
||||
if value == str(stale):
|
||||
raise ValueError(f"Path does not exist: {stale}")
|
||||
return Path(value).resolve()
|
||||
|
||||
saved = {"count": 0}
|
||||
|
||||
def fake_save():
|
||||
saved["count"] += 1
|
||||
|
||||
session = SimpleNamespace(session_id="sess-2", workspace=str(fallback), save=fake_save)
|
||||
|
||||
monkeypatch.setattr(routes, "resolve_trusted_workspace", fake_resolve)
|
||||
monkeypatch.setattr(routes, "get_last_workspace", lambda: str(fallback))
|
||||
|
||||
with pytest.raises(ValueError, match="Path does not exist"):
|
||||
routes._resolve_chat_workspace_with_recovery(session, str(stale))
|
||||
|
||||
assert session.workspace == str(fallback)
|
||||
assert saved["count"] == 0
|
||||
Reference in New Issue
Block a user