mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-02 00:41:43 +08:00
Adds a deterministic pre-check on top of htsh's exception-based fallback: before calling /content/abstract or /content/overview on a non-pseudo URI, probe /api/v1/fs/stat. If the server says the URI is a file, route straight to /content/read instead of eating a failing 500 round-trip. This is the same idea pty819 and chennest independently landed in PRs #12757 and #12937 — merged here on top of htsh's broader fix so we keep pseudo-URI normalization and v0.3.3 browse-shape handling while avoiding the slow exception path on servers that return a raised 500 every time. The exception fallback from #5886 stays in place for environments where fs/stat is unavailable or returns an unfamiliar shape. Also credits pty819, chennest, and htsh in AUTHOR_MAP so future release notes attribute them correctly.
234 lines
9.2 KiB
Python
234 lines
9.2 KiB
Python
"""Tests for plugins/memory/openviking/__init__.py — URI normalization and payload handling."""
|
|
|
|
import json
|
|
|
|
from plugins.memory.openviking import OpenVikingMemoryProvider
|
|
|
|
|
|
class FakeVikingClient:
|
|
def __init__(self, responses):
|
|
self.responses = responses
|
|
self.calls = []
|
|
|
|
def get(self, path, params=None, **kwargs):
|
|
self.calls.append((path, params or {}))
|
|
response = self.responses[(path, tuple(sorted((params or {}).items())))]
|
|
if isinstance(response, Exception):
|
|
raise response
|
|
return response
|
|
|
|
|
|
class TestOpenVikingSummaryUriNormalization:
|
|
def test_normalize_summary_uri_maps_pseudo_files_to_parent_directory(self):
|
|
assert OpenVikingMemoryProvider._normalize_summary_uri("viking://user/hermes/.overview.md") == "viking://user/hermes"
|
|
assert OpenVikingMemoryProvider._normalize_summary_uri("viking://resources/.abstract.md") == "viking://resources"
|
|
assert OpenVikingMemoryProvider._normalize_summary_uri("viking://") == "viking://"
|
|
assert OpenVikingMemoryProvider._normalize_summary_uri("viking://user/hermes/memories/profile.md") == "viking://user/hermes/memories/profile.md"
|
|
|
|
|
|
class TestOpenVikingRead:
|
|
def test_overview_read_normalizes_uri_and_unwraps_result(self):
|
|
provider = OpenVikingMemoryProvider()
|
|
provider._client = FakeVikingClient(
|
|
{
|
|
(
|
|
"/api/v1/content/overview",
|
|
(("uri", "viking://user/hermes"),),
|
|
): {"result": {"content": "overview text"}},
|
|
}
|
|
)
|
|
|
|
result = json.loads(provider._tool_read({"uri": "viking://user/hermes/.overview.md", "level": "overview"}))
|
|
|
|
assert result["uri"] == "viking://user/hermes/.overview.md"
|
|
assert result["resolved_uri"] == "viking://user/hermes"
|
|
assert result["level"] == "overview"
|
|
assert result["content"] == "overview text"
|
|
assert provider._client.calls == [(
|
|
"/api/v1/content/overview",
|
|
{"uri": "viking://user/hermes"},
|
|
)]
|
|
|
|
def test_full_read_keeps_original_uri(self):
|
|
provider = OpenVikingMemoryProvider()
|
|
provider._client = FakeVikingClient(
|
|
{
|
|
(
|
|
"/api/v1/content/read",
|
|
(("uri", "viking://user/hermes/memories/profile.md"),),
|
|
): {"result": "full text"},
|
|
}
|
|
)
|
|
|
|
result = json.loads(provider._tool_read({"uri": "viking://user/hermes/memories/profile.md", "level": "full"}))
|
|
|
|
assert result["uri"] == "viking://user/hermes/memories/profile.md"
|
|
assert result["resolved_uri"] == "viking://user/hermes/memories/profile.md"
|
|
assert result["level"] == "full"
|
|
assert result["content"] == "full text"
|
|
assert provider._client.calls == [(
|
|
"/api/v1/content/read",
|
|
{"uri": "viking://user/hermes/memories/profile.md"},
|
|
)]
|
|
|
|
def test_overview_file_uri_routes_straight_to_content_read_via_stat_probe(self):
|
|
"""Pre-check via fs/stat: file URIs skip the directory-only endpoint entirely."""
|
|
provider = OpenVikingMemoryProvider()
|
|
file_uri = "viking://user/hermes/memories/entities/mem_abc.md"
|
|
provider._client = FakeVikingClient(
|
|
{
|
|
(
|
|
"/api/v1/fs/stat",
|
|
(("uri", file_uri),),
|
|
): {"result": {"isDir": False}},
|
|
(
|
|
"/api/v1/content/read",
|
|
(("uri", file_uri),),
|
|
): {"result": {"content": "full content"}},
|
|
}
|
|
)
|
|
|
|
result = json.loads(provider._tool_read({"uri": file_uri, "level": "overview"}))
|
|
|
|
assert result["uri"] == file_uri
|
|
assert result["resolved_uri"] == file_uri
|
|
assert result["level"] == "overview"
|
|
assert result["fallback"] == "content/read"
|
|
assert result["content"] == "full content"
|
|
assert provider._client.calls == [
|
|
("/api/v1/fs/stat", {"uri": file_uri}),
|
|
("/api/v1/content/read", {"uri": file_uri}),
|
|
]
|
|
|
|
def test_overview_dir_uri_skips_stat_when_pseudo_summary(self):
|
|
"""Pseudo-URI path already resolves to dir, so no stat probe needed."""
|
|
provider = OpenVikingMemoryProvider()
|
|
provider._client = FakeVikingClient(
|
|
{
|
|
(
|
|
"/api/v1/content/overview",
|
|
(("uri", "viking://user/hermes"),),
|
|
): {"result": "overview"},
|
|
}
|
|
)
|
|
|
|
result = json.loads(provider._tool_read({"uri": "viking://user/hermes/.overview.md", "level": "overview"}))
|
|
|
|
assert result["content"] == "overview"
|
|
# No fs/stat call — normalization already determined it's a directory.
|
|
assert provider._client.calls == [
|
|
("/api/v1/content/overview", {"uri": "viking://user/hermes"}),
|
|
]
|
|
|
|
def test_overview_directory_uri_uses_stat_probe_then_overview(self):
|
|
"""Non-pseudo directory URI: stat → isDir=True → summary endpoint."""
|
|
provider = OpenVikingMemoryProvider()
|
|
dir_uri = "viking://user/hermes/memories"
|
|
provider._client = FakeVikingClient(
|
|
{
|
|
(
|
|
"/api/v1/fs/stat",
|
|
(("uri", dir_uri),),
|
|
): {"result": {"isDir": True}},
|
|
(
|
|
"/api/v1/content/overview",
|
|
(("uri", dir_uri),),
|
|
): {"result": "dir overview"},
|
|
}
|
|
)
|
|
|
|
result = json.loads(provider._tool_read({"uri": dir_uri, "level": "overview"}))
|
|
|
|
assert result["content"] == "dir overview"
|
|
assert "fallback" not in result
|
|
assert provider._client.calls == [
|
|
("/api/v1/fs/stat", {"uri": dir_uri}),
|
|
("/api/v1/content/overview", {"uri": dir_uri}),
|
|
]
|
|
|
|
def test_overview_file_uri_falls_back_via_exception_when_stat_indeterminate(self):
|
|
"""If fs/stat raises or returns unknown shape, legacy exception fallback still kicks in."""
|
|
provider = OpenVikingMemoryProvider()
|
|
file_uri = "viking://user/hermes/memories/entities/mem_abc.md"
|
|
provider._client = FakeVikingClient(
|
|
{
|
|
(
|
|
"/api/v1/fs/stat",
|
|
(("uri", file_uri),),
|
|
): RuntimeError("stat unavailable"),
|
|
(
|
|
"/api/v1/content/overview",
|
|
(("uri", file_uri),),
|
|
): RuntimeError("500 Internal Server Error"),
|
|
(
|
|
"/api/v1/content/read",
|
|
(("uri", file_uri),),
|
|
): {"result": {"content": "fallback full content"}},
|
|
}
|
|
)
|
|
|
|
result = json.loads(provider._tool_read({"uri": file_uri, "level": "overview"}))
|
|
|
|
assert result["uri"] == file_uri
|
|
assert result["level"] == "overview"
|
|
assert result["fallback"] == "content/read"
|
|
assert result["content"] == "fallback full content"
|
|
assert provider._client.calls == [
|
|
("/api/v1/fs/stat", {"uri": file_uri}),
|
|
("/api/v1/content/overview", {"uri": file_uri}),
|
|
("/api/v1/content/read", {"uri": file_uri}),
|
|
]
|
|
|
|
def test_summary_uri_error_does_not_fallback_and_raises(self):
|
|
provider = OpenVikingMemoryProvider()
|
|
provider._client = FakeVikingClient(
|
|
{
|
|
(
|
|
"/api/v1/content/overview",
|
|
(("uri", "viking://user/hermes"),),
|
|
): RuntimeError("500 Internal Server Error"),
|
|
}
|
|
)
|
|
|
|
try:
|
|
provider._tool_read({"uri": "viking://user/hermes/.overview.md", "level": "overview"})
|
|
assert False, "Expected summary endpoint error to be raised"
|
|
except RuntimeError:
|
|
pass
|
|
|
|
assert provider._client.calls == [
|
|
("/api/v1/content/overview", {"uri": "viking://user/hermes"}),
|
|
]
|
|
|
|
|
|
class TestOpenVikingBrowse:
|
|
def test_list_browse_unwraps_and_normalizes_entry_shapes(self):
|
|
provider = OpenVikingMemoryProvider()
|
|
provider._client = FakeVikingClient(
|
|
{
|
|
(
|
|
"/api/v1/fs/ls",
|
|
(("uri", "viking://user/hermes"),),
|
|
): {
|
|
"result": {
|
|
"entries": [
|
|
{"name": "memories", "uri": "viking://user/hermes/memories", "type": "dir"},
|
|
{"rel_path": "profile.md", "uri": "viking://user/hermes/memories/profile.md", "isDir": False, "abstract": "Profile"},
|
|
]
|
|
}
|
|
},
|
|
}
|
|
)
|
|
|
|
result = json.loads(provider._tool_browse({"action": "list", "path": "viking://user/hermes"}))
|
|
|
|
assert result["path"] == "viking://user/hermes"
|
|
assert result["entries"] == [
|
|
{"name": "memories", "uri": "viking://user/hermes/memories", "type": "dir", "abstract": ""},
|
|
{"name": "profile.md", "uri": "viking://user/hermes/memories/profile.md", "type": "file", "abstract": "Profile"},
|
|
]
|
|
assert provider._client.calls == [(
|
|
"/api/v1/fs/ls",
|
|
{"uri": "viking://user/hermes"},
|
|
)]
|