replace main with lightweight memory gateway

This commit is contained in:
2026-06-11 10:06:48 +08:00
parent 000415404b
commit b74923e435
56 changed files with 2052 additions and 76129 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 KiB

419
tests/test_gateway.py Normal file
View File

@ -0,0 +1,419 @@
from __future__ import annotations
from pathlib import Path
from typing import Any
import httpx
import pytest
from core.api import create_app
from core.config import GatewayConfig
from core.db import init_db
from core.repository import MemoryRepository
class FakeEverOSClient:
def __init__(self, search_results: list[dict[str, Any]] | None = None) -> None:
self.add_calls: list[dict[str, Any]] = []
self.flush_calls: list[dict[str, str]] = []
self.search_calls: list[dict[str, Any]] = []
self.search_results = search_results or []
async def add_memory(self, payload: dict[str, Any]) -> dict[str, Any]:
self.add_calls.append(payload)
return {"request_id": "add", "data": {"status": "accumulated"}}
async def flush_memory(
self,
session_id: str,
app_id: str,
project_id: str,
) -> dict[str, Any]:
self.flush_calls.append(
{"session_id": session_id, "app_id": app_id, "project_id": project_id}
)
return {"request_id": "flush", "data": {"status": "extracted"}}
async def search_memory(self, payload: dict[str, Any]) -> dict[str, Any]:
self.search_calls.append(payload)
return {"request_id": "search", "data": {"episodes": self.search_results}}
@pytest.fixture
def config(tmp_path: Path) -> GatewayConfig:
return GatewayConfig(
everos_base_url="http://everos.test",
database_path=tmp_path / "gateway.sqlite3",
storage_dir=tmp_path / "storage",
)
@pytest.fixture
def repo(config: GatewayConfig) -> MemoryRepository:
init_db(config.database_path)
return MemoryRepository(config.database_path)
def app_client(
config: GatewayConfig,
everos_client: FakeEverOSClient,
) -> httpx.AsyncClient:
app = create_app(config=config, everos_client=everos_client)
transport = httpx.ASGITransport(app=app)
return httpx.AsyncClient(transport=transport, base_url="http://test")
async def create_user(client: httpx.AsyncClient, user_id: str = "u_123") -> str:
response = await client.post("/users", json={"user_id": user_id})
assert response.status_code == 200, response.text
body = response.json()
assert body["user_id"] == user_id
assert body["user_key"]
return body["user_key"]
@pytest.mark.asyncio
async def test_create_user_generates_and_persists_user_key(
config: GatewayConfig,
repo: MemoryRepository,
) -> None:
everos = FakeEverOSClient()
async with app_client(config, everos) as client:
user_key = await create_user(client, "u_123")
user = repo.get_user("u_123")
assert user is not None
assert user["user_key"] == user_key
@pytest.mark.asyncio
async def test_upload_resource_creates_record_and_calls_everos(
config: GatewayConfig,
) -> None:
everos = FakeEverOSClient()
async with app_client(config, everos) as client:
user_key = await create_user(client)
response = await client.post(
"/resources",
data={"user_id": "u_123", "user_key": user_key, "title": "Contract"},
files={"file": ("contract.txt", b"pay in 30 days", "text/plain")},
)
assert response.status_code == 200, response.text
body = response.json()
resource_id = body["resource_id"]
assert body["session_id"] == f"resource:u_123:{resource_id}"
assert body["uri"] == f"resource://u_123/{resource_id}"
assert body["status"] == "extracted"
repo = MemoryRepository(config.database_path)
resource = repo.get_resource(resource_id)
assert resource is not None
assert resource["status"] == "extracted"
assert resource["original_filename"] == "contract.txt"
assert resource["content_type"] == "text"
assert resource["sha256"]
assert resource["size_bytes"] == len(b"pay in 30 days")
assert not resource["uri"].startswith("resource://")
assert len(everos.add_calls) == 1
add_payload = everos.add_calls[0]
assert add_payload["session_id"] == f"resource:u_123:{resource_id}"
content = add_payload["messages"][0]["content"][0]
assert content["type"] == "text"
assert content["uri"].startswith("file://")
assert content["extras"] == {"resource_id": resource_id, "source": "user_upload"}
assert everos.flush_calls == [
{
"session_id": f"resource:u_123:{resource_id}",
"app_id": "default",
"project_id": "default",
}
]
@pytest.mark.asyncio
async def test_resource_detail_does_not_leak_internal_uri(
config: GatewayConfig,
) -> None:
everos = FakeEverOSClient()
async with app_client(config, everos) as client:
user_key = await create_user(client)
created = await client.post(
"/resources",
data={"user_id": "u_123", "user_key": user_key},
files={"file": ("note.txt", b"hello", "text/plain")},
)
resource_id = created.json()["resource_id"]
detail = await client.get(
f"/resources/{resource_id}",
params={"user_id": "u_123", "user_key": user_key},
)
assert detail.status_code == 200, detail.text
resources = detail.json()["resources"]
assert len(resources) == 1
assert resources[0]["uri"] == f"resource://u_123/{resource_id}"
assert "file://" not in detail.text
@pytest.mark.asyncio
async def test_resource_detail_returns_empty_when_user_has_no_resource(
config: GatewayConfig,
) -> None:
everos = FakeEverOSClient()
async with app_client(config, everos) as client:
user_key = await create_user(client, "u_empty")
response = await client.get(
"/resources/r_missing",
params={"user_id": "u_empty", "user_key": user_key},
)
assert response.status_code == 200, response.text
assert response.json() == {"resources": []}
@pytest.mark.asyncio
async def test_resources_are_isolated_by_user_key(
config: GatewayConfig,
) -> None:
everos = FakeEverOSClient()
async with app_client(config, everos) as client:
alice_key = await create_user(client, "alice")
bob_key = await create_user(client, "bob")
created = await client.post(
"/resources",
data={"user_id": "alice", "user_key": alice_key},
files={"file": ("alice.txt", b"alice-only", "text/plain")},
)
resource_id = created.json()["resource_id"]
bob_detail = await client.get(
f"/resources/{resource_id}",
params={"user_id": "bob", "user_key": bob_key},
)
bob_list = await client.get(
"/resources",
params={"user_id": "bob", "user_key": bob_key},
)
assert bob_detail.status_code == 200, bob_detail.text
assert bob_detail.json() == {"resources": []}
assert bob_list.status_code == 200, bob_list.text
assert bob_list.json() == {"resources": []}
@pytest.mark.asyncio
async def test_resource_api_rejects_invalid_user_key(
config: GatewayConfig,
) -> None:
everos = FakeEverOSClient()
async with app_client(config, everos) as client:
await create_user(client, "u_123")
response = await client.get(
"/resources",
params={"user_id": "u_123", "user_key": "wrong"},
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_deleted_resource_is_excluded_from_resource_scope_search(
config: GatewayConfig,
) -> None:
everos = FakeEverOSClient(
[{"id": "mem_1", "session_id": "resource:u_123:r_live", "episode": "live"}]
)
async with app_client(config, everos) as client:
user_key = await create_user(client)
created = await client.post(
"/resources",
data={"user_id": "u_123", "user_key": user_key},
files={"file": ("note.txt", b"hello", "text/plain")},
)
resource_id = created.json()["resource_id"]
delete_response = await client.delete(
f"/resources/{resource_id}",
params={"user_id": "u_123", "user_key": user_key},
)
search_response = await client.post(
"/memories/search",
json={
"user_id": "u_123",
"user_key": user_key,
"query": "hello",
"scope": ["resources"],
"top_k": 8,
},
)
assert delete_response.status_code == 200
assert search_response.status_code == 200
assert everos.search_calls == []
assert search_response.json()["results"] == []
@pytest.mark.asyncio
async def test_tombstone_filters_search_results(
config: GatewayConfig,
repo: MemoryRepository,
) -> None:
repo.create_resource(
id="r_1",
user_id="u_123",
app_id="default",
project_id="default",
session_id="resource:u_123:r_1",
original_filename="a.txt",
mime_type="text/plain",
content_type="text",
uri="file:///private/a.txt",
uri_public=False,
sha256="abc",
size_bytes=1,
title=None,
description=None,
status="extracted",
error_message=None,
)
repo.add_tombstone(
user_id="u_123",
memory_id="mem_deleted",
session_id=None,
reason="user deleted",
)
everos = FakeEverOSClient(
[
{"id": "mem_deleted", "session_id": "resource:u_123:r_1", "episode": "x"},
{"id": "mem_live", "session_id": "resource:u_123:r_1", "episode": "y"},
]
)
async with app_client(config, everos) as client:
user_key = await create_user(client)
response = await client.post(
"/memories/search",
json={
"user_id": "u_123",
"user_key": user_key,
"query": "hello",
"scope": ["resources"],
},
)
assert response.status_code == 200, response.text
results = response.json()["results"]
assert [result["id"] for result in results] == ["mem_live"]
@pytest.mark.asyncio
async def test_override_replaces_search_result_text(
config: GatewayConfig,
repo: MemoryRepository,
) -> None:
repo.create_resource(
id="r_1",
user_id="u_123",
app_id="default",
project_id="default",
session_id="resource:u_123:r_1",
original_filename="a.txt",
mime_type="text/plain",
content_type="text",
uri="file:///private/a.txt",
uri_public=False,
sha256="abc",
size_bytes=1,
title=None,
description=None,
status="extracted",
error_message=None,
)
everos = FakeEverOSClient(
[{"id": "mem_1", "session_id": "resource:u_123:r_1", "episode": "old text"}]
)
async with app_client(config, everos) as client:
user_key = await create_user(client)
patch_response = await client.patch(
"/memories/mem_1",
json={
"user_id": "u_123",
"user_key": user_key,
"session_id": "resource:u_123:r_1",
"override_text": "corrected text",
},
)
search_response = await client.post(
"/memories/search",
json={
"user_id": "u_123",
"user_key": user_key,
"query": "hello",
"scope": ["resources"],
},
)
assert patch_response.status_code == 200, patch_response.text
result = search_response.json()["results"][0]
assert result["id"] == "mem_1"
assert result["text"] == "corrected text"
assert result["raw"]["episode"] == "old text"
@pytest.mark.asyncio
async def test_list_resources_returns_only_not_deleted(
config: GatewayConfig,
) -> None:
everos = FakeEverOSClient()
async with app_client(config, everos) as client:
user_key = await create_user(client)
first = await client.post(
"/resources",
data={"user_id": "u_123", "user_key": user_key},
files={"file": ("a.txt", b"a", "text/plain")},
)
await client.post(
"/resources",
data={"user_id": "u_123", "user_key": user_key},
files={"file": ("b.txt", b"b", "text/plain")},
)
await client.delete(
f"/resources/{first.json()['resource_id']}",
params={"user_id": "u_123", "user_key": user_key},
)
response = await client.get(
"/resources",
params={"user_id": "u_123", "user_key": user_key},
)
assert response.status_code == 200
items = response.json()["resources"]
assert len(items) == 1
assert items[0]["filename"] == "b.txt"
assert items[0]["uri"].startswith("resource://u_123/")
@pytest.mark.asyncio
async def test_delete_memory_writes_tombstone(
config: GatewayConfig,
repo: MemoryRepository,
) -> None:
everos = FakeEverOSClient()
async with app_client(config, everos) as client:
user_key = await create_user(client)
response = await client.request(
"DELETE",
"/memories/mem_1",
json={
"user_id": "u_123",
"user_key": user_key,
"session_id": "resource:u_123:r_1",
"reason": "manual delete",
},
)
assert response.status_code == 200, response.text
tombstones = repo.get_tombstones("u_123")
assert len(tombstones) == 1
assert tombstones[0]["memory_id"] == "mem_1"
assert tombstones[0]["session_id"] == "resource:u_123:r_1"

View File

@ -1,417 +0,0 @@
import importlib.util
import json
import os
import sys
from pathlib import Path
HERMES_ROOT = Path("/home/tom/hermes-agent")
PLUGIN_PATH = Path(__file__).resolve().parents[1] / "plugins" / "memory" / "memory_system" / "__init__.py"
def load_plugin_module():
if str(HERMES_ROOT) not in sys.path:
sys.path.insert(0, str(HERMES_ROOT))
spec = importlib.util.spec_from_file_location("memory_system_plugin", PLUGIN_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
class FakeClient:
def __init__(self, commit_status="success"):
self.posts = []
self.gets = []
self.deletes = []
self.commit_status = commit_status
def post(self, path, payload=None):
self.posts.append((path, payload or {}))
if path == "/memory-system/users":
return {
"status": "success",
"account": {
"status": "ok",
"result": {
"account_id": f"{payload['user_id']}_account",
"admin_user_id": payload["user_id"],
"user_key": f"{payload['user_id']}-key",
},
},
}
if path == "/memory-system/search":
return {
"status": "success",
"items": [
{
"source_backend": "openviking",
"content": "likes latte",
"score": 0.9,
"uri": "viking://user/user-1/memories/a",
"vector": [0.1, 0.2],
},
{
"source_backend": "everos",
"memory": "prefers warm coffee",
"memory_type": "profile",
"original_data": {"large": "payload"},
},
],
"backends": {
"openviking": {"status": "success", "result": {"verbose": True}},
"everos": {"status": "success", "result": {"verbose": True}},
},
}
if path.endswith("/commit"):
return {"status": self.commit_status}
if path == "/memory-system/memories":
return {"status": "success", "memory": {"status": "ok", "result": {"uri": payload.get("uri")}}}
return {"status": "success"}
def get(self, path):
self.gets.append(path)
if path.startswith("/memory-system/memories/content"):
return {"status": "success", "memory": {"status": "ok", "result": {"content": "# Python"}}}
if path.startswith("/memory-system/memories"):
return {"status": "success", "memory": {"status": "ok", "result": {"children": []}}}
return {"status": "success", "profile": {"coffee": "latte"}}
def delete(self, path):
self.deletes.append(path)
return {"status": "success", "memory": {"status": "ok", "result": {"estimated_deleted_count": 1}}}
def make_provider():
module = load_plugin_module()
provider = module.MemorySystemMemoryProvider()
provider._client = FakeClient()
provider._endpoint = "http://127.0.0.1:1934"
provider._user_id = "user-1"
provider._user_key = "user-1-key"
provider._session_id = "session-1"
provider._commit_every_turns = 0
provider._commit_interval_seconds = 0
return provider
def wait_for_sync(provider):
thread = provider._sync_thread
if thread and thread.is_alive():
thread.join(timeout=2.0)
def test_initialize_loads_config_from_hermes_env_file(tmp_path, monkeypatch):
module = load_plugin_module()
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
env_file = hermes_home / ".env"
env_file.write_text(
"\n".join(
[
"MEMORY_SYSTEM_ENDPOINT=http://127.0.0.1:1934",
"MEMORY_SYSTEM_USER_ID=file-user",
"MEMORY_SYSTEM_USER_KEY=file-user-key",
"MEMORY_SYSTEM_COMMIT_EVERY_TURNS=3",
"MEMORY_SYSTEM_COMMIT_INTERVAL_SECONDS=60",
"MEMORY_SYSTEM_TIMEOUT_SECONDS=123",
]
),
encoding="utf-8",
)
for key in list(os.environ):
if key.startswith("MEMORY_SYSTEM_"):
monkeypatch.delenv(key, raising=False)
monkeypatch.chdir(tmp_path)
class InitClient(FakeClient):
def __init__(self, endpoint, api_key="", timeout=0):
super().__init__()
self.endpoint = endpoint
self.api_key = api_key
self.timeout = timeout
def health(self):
return True
monkeypatch.setattr(module, "_MemorySystemClient", InitClient)
provider = module.MemorySystemMemoryProvider()
provider.initialize("session-1", hermes_home=str(hermes_home))
assert provider._endpoint == "http://127.0.0.1:1934"
assert provider._user_id == "file-user"
assert provider._user_key == "file-user-key"
assert provider._commit_every_turns == 3
assert provider._commit_interval_seconds == 60
assert provider._timeout == 123
assert provider._client.endpoint == "http://127.0.0.1:1934"
assert provider._client.timeout == 123
def test_sync_turn_posts_user_and_assistant_messages():
provider = make_provider()
provider.sync_turn("hello", "hi there")
wait_for_sync(provider)
assert provider._client.posts == [
(
"/memory-system/messages",
{
"user_id": "user-1",
"user_key": "user-1-key",
"session_id": "session-1",
"user_message": "hello",
"assistant_message": "hi there",
"metadata": {"source": "hermes", "provider": "memory_system"},
},
)
]
def test_on_session_end_commits_after_turn_sync():
provider = make_provider()
provider.sync_turn("hello", "hi there")
provider.on_session_end([])
assert provider._client.posts[-1] == (
"/memory-system/sessions/session-1/commit",
{"user_id": "user-1", "user_key": "user-1-key"},
)
def test_sync_turn_commits_every_configured_turns():
provider = make_provider()
provider._commit_every_turns = 2
provider._commit_interval_seconds = 0
provider.sync_turn("turn 1", "reply 1")
wait_for_sync(provider)
provider.sync_turn("turn 2", "reply 2")
wait_for_sync(provider)
assert provider._client.posts[-1] == (
"/memory-system/sessions/session-1/commit",
{"user_id": "user-1", "user_key": "user-1-key"},
)
assert provider._last_commit_turn == 2
def test_partial_commit_does_not_mark_turns_committed():
provider = make_provider()
provider._client = FakeClient(commit_status="partial_success")
provider._turn_count = 2
response = provider._commit_session("session-1")
assert response["status"] == "partial_success"
assert provider._last_commit_turn == 0
def test_on_session_end_skips_when_periodic_commit_is_current():
provider = make_provider()
provider._commit_every_turns = 1
provider._commit_interval_seconds = 0
provider.sync_turn("hello", "hi there")
wait_for_sync(provider)
provider.on_session_end([])
commit_posts = [
post for post in provider._client.posts if post[0] == "/memory-system/sessions/session-1/commit"
]
assert len(commit_posts) == 1
def test_search_tool_uses_memory_system_api():
provider = make_provider()
result = json.loads(provider.handle_tool_call("memory_system_search", {"query": "coffee", "limit": 3}))
assert result["status"] == "success"
assert result["items"] == [
{
"source_backend": "openviking",
"score": 0.9,
"text": "likes latte",
"uri": "viking://user/user-1/memories/a",
},
{
"source_backend": "everos",
"memory_type": "profile",
"text": "prefers warm coffee",
},
]
assert "backends" not in result
assert "vector" not in json.dumps(result)
assert "original_data" not in json.dumps(result)
assert provider._client.posts[-1] == (
"/memory-system/search",
{
"user_id": "user-1",
"user_key": "user-1-key",
"session_id": "session-1",
"query": "coffee",
"use_llm": False,
"limit": 3,
},
)
def test_profile_tool_reads_user_profile():
provider = make_provider()
result = json.loads(provider.handle_tool_call("memory_system_profile", {}))
assert result["profile"] == {"coffee": "latte"}
assert provider._client.gets == ["/memory-system/users/user-1/profile?user_key=user-1-key"]
def test_remember_tool_writes_and_commits():
provider = make_provider()
result = json.loads(provider.handle_tool_call("memory_system_remember", {"content": "likes latte"}))
assert result["status"] == "success"
assert provider._client.posts == [
(
"/memory-system/messages",
{
"user_id": "user-1",
"user_key": "user-1-key",
"session_id": "session-1",
"user_message": "likes latte",
"metadata": {"source": "hermes", "provider": "memory_system"},
},
),
(
"/memory-system/sessions/session-1/commit",
{"user_id": "user-1", "user_key": "user-1-key"},
),
]
def test_initialize_creates_user_when_user_key_is_not_configured(tmp_path, monkeypatch):
module = load_plugin_module()
for key in list(os.environ):
if key.startswith("MEMORY_SYSTEM_"):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("MEMORY_SYSTEM_ENDPOINT", "http://127.0.0.1:1934")
monkeypatch.setenv("MEMORY_SYSTEM_USER_ID", "new-user")
monkeypatch.chdir(tmp_path)
class InitClient(FakeClient):
def __init__(self, endpoint, api_key="", timeout=0):
super().__init__()
self.endpoint = endpoint
def health(self):
return True
monkeypatch.setattr(module, "_MemorySystemClient", InitClient)
provider = module.MemorySystemMemoryProvider()
provider.initialize("session-1")
assert provider._user_key == "new-user-key"
assert provider._client.posts == [("/memory-system/users", {"user_id": "new-user"})]
def test_memory_tool_schemas_include_memory_management_tools():
provider = make_provider()
tool_names = {schema["name"] for schema in provider.get_tool_schemas()}
assert {
"memory_system_memory_list",
"memory_system_memory_read",
"memory_system_memory_write",
"memory_system_memory_delete",
} <= tool_names
def test_memory_list_tool_calls_memories_endpoint():
provider = make_provider()
result = json.loads(provider.handle_tool_call("memory_system_memory_list", {"recursive": True}))
assert result["status"] == "success"
assert provider._client.gets == [
"/memory-system/memories?user_id=user-1&user_key=user-1-key&uri=viking%3A%2F%2Fuser%2Fmemories&recursive=true"
]
def test_memory_read_tool_calls_memory_content_endpoint():
provider = make_provider()
result = json.loads(provider.handle_tool_call(
"memory_system_memory_read",
{"uri": "viking://user/memories/preferences/python.md"},
))
assert result["status"] == "success"
assert provider._client.gets == [
"/memory-system/memories/content?user_id=user-1&user_key=user-1-key&uri=viking%3A%2F%2Fuser%2Fmemories%2Fpreferences%2Fpython.md"
]
def test_memory_write_tool_posts_memory_payload():
provider = make_provider()
result = json.loads(provider.handle_tool_call(
"memory_system_memory_write",
{
"uri": "viking://user/memories/preferences/python.md",
"content": "# Python",
"mode": "replace",
"wait": True,
},
))
assert result["status"] == "success"
assert provider._client.posts == [
(
"/memory-system/memories",
{
"user_id": "user-1",
"user_key": "user-1-key",
"uri": "viking://user/memories/preferences/python.md",
"content": "# Python",
"mode": "replace",
"wait": True,
},
)
]
def test_memory_delete_tool_deletes_memory_uri_non_recursive_by_default():
provider = make_provider()
result = json.loads(provider.handle_tool_call(
"memory_system_memory_delete",
{"uri": "viking://user/memories/preferences/python.md"},
))
assert result["status"] == "success"
assert provider._client.deletes == [
"/memory-system/memories?user_id=user-1&user_key=user-1-key&uri=viking%3A%2F%2Fuser%2Fmemories%2Fpreferences%2Fpython.md&recursive=false"
]
def test_register_adds_provider():
module = load_plugin_module()
class Ctx:
def __init__(self):
self.providers = []
def register_memory_provider(self, provider):
self.providers.append(provider)
ctx = Ctx()
module.register(ctx)
assert len(ctx.providers) == 1
assert ctx.providers[0].name == "memory_system"

View File

@ -1,848 +0,0 @@
import asyncio
from memory_system_api.clients import EverOSMemorySystemClient, OpenVikingMemorySystemClient
class FakeStore:
def __init__(self):
self.users = {}
self.accounts = {}
self.sessions = []
self.tasks = []
def get_user_key(self, user_id: str) -> str | None:
return self.users.get(user_id)
def save_user_key(self, user_id: str, user_key: str, account_id: str = "admin") -> None:
self.users[user_id] = user_key
def get_account_key(self, account_id: str) -> str | None:
return self.accounts.get(account_id)
def save_account_key(self, account_id: str, admin_user_id: str, account_key: str) -> None:
self.accounts[account_id] = account_key
def account_key_matches(self, account_id: str, account_key: str) -> bool:
return self.accounts.get(account_id) == account_key
def user_key_matches(self, user_id: str, user_key: str) -> bool:
return self.users.get(user_id) == user_key
def save_session(self, user_id: str, session_id: str) -> None:
self.sessions.append((user_id, session_id))
def save_task(self, user_id: str, session_id: str, task_id: str, archive_uri: str | None) -> None:
self.tasks.append((user_id, session_id, task_id, archive_uri))
class FakeResponse:
def __init__(self, status_code: int, data: dict):
self.status_code = status_code
self._data = data
def json(self) -> dict:
return self._data
def raise_for_status(self) -> None:
if self.status_code >= 400:
raise AssertionError(f"unexpected status {self.status_code}")
class FakeAsyncClient:
def __init__(self, calls: list, responses: list[FakeResponse], api_key: str, headers: dict):
self.calls = calls
self.responses = responses
self.api_key = api_key
self.headers = headers
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
async def post(self, path: str, json: dict | None = None, files: dict | None = None) -> FakeResponse:
if files and "file" in files:
uploaded = files["file"]
files = {"file": uploaded[0] if isinstance(uploaded, tuple) else uploaded}
self.calls.append(("post", self.api_key, self.headers, path, json, files))
return self.responses.pop(0)
async def get(self, path: str, params: dict | None = None) -> FakeResponse:
self.calls.append(("get", self.api_key, self.headers, path, params))
return self.responses.pop(0)
async def delete(self, path: str, params: dict | None = None) -> FakeResponse:
self.calls.append(("delete", self.api_key, self.headers, path, params))
return self.responses.pop(0)
def test_openviking_rejects_unknown_user_credentials():
store = FakeStore()
client = OpenVikingMemorySystemClient(store=store)
try:
client.credential_for_user("tom", "missing-key")
except PermissionError as exc:
assert "Invalid user key" in str(exc)
else:
raise AssertionError("expected PermissionError")
def test_openviking_accepts_matching_user_credentials():
store = FakeStore()
store.save_user_key("tom", "tom-key")
client = OpenVikingMemorySystemClient(store=store)
client.root_key = "root-key"
credential = client.credential_for_user("tom", "tom-key", agent_id="sess-1")
assert credential.api_key == "tom-key"
assert credential.account_id == "tom_account"
assert credential.user_id == "tom"
assert credential.agent_id == "sess-1"
def test_openviking_client_uses_x_api_key_for_user_keys():
client = OpenVikingMemorySystemClient(store=FakeStore())
client.root_key = "root-key"
http_client = client._client("tom-key")
try:
assert http_client.headers["X-API-Key"] == "tom-key"
assert "Authorization" not in http_client.headers
finally:
asyncio.run(http_client.aclose())
def test_openviking_create_user_creates_isolated_admin_account():
store = FakeStore()
client = OpenVikingMemorySystemClient(store=store)
client.root_key = "root-key"
calls = []
responses = [
FakeResponse(
200,
{
"status": "ok",
"result": {
"account_id": "userA_account",
"admin_user_id": "userA",
"user_key": "userA-key",
},
},
),
]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
result = asyncio.run(client.create_user("userA"))
assert result == {
"status": "ok",
"result": {
"account_id": "userA_account",
"admin_user_id": "userA",
"user_key": "userA-key",
},
}
assert store.accounts == {"userA_account": "userA-key"}
assert store.users == {"userA": "userA-key"}
assert calls == [
(
"post",
"root-key",
{},
"/api/v1/admin/accounts",
{"account_id": "userA_account", "admin_user_id": "userA"},
None,
),
]
def test_openviking_create_user_creates_account_even_when_admin_workspace_exists():
store = FakeStore()
store.save_account_key("admin", "admin", "admin-key")
client = OpenVikingMemorySystemClient(store=store)
client.root_key = "root-key"
calls = []
responses = [
FakeResponse(
200,
{
"status": "ok",
"result": {
"account_id": "userB_account",
"admin_user_id": "userB",
"user_key": "userB-key",
},
},
)
]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
result = asyncio.run(client.create_user("userB"))
assert result == {
"status": "ok",
"result": {
"account_id": "userB_account",
"admin_user_id": "userB",
"user_key": "userB-key",
},
}
assert store.accounts == {"admin": "admin-key", "userB_account": "userB-key"}
assert store.users == {"userB": "userB-key"}
assert calls == [
(
"post",
"root-key",
{},
"/api/v1/admin/accounts",
{"account_id": "userB_account", "admin_user_id": "userB"},
None,
)
]
def test_openviking_user_key_auth_is_used_for_session_create():
client = OpenVikingMemorySystemClient(store=FakeStore())
client.root_key = "root-key"
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"session_id": "sess-2"}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom", agent_id="sess-2")
result = asyncio.run(client.ensure_session(credential, "sess-2"))
assert result == {"status": "ok", "result": {"session_id": "sess-2"}}
assert client.store.sessions == [("tom", "sess-2")]
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/sessions",
{"session_id": "sess-2"},
None,
)
]
def test_openviking_find_uses_current_identity_memory_scope():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom", agent_id="sess-1")
result = asyncio.run(client.find(credential, "咖啡", 5))
assert result == {"status": "ok", "result": {"memories": []}}
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/search/find",
{"query": "咖啡", "target_uri": "viking://user/tom/memories/", "limit": 5},
None,
)
]
def test_openviking_search_uses_fixed_user_memory_target_with_level_and_score_threshold():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom", agent_id="sess-1")
result = asyncio.run(client.search(credential, "咖啡", 5, level=3, score_threshold=0.7))
assert result == {"status": "ok", "result": {"memories": []}}
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/search/search",
{
"query": "咖啡",
"target_uri": "viking://user/memories",
"limit": 5,
"level": 3,
"score_threshold": 0.7,
},
None,
)
]
def test_openviking_search_accepts_custom_target_uri():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom", agent_id="sess-1")
result = asyncio.run(client.search(
credential,
"咖啡",
5,
level=3,
score_threshold=0.7,
target_uri="viking://user/custom/memories",
))
assert result == {"status": "ok", "result": {"memories": []}}
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/search/search",
{
"query": "咖啡",
"target_uri": "viking://user/custom/memories",
"limit": 5,
"level": 3,
"score_threshold": 0.7,
},
None,
)
]
def test_openviking_profile_search_uses_user_memory_target_and_level():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"memories": []}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(client.search_profile_memories(credential, "我想喝东西", 10, 2))
assert result == {"status": "ok", "result": {"memories": []}}
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/search/search",
{
"query": "我想喝东西",
"limit": 10,
"level": 2,
"target_uri": "viking://user/memories",
},
None,
)
]
def test_openviking_get_session_context_uses_user_key_auth():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [
FakeResponse(
200,
{
"status": "ok",
"result": {
"latest_archive_overview": "# Working Memory",
"messages": [],
},
},
)
]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom", agent_id="sess-1")
result = asyncio.run(client.get_session_context(credential, "sess-1"))
assert result == {
"status": "ok",
"result": {
"latest_archive_overview": "# Working Memory",
"messages": [],
},
}
assert calls == [
(
"get",
"tom-key",
{},
"/api/v1/sessions/sess-1/context",
None,
)
]
def test_openviking_commit_keeps_no_recent_live_messages():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [
FakeResponse(
200,
{
"status": "ok",
"result": {
"status": "accepted",
"task_id": "task-1",
"archive_uri": "viking://session/tom/sess-1/history/archive_001",
},
},
)
]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(client.commit_session(credential, "sess-1"))
assert result == {
"status": "ok",
"result": {
"status": "accepted",
"task_id": "task-1",
"archive_uri": "viking://session/tom/sess-1/history/archive_001",
},
}
assert client.store.tasks == [("tom", "sess-1", "task-1", "viking://session/tom/sess-1/history/archive_001")]
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/sessions/sess-1/commit",
{"keep_recent_count": 0},
None,
)
]
def test_openviking_upload_temp_file_posts_multipart(tmp_path):
path = tmp_path / "report.pdf"
path.write_bytes(b"pdf-bytes")
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [
FakeResponse(
200,
{"status": "ok", "result": {"temp_file_id": "upload_report.pdf"}},
)
]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(client.upload_temp_file(credential, path))
assert result == {"status": "ok", "result": {"temp_file_id": "upload_report.pdf"}}
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/resources/temp_upload",
None,
{"file": "report.pdf"},
)
]
def test_openviking_add_resource_posts_url_payload():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"uri": "viking://resources/tom/images/photo.png"}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(
client.add_resource(
credential,
path="https://example.com/photo.png",
to="viking://resources/tom/images/photo.png",
reason="上传远程图片",
wait=True,
directly_upload_media=True,
)
)
assert result == {"status": "ok", "result": {"uri": "viking://resources/tom/images/photo.png"}}
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/resources",
{
"path": "https://example.com/photo.png",
"to": "viking://resources/tom/images/photo.png",
"reason": "上传远程图片",
"wait": True,
"directly_upload_media": True,
},
None,
)
]
def test_openviking_delete_resource_sends_uri_and_recursive_flag():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"estimated_deleted_count": 4}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(
client.delete_resource(
credential,
uri="viking://resources/tom/files/report.pdf",
recursive=True,
)
)
assert result == {"status": "ok", "result": {"estimated_deleted_count": 4}}
assert calls == [
(
"delete",
"tom-key",
{},
"/api/v1/fs",
{"uri": "viking://resources/tom/files/report.pdf", "recursive": "true"},
)
]
def test_openviking_list_memories_calls_fs_ls_with_recursive_flag():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"children": []}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(
client.list_memories(
credential,
uri="viking://user/memories",
recursive=True,
)
)
assert result == {"status": "ok", "result": {"children": []}}
assert calls == [
(
"get",
"tom-key",
{},
"/api/v1/fs/ls",
{"uri": "viking://user/memories", "recursive": "true"},
)
]
def test_openviking_read_memory_calls_content_read():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"content": "# Python"}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(client.read_memory(credential, "viking://user/memories/preferences/python.md"))
assert result == {"status": "ok", "result": {"content": "# Python"}}
assert calls == [
(
"get",
"tom-key",
{},
"/api/v1/content/read",
{"uri": "viking://user/memories/preferences/python.md"},
)
]
def test_openviking_write_memory_posts_content_write_mode_and_wait():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"uri": "viking://user/memories/profile.md"}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(
client.write_memory(
credential,
uri="viking://user/memories/profile.md",
content="# Profile\n\nLikes Python.",
mode="replace",
wait=True,
)
)
assert result == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md"}}
assert calls == [
(
"post",
"tom-key",
{},
"/api/v1/content/write",
{
"uri": "viking://user/memories/profile.md",
"content": "# Profile\n\nLikes Python.",
"mode": "replace",
"wait": True,
},
None,
)
]
def test_openviking_delete_memory_defaults_non_recursive():
client = OpenVikingMemorySystemClient(store=FakeStore())
calls = []
responses = [FakeResponse(200, {"status": "ok", "result": {"estimated_deleted_count": 1}})]
client._client = lambda api_key, extra_headers=None, json_content_type=True: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
api_key,
extra_headers or {},
)
credential = client.user_credential("tom-key", "tom")
result = asyncio.run(
client.delete_memory(
credential,
uri="viking://user/memories/preferences/python.md",
)
)
assert result == {"status": "ok", "result": {"estimated_deleted_count": 1}}
assert calls == [
(
"delete",
"tom-key",
{},
"/api/v1/fs",
{"uri": "viking://user/memories/preferences/python.md", "recursive": "false"},
)
]
def test_everos_assistant_payload_does_not_use_user_id_as_sender():
client = EverOSMemorySystemClient()
payload = client.build_message_payload(
user_id="tom",
session_id="sess-1",
role="assistant",
content="我记住了",
)
message = payload["messages"][0]
assert message["role"] == "assistant"
assert message["sender_id"] != "tom"
assert message["sender_name"] != "tom"
def test_everos_user_payload_uses_user_id_as_sender():
client = EverOSMemorySystemClient()
payload = client.build_message_payload(
user_id="tom",
session_id="sess-1",
role="user",
content="我喜欢拿铁",
)
message = payload["messages"][0]
assert message["role"] == "user"
assert message["sender_id"] == "tom"
assert message["sender_name"] == "tom"
def test_everos_append_message_posts_current_memory_add_contract():
client = EverOSMemorySystemClient()
calls = []
responses = [FakeResponse(200, {"request_id": "req-1", "data": {"status": "accumulated", "message_count": 1}})]
client._client = lambda: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
client.api_key or "",
{},
)
result = asyncio.run(client.append_message("tom", "sess-1", "user", "我喜欢拿铁"))
assert result == {"request_id": "req-1", "data": {"status": "accumulated", "message_count": 1}}
assert calls[0][0] == "post"
assert calls[0][3] == "/api/v1/memory/add"
payload = calls[0][4]
assert payload["session_id"] == "sess-1"
assert "user_id" not in payload
assert payload["messages"][0]["sender_id"] == "tom"
assert payload["messages"][0]["role"] == "user"
assert payload["messages"][0]["content"] == "我喜欢拿铁"
def test_everos_flush_posts_current_memory_flush_contract():
client = EverOSMemorySystemClient()
calls = []
responses = [FakeResponse(200, {"request_id": "req-1", "data": {"status": "extracted"}})]
client._client = lambda: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
client.api_key or "",
{},
)
result = asyncio.run(client.flush("tom", "sess-1"))
assert result == {"request_id": "req-1", "data": {"status": "extracted"}}
assert calls == [
(
"post",
client.api_key or "",
{},
"/api/v1/memory/flush",
{"session_id": "sess-1"},
None,
)
]
def test_everos_search_posts_current_memory_search_contract():
client = EverOSMemorySystemClient()
calls = []
responses = [FakeResponse(200, {"request_id": "req-1", "data": {"episodes": []}})]
client._client = lambda: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
client.api_key or "",
{},
)
result = asyncio.run(client.search("tom", "sess-1", "牛奶在哪里", "hybrid", 7))
assert result == {"request_id": "req-1", "data": {"episodes": []}}
assert calls == [
(
"post",
client.api_key or "",
{},
"/api/v1/memory/search",
{
"user_id": "tom",
"query": "牛奶在哪里",
"method": "hybrid",
"top_k": 7,
"include_profile": True,
"filters": {"session_id": "sess-1"},
},
None,
)
]
def test_everos_get_profile_posts_current_memory_get_contract():
client = EverOSMemorySystemClient()
calls = []
responses = [FakeResponse(200, {"request_id": "req-1", "data": {"profiles": []}})]
client._client = lambda: FakeAsyncClient( # type: ignore[method-assign]
calls,
responses,
client.api_key or "",
{},
)
result = asyncio.run(client.get_profile("tom"))
assert result == {"request_id": "req-1", "data": {"profiles": []}}
assert calls == [
(
"post",
client.api_key or "",
{},
"/api/v1/memory/get",
{
"user_id": "tom",
"memory_type": "profile",
"page": 1,
"page_size": 20,
},
None,
)
]

View File

@ -1,93 +0,0 @@
import logging
from fastapi.testclient import TestClient
def test_memory_system_server_exposes_routes():
from memory_system_api.server import app
paths = {route.path for route in app.routes}
assert "/memory-system/users" in paths
assert "/memory-system/messages" in paths
assert "/memory-system/sessions/{session_id}/context" in paths
context_methods = {
method
for route in app.routes
if getattr(route, "path", "") == "/memory-system/sessions/{session_id}/context"
for method in getattr(route, "methods", set())
}
assert {"GET", "POST"} <= context_methods
assert "/memory-system/search" in paths
assert "/memory-system/resources" in paths
assert "/memory-system/memories" in paths
assert "/memory-system/memories/content" in paths
assert "/memory-system/users/{user_id}/profile" in paths
task_methods = {
method
for route in app.routes
if getattr(route, "path", "") == "/memory-system/openviking/tasks/{task_id}"
for method in getattr(route, "methods", set())
}
profile_methods = {
method
for route in app.routes
if getattr(route, "path", "") == "/memory-system/users/{user_id}/profile"
for method in getattr(route, "methods", set())
}
assert {"GET", "POST"} <= task_methods
assert {"GET", "POST"} <= profile_methods
resource_methods = {
method
for route in app.routes
if getattr(route, "path", "") == "/memory-system/resources"
for method in getattr(route, "methods", set())
}
assert {"DELETE", "POST"} <= resource_methods
memory_methods = {
method
for route in app.routes
if getattr(route, "path", "") == "/memory-system/memories"
for method in getattr(route, "methods", set())
}
memory_content_methods = {
method
for route in app.routes
if getattr(route, "path", "") == "/memory-system/memories/content"
for method in getattr(route, "methods", set())
}
assert {"DELETE", "GET", "POST"} <= memory_methods
assert {"GET"} <= memory_content_methods
def test_memory_system_messages_does_not_require_account_key_header():
from memory_system_api.server import app
route = next(route for route in app.routes if getattr(route, "path", "") == "/memory-system/messages")
assert all(getattr(dependency.call, "__name__", "") != "account_key_header" for dependency in route.dependant.dependencies)
def test_memory_system_logs_request_and_response_bodies(caplog):
from memory_system_api.api import get_service
from memory_system_api.server import app
class FakeService:
async def create_user(self, user_id: str):
return {"status": "success", "account": {"user_id": user_id}}
app.dependency_overrides[get_service] = lambda: FakeService()
try:
with caplog.at_level(logging.INFO, logger="memory_system_api.requests"):
response = TestClient(app).post("/memory-system/users", json={"user_id": "userA"})
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {"status": "success", "account": {"user_id": "userA"}}
assert any("request POST /memory-system/users body={\"user_id\":\"userA\"}" in record.message for record in caplog.records)
assert any(
"response POST /memory-system/users status=200 body={\"status\":\"success\",\"account\":{\"user_id\":\"userA\"}}"
in record.message
for record in caplog.records
)

View File

@ -1,725 +0,0 @@
import asyncio
from memory_system_api.schemas import (
MemoryWriteRequest,
MessageIngestRequest,
ResourceUploadRequest,
SearchRequest,
SessionContextRequest,
)
from memory_system_api.service import MemorySystemService
class FakeOpenViking:
def __init__(self, fail_on_append: bool = False):
self.fail_on_append = fail_on_append
self.calls = []
async def create_user(self, user_id: str) -> dict:
self.calls.append(("create_user", user_id))
return {"account_id": f"{user_id}_account", "admin_user_id": user_id, "user_key": f"{user_id}-key"}
def credential_for_user(
self,
user_id: str,
user_key: str,
agent_id: str | None = None,
) -> str:
self.calls.append(("credential_for_user", user_id, user_key, agent_id))
if user_key != f"{user_id}-key":
raise PermissionError("Invalid user key")
return f"key-{user_id}"
async def ensure_session(self, user_key: str, session_id: str) -> dict:
self.calls.append(("ensure_session", user_key, session_id))
return {"session_id": session_id}
async def append_message(self, user_key: str, session_id: str, role: str, content: str) -> dict:
self.calls.append(("append_message", user_key, session_id, role, content))
if self.fail_on_append:
raise RuntimeError("openviking append failed")
return {"message_count": len([call for call in self.calls if call[0] == "append_message"])}
async def find(self, user_key: str, query: str, limit: int) -> dict:
self.calls.append(("find", user_key, query, limit))
await asyncio.sleep(0.01)
return {"items": [{"source": "openviking-find"}]}
async def search(
self,
user_key: str,
query: str,
limit: int,
level: int = 2,
score_threshold: float = 0.8,
target_uri: str = "viking://user/memories",
) -> dict:
self.calls.append(("search", user_key, query, limit, level, score_threshold, target_uri))
await asyncio.sleep(0.01)
return {"items": [{"source": "openviking-search"}]}
async def search_profile_memories(self, user_key: str, query: str, limit: int, level: int) -> dict:
self.calls.append(("search_profile_memories", user_key, query, limit, level))
return {
"status": "ok",
"result": {
"memories": [
{
"context_type": "memory",
"uri": "viking://user/tom/memories/preferences/coffee.md",
"level": 2,
"score": 0.91,
"abstract": "用户喜欢喝咖啡。",
}
],
"resources": [],
"skills": [],
"total": 1,
},
}
async def get_session_context(self, user_key: str, session_id: str) -> dict:
self.calls.append(("get_session_context", user_key, session_id))
return {
"status": "ok",
"result": {
"latest_archive_overview": "# Working Memory\nUser likes coffee.",
"pre_archive_abstracts": [],
"messages": [],
"estimatedTokens": 42,
"stats": {"totalArchives": 1},
},
}
async def commit_session(self, user_key: str, session_id: str) -> dict:
self.calls.append(("commit_session", user_key, session_id))
return {"status": "ok", "result": {"task_id": "task-1", "archive_uri": "archive-1"}}
async def upload_temp_file(self, user_key: str, path) -> dict:
self.calls.append(("upload_temp_file", user_key, str(path)))
return {"status": "ok", "result": {"temp_file_id": "upload_report.pdf"}}
async def add_resource(
self,
user_key: str,
*,
to: str,
reason: str | None,
wait: bool,
directly_upload_media: bool,
path: str | None = None,
temp_file_id: str | None = None,
) -> dict:
self.calls.append((
"add_resource",
user_key,
path,
temp_file_id,
to,
reason,
wait,
directly_upload_media,
))
return {"status": "ok", "result": {"uri": to}}
async def delete_resource(self, user_key: str, uri: str, recursive: bool = True) -> dict:
self.calls.append(("delete_resource", user_key, uri, recursive))
return {"status": "ok", "result": {"uri": uri, "estimated_deleted_count": 4}}
async def list_memories(self, user_key: str, uri: str, recursive: bool = True) -> dict:
self.calls.append(("list_memories", user_key, uri, recursive))
return {"status": "ok", "result": {"children": [{"uri": "viking://user/memories/profile.md"}]}}
async def read_memory(self, user_key: str, uri: str) -> dict:
self.calls.append(("read_memory", user_key, uri))
return {"status": "ok", "result": {"uri": uri, "content": "# Profile"}}
async def write_memory(self, user_key: str, uri: str, content: str, mode: str, wait: bool = True) -> dict:
self.calls.append(("write_memory", user_key, uri, content, mode, wait))
return {"status": "ok", "result": {"uri": uri, "mode": mode}}
async def delete_memory(self, user_key: str, uri: str, recursive: bool = False) -> dict:
self.calls.append(("delete_memory", user_key, uri, recursive))
return {"status": "ok", "result": {"uri": uri, "estimated_deleted_count": 1}}
class FakeEverOS:
def __init__(self, fail_on_append: bool = False):
self.fail_on_append = fail_on_append
self.calls = []
async def append_message(self, user_id: str, session_id: str, role: str, content: str) -> dict:
self.calls.append(("append_message", user_id, session_id, role, content))
if self.fail_on_append:
raise RuntimeError("everos append failed")
return {"status": "accumulated"}
async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
self.calls.append(("search", user_id, session_id, query, method, limit))
await asyncio.sleep(0.01)
return {"items": [{"source": f"everos-{method}"}]}
async def flush(self, user_id: str, session_id: str) -> dict:
self.calls.append(("flush", user_id, session_id))
return {"status": "flushed"}
async def get_profile(self, user_id: str) -> dict:
self.calls.append(("get_profile", user_id))
return {
"data": {
"episodes": [],
"profiles": [
{
"id": "profile-1",
"user_id": user_id,
"profile_data": {"summary": "喜欢咖啡"},
}
],
"agent_cases": [],
"agent_skills": [],
"total_count": 1,
"count": 1,
}
}
class FakeEverOSWithVector(FakeEverOS):
async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
self.calls.append(("search", user_id, session_id, query, method, limit))
return {
"data": {
"episodes": [{"id": "episode-1", "vector": [0.1, 0.2]}],
"original_data": {
"episodes": {
"episode-1": {
"summary": "喜欢拿铁",
"vector": [0.1, 0.2],
"nested": {"vector": [0.3]},
}
}
},
}
}
class FakeEverOSVerbose(FakeEverOS):
async def search(self, user_id: str, session_id: str | None, query: str, method: str, limit: int) -> dict:
self.calls.append(("search", user_id, session_id, query, method, limit))
return {
"data": {
"episodes": [
{
"id": "episode-1",
"user_id": user_id,
"session_id": session_id,
"timestamp": "2026-05-22T07:50:51.750000Z",
"summary": "userB 在对话中表示自己喜欢拿铁。",
"subject": "UserB 表达对拿铁的喜好",
"episode": "userB 在对话中表示自己喜欢拿铁。",
"type": "Conversation",
"parent_id": "parent-1",
"score": 0.72,
"atomic_facts": [],
}
],
"profiles": [],
"raw_messages": [],
"query": {
"text": query,
"method": method,
"filters_applied": {"user_id": user_id, "session_id": session_id},
},
"original_data": {
"episodes": {
"episode-1": {
"id": "episode-1",
"summary": "userB 在对话中表示自己喜欢拿铁。",
"episode": "userB 在对话中表示自己喜欢拿铁。",
"vector_model": "Qwen3-VL-Embedding-2B",
}
}
},
}
}
def test_capture_includes_exception_type_when_message_is_empty():
service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOS())
class EmptyError(Exception):
pass
async def fail():
raise EmptyError()
response = asyncio.run(service._capture(fail))
assert response.status == "failed"
assert response.error == "EmptyError"
def test_create_user_delegates_to_openviking_only():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.create_user("alice"))
assert response.status == "success"
assert response.account == {"account_id": "alice_account", "admin_user_id": "alice", "user_key": "alice-key"}
assert openviking.calls == [("create_user", "alice")]
assert everos.calls == []
def test_upload_resource_with_url_delegates_directly_to_openviking_add_resource():
openviking = FakeOpenViking()
service = MemorySystemService(openviking=openviking, everos=FakeEverOS())
response = asyncio.run(service.upload_resource(ResourceUploadRequest(
user_id="tom",
user_key="tom-key",
path="https://example.com/images/photo.png",
to="viking://resources/tom/images/photo.png",
reason="上传远程图片",
)))
assert response.status == "success"
assert response.resource == {"status": "ok", "result": {"uri": "viking://resources/tom/images/photo.png"}}
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", None),
(
"add_resource",
"key-tom",
"https://example.com/images/photo.png",
None,
"viking://resources/tom/images/photo.png",
"上传远程图片",
True,
True,
),
]
def test_upload_resource_with_local_path_uploads_temp_file_first(tmp_path):
path = tmp_path / "report.pdf"
path.write_bytes(b"pdf-bytes")
openviking = FakeOpenViking()
service = MemorySystemService(openviking=openviking, everos=FakeEverOS())
response = asyncio.run(service.upload_resource(ResourceUploadRequest(
user_id="tom",
user_key="tom-key",
path=str(path),
to="viking://resources/tom/files/report.pdf",
reason="上传本地文件",
)))
assert response.status == "success"
assert response.resource == {"status": "ok", "result": {"uri": "viking://resources/tom/files/report.pdf"}}
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", None),
("upload_temp_file", "key-tom", str(path)),
(
"add_resource",
"key-tom",
None,
"upload_report.pdf",
"viking://resources/tom/files/report.pdf",
"上传本地文件",
True,
True,
),
]
def test_delete_resource_delegates_to_openviking_only():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.delete_resource(
user_id="tom",
user_key="tom-key",
uri="viking://resources/tom/files/report.pdf",
recursive=True,
))
assert response.status == "success"
assert response.resource == {
"status": "ok",
"result": {"uri": "viking://resources/tom/files/report.pdf", "estimated_deleted_count": 4},
}
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", None),
("delete_resource", "key-tom", "viking://resources/tom/files/report.pdf", True),
]
assert everos.calls == []
def test_list_memories_delegates_to_openviking_only():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.list_memories(
user_id="tom",
user_key="tom-key",
uri="viking://user/memories",
recursive=True,
))
assert response.status == "success"
assert response.memory == {"status": "ok", "result": {"children": [{"uri": "viking://user/memories/profile.md"}]}}
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", None),
("list_memories", "key-tom", "viking://user/memories", True),
]
assert everos.calls == []
def test_read_memory_delegates_to_openviking_only():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.read_memory(
user_id="tom",
user_key="tom-key",
uri="viking://user/memories/profile.md",
))
assert response.status == "success"
assert response.memory == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md", "content": "# Profile"}}
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", None),
("read_memory", "key-tom", "viking://user/memories/profile.md"),
]
assert everos.calls == []
def test_write_memory_delegates_to_openviking_content_write_only():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.write_memory(MemoryWriteRequest(
user_id="tom",
user_key="tom-key",
uri="viking://user/memories/profile.md",
content="# Profile\n\nLikes Python.",
mode="replace",
wait=True,
)))
assert response.status == "success"
assert response.memory == {"status": "ok", "result": {"uri": "viking://user/memories/profile.md", "mode": "replace"}}
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", None),
("write_memory", "key-tom", "viking://user/memories/profile.md", "# Profile\n\nLikes Python.", "replace", True),
]
assert everos.calls == []
def test_write_memory_supports_openviking_create_mode():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.write_memory(MemoryWriteRequest(
user_id="tom",
user_key="tom-key",
uri="viking://user/tom/memories/preferences/饮食偏好.md",
content="用户喜欢喝茶",
mode="create",
wait=True,
)))
assert response.status == "success"
assert response.memory == {"status": "ok", "result": {"uri": "viking://user/tom/memories/preferences/饮食偏好.md", "mode": "create"}}
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", None),
("write_memory", "key-tom", "viking://user/tom/memories/preferences/饮食偏好.md", "用户喜欢喝茶", "create", True),
]
assert everos.calls == []
def test_delete_memory_delegates_to_openviking_only_and_defaults_non_recursive():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.delete_memory(
user_id="tom",
user_key="tom-key",
uri="viking://user/memories/preferences/python.md",
))
assert response.status == "success"
assert response.memory == {
"status": "ok",
"result": {"uri": "viking://user/memories/preferences/python.md", "estimated_deleted_count": 1},
}
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", None),
("delete_memory", "key-tom", "viking://user/memories/preferences/python.md", False),
]
assert everos.calls == []
def test_search_removes_vectors_from_items_and_backend_results():
service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOSWithVector())
response = asyncio.run(service.search(
SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="咖啡偏好", use_llm=False, limit=5),
))
assert response.items == [
{"source_backend": "openviking", "source": "openviking-search"},
{"source_backend": "everos", "memory_type": "episode", "id": "episode-1"},
]
assert not _has_key(response.backends["everos"].result, "vector")
def test_search_returns_compact_items_and_backend_diagnostics_without_duplicate_raw_payloads():
service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOSVerbose())
response = asyncio.run(service.search(
SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="我喜欢喝什么?", use_llm=True),
))
assert response.items == [
{"source_backend": "openviking", "source": "openviking-search"},
{
"source_backend": "everos",
"memory_type": "episode",
"id": "episode-1",
"user_id": "tom",
"session_id": "sess-1",
"timestamp": "2026-05-22T07:50:51.750000Z",
"summary": "userB 在对话中表示自己喜欢拿铁。",
"score": 0.72,
},
]
assert response.backends["everos"].result == {
"counts": {"episodes": 1, "profiles": 0, "raw_messages": 0},
"query": {
"text": "我喜欢喝什么?",
"method": "agentic",
"filters_applied": {"user_id": "tom", "session_id": "sess-1"},
},
}
assert not _has_key(response.backends["everos"].result, "original_data")
def test_session_context_combines_openviking_context_and_everos_search_items():
openviking = FakeOpenViking()
everos = FakeEverOSVerbose()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(
service.get_session_context(
"sess-1",
SessionContextRequest(user_id="tom", user_key="tom-key", query="我喜欢喝什么?", limit=5),
)
)
assert response.status == "success"
assert response.context == {
"latest_archive_overview": "# Working Memory\nUser likes coffee.",
"pre_archive_abstracts": [],
"messages": [],
"estimatedTokens": 42,
"stats": {"totalArchives": 1},
}
assert response.items == [
{
"source_backend": "everos",
"memory_type": "episode",
"id": "episode-1",
"user_id": "tom",
"session_id": "sess-1",
"timestamp": "2026-05-22T07:50:51.750000Z",
"summary": "userB 在对话中表示自己喜欢拿铁。",
"score": 0.72,
}
]
assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
assert ("get_session_context", "key-tom", "sess-1") in openviking.calls
assert ("search", "tom", "sess-1", "我喜欢喝什么?", "hybrid", 5) in everos.calls
def test_profile_combines_everos_profile_and_openviking_memory_search():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.get_profile("tom", "tom-key", query="我想喝东西", limit=10, level=2))
assert response.status == "success"
assert response.profile == {
"data": {
"episodes": [],
"profiles": [
{
"id": "profile-1",
"user_id": "tom",
"profile_data": {"summary": "喜欢咖啡"},
}
],
"agent_cases": [],
"agent_skills": [],
"total_count": 1,
"count": 1,
}
}
assert response.items == [
{
"source_backend": "openviking",
"context_type": "memory",
"uri": "viking://user/tom/memories/preferences/coffee.md",
"level": 2,
"score": 0.91,
"abstract": "用户喜欢喝咖啡。",
}
]
assert response.backends["everos"].result == {
"total_count": 1,
"count": 1,
"counts": {
"episodes": 0,
"profiles": 1,
"agent_cases": 0,
"agent_skills": 0,
},
}
assert "profiles" not in response.backends["everos"].result
assert ("credential_for_user", "tom", "tom-key", None) in openviking.calls
assert ("search_profile_memories", "key-tom", "我想喝东西", 10, 2) in openviking.calls
assert everos.calls == [("get_profile", "tom")]
def _has_key(value, key: str) -> bool:
if isinstance(value, dict):
return key in value or any(_has_key(item, key) for item in value.values())
if isinstance(value, list):
return any(_has_key(item, key) for item in value)
return False
def test_ingest_splits_user_and_assistant_messages():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.ingest_messages(
MessageIngestRequest(
user_id="tom",
user_key="tom-key",
session_id="sess-1",
user_message="我喜欢拿铁",
assistant_message="我记住了",
)
))
assert response.status == "success"
assert response.message_count == 2
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", "sess-1"),
("ensure_session", "key-tom", "sess-1"),
("append_message", "key-tom", "sess-1", "user", "我喜欢拿铁"),
("append_message", "key-tom", "sess-1", "assistant", "我记住了"),
]
assert everos.calls == [
("append_message", "tom", "sess-1", "user", "我喜欢拿铁"),
("append_message", "tom", "sess-1", "assistant", "我记住了"),
]
def test_ingest_requires_at_least_one_message():
service = MemorySystemService(openviking=FakeOpenViking(), everos=FakeEverOS())
try:
asyncio.run(
service.ingest_messages(
MessageIngestRequest(user_id="tom", user_key="tom-key", session_id="sess-1"),
)
)
except ValueError as exc:
assert "at least one message" in str(exc)
else:
raise AssertionError("expected ValueError")
def test_ingest_returns_partial_success_when_one_backend_fails():
service = MemorySystemService(openviking=FakeOpenViking(fail_on_append=True), everos=FakeEverOS())
response = asyncio.run(service.ingest_messages(
MessageIngestRequest(user_id="tom", user_key="tom-key", session_id="sess-1", user_message="hello"),
))
assert response.status == "partial_success"
assert response.backends["openviking"].status == "failed"
assert response.backends["everos"].status == "success"
def test_commit_uses_user_key_without_account_id():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.commit_session("tom", "tom-key", "sess-1"))
assert response.status == "success"
assert openviking.calls == [
("credential_for_user", "tom", "tom-key", "sess-1"),
("commit_session", "key-tom", "sess-1"),
]
assert everos.calls == [("flush", "tom", "sess-1")]
def test_search_uses_openviking_search_and_hybrid_without_llm():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.search(
SearchRequest(
user_id="tom",
user_key="tom-key",
session_id="sess-1",
query="咖啡偏好",
use_llm=False,
limit=5,
level=3,
score_threshold=0.7,
target_uri="viking://user/custom/memories",
),
))
assert response.status == "success"
assert response.items == [
{"source_backend": "openviking", "source": "openviking-search"},
{"source_backend": "everos", "source": "everos-hybrid"},
]
assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
assert ("search", "key-tom", "咖啡偏好", 5, 3, 0.7, "viking://user/custom/memories") in openviking.calls
assert ("search", "tom", "sess-1", "咖啡偏好", "hybrid", 5) in everos.calls
def test_search_uses_search_and_agentic_with_llm():
openviking = FakeOpenViking()
everos = FakeEverOS()
service = MemorySystemService(openviking=openviking, everos=everos)
response = asyncio.run(service.search(
SearchRequest(user_id="tom", user_key="tom-key", session_id="sess-1", query="咖啡偏好", use_llm=True, limit=5),
))
assert response.status == "success"
assert response.items == [
{"source_backend": "openviking", "source": "openviking-search"},
{"source_backend": "everos", "source": "everos-agentic"},
]
assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
assert ("search", "key-tom", "咖啡偏好", 5, 2, 0.8, "viking://user/memories") in openviking.calls
assert ("search", "tom", "sess-1", "咖啡偏好", "agentic", 5) in everos.calls

View File

@ -1,32 +0,0 @@
from memory_system_api.store import OpenVikingUserKeyStore
def test_store_persists_openviking_user_session_and_task_metadata(tmp_path):
db_path = tmp_path / "memory.sqlite3"
store = OpenVikingUserKeyStore(str(db_path))
store.save_user_key("userA", "userA-key")
store.save_session("userA", "sessionA1")
store.save_task(
user_id="userA",
session_id="sessionA1",
task_id="task-1",
archive_uri="viking://session/userA/sessionA1/history/archive_001",
)
reopened = OpenVikingUserKeyStore(str(db_path))
assert reopened.get_user_key("userA") == "userA-key"
assert reopened.user_key_matches("userA", "userA-key")
assert reopened.get_session("userA", "sessionA1") == {
"user_id": "userA",
"session_id": "sessionA1",
"latest_task_id": "task-1",
"latest_archive_uri": "viking://session/userA/sessionA1/history/archive_001",
}
assert reopened.get_task("task-1") == {
"task_id": "task-1",
"user_id": "userA",
"session_id": "sessionA1",
"archive_uri": "viking://session/userA/sessionA1/history/archive_001",
}

Binary file not shown.