feat: qualify native Hermes research and private conversation exports

This commit is contained in:
snowspeeder committed 2026-10-03 08:56:18 -04:00
1 parent fb3178d34b
commit e048793b02
5 files changed
+206 -6

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
"""Offline owned exports; no Hermes/LiveKit/deployment calls."""
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from control.accounts import Accounts
from control.app import Settings
from control.chat_exports import create_export_router, export_conversation
from control.chat_store import ChatStore
@pytest.fixture
def env(tmp_path):
accounts = Accounts(str(tmp_path / 'accounts.db'))
users = [accounts.create(name, name, 'user', 'correct-horse-password') for name in ('alice', 'bob')]
tokens = [accounts.issue_session(u['id'], 3600, u['password_hash'])[0] for u in users]
store = ChatStore(accounts)
conversation = store.create_conversation(users[0]['id'], title='Private')
store.add_message(users[0]['id'], conversation['id'], 'user', 'Original question')
store.add_message(users[0]['id'], conversation['id'], 'assistant', '<script>untrusted</script> [Source](https://example.test)')
app = FastAPI()
app.state.accounts = accounts
app.state.settings = Settings()
app.state.chat_store = store
app.include_router(create_export_router())
with TestClient(app) as client:
yield accounts, store, client, users, tokens, conversation
accounts.close()
def test_full_projection_and_safe_markdown(env):
accounts, store, client, users, tokens, conversation = env
headers = {'Authorization': 'Bearer ' + tokens[0]}
url = '/api/conversations/' + conversation['id'] + '/export'
response = client.get(url, headers=headers)
assert response.status_code == 200
document = response.json()
assert len(document['messages']) == 2
assert document['messages'][0]['content'] == 'Original question'
assert 'hermes_session_id' not in response.text
assert response.headers['cache-control'] == 'no-store'
assert response.headers['content-disposition'].startswith('attachment;')
markdown = client.get(url + '?format=markdown', headers=headers)
assert markdown.status_code == 200
assert markdown.headers['content-type'].startswith('text/plain')
assert '[Source](https://example.test)' in markdown.text
assert '<script>untrusted</script>' in markdown.text
assert client.get(url + '?format=html', headers=headers).status_code == 422
def test_owned_revoked_deleted_and_bounded(env):
accounts, store, client, users, tokens, conversation = env
url = '/api/conversations/' + conversation['id'] + '/export'
assert client.get(url).status_code == 401
assert client.get(url, headers={'Authorization': 'Bearer ' + tokens[1]}).status_code == 404
with pytest.raises(HTTPException) as error:
export_conversation(store, users[0]['id'], conversation['id'], byte_limit=4)
assert error.value.status_code == 413
accounts.revoke(tokens[0])
assert client.get(url, headers={'Authorization': 'Bearer ' + tokens[0]}).status_code == 401
store.delete_conversation(users[0]['id'], conversation['id'])
with pytest.raises(HTTPException) as error:
export_conversation(store, users[0]['id'], conversation['id'])
assert error.value.status_code == 404
+28
View File
@@ -310,3 +310,31 @@ def test_pdf_subprocess_limits_and_admission(env, monkeypatch):
finally:
env.store.upload_admission.release()
env.store.upload_admission.release()
def test_research_exact_tool_manifest_and_status_projection(env):
import httpx
manifest = {'object': 'list', 'platform': 'api_server', 'data': [
{'name': 'web', 'enabled': True, 'configured': True, 'tools': ['web_search', 'web_extract'], 'secret': 'never expose'}]}
requests = []
def handle(request):
requests.append(request)
assert request.url.path == '/v1/toolsets'
return httpx.Response(200, json=manifest)
http = httpx.AsyncClient(transport=httpx.MockTransport(handle))
env.accounts.update(env.users[0]['id'], hermes_url='http://127.0.0.1:20000', hermes_api_key='private')
provider = FeatureProvider(env.accounts, http_client=http)
assert asyncio.run(provider.research_status(env.users[0]['id'])) == {'available': True, 'mode': 'hermes_tools', 'reason': None}
instruction = asyncio.run(provider.research_instruction(env.users[0]['id']))
assert 'same Hermes run' in instruction and 'cite source URLs' in instruction
assert requests[-1].headers['authorization'] == 'Bearer private'
assert 'secret' not in str(asyncio.run(provider.toolset_status(env.users[0]['id'])))
manifest['data'][0]['configured'] = False
assert not asyncio.run(provider.research_status(env.users[0]['id']))['available']
with pytest.raises(HTTPException) as error:
asyncio.run(provider.research_instruction(env.users[0]['id']))
assert error.value.status_code == 409
manifest['data'][0]['configured'] = True
manifest['data'][0]['tools'] = ['web_search']
assert not asyncio.run(provider.research_status(env.users[0]['id']))['available']
asyncio.run(http.aclose())