122 lines
5.7 KiB
Python
122 lines
5.7 KiB
Python
import asyncio
|
|
import pytest
|
|
from app.contracts import ModelRequest, Message, ProviderConfig
|
|
from app.errors import ApiError
|
|
from app.services.persona_settings import PersonaSettings, DialoguePair, save_persona, load_persona, apply_global_persona
|
|
|
|
|
|
def request():
|
|
return ModelRequest(provider_id="p", model="test", system="任务要求", messages=[Message(role="user", content="hello")])
|
|
|
|
|
|
def test_global_persona_persists_and_keeps_task_prompt():
|
|
save_persona(PersonaSettings(name="老师", system_prompt="耐心解释", dialogue_pairs=[DialoguePair(user="问题", assistant="回答"), DialoguePair()]))
|
|
assert load_persona().version == 1
|
|
original = request()
|
|
assembled = apply_global_persona(original)
|
|
assert assembled.system == "任务要求\n\n全局人设 / Global persona\n耐心解释\n\n预设对话示例 / Example dialogue\nUser: 问题\nAssistant: 回答"
|
|
assert original.system == "任务要求"
|
|
with pytest.raises(ApiError):
|
|
save_persona(PersonaSettings())
|
|
|
|
|
|
def test_empty_persona_omits_all_global_sections():
|
|
save_persona(PersonaSettings(system_prompt=" ", dialogue_pairs=[DialoguePair(user=" ")]))
|
|
assert apply_global_persona(request()).system == "任务要求"
|
|
|
|
|
|
def test_existing_provider_reads_latest_global_persona_for_complete_and_stream(monkeypatch):
|
|
from app.providers.factory import ProviderFactory
|
|
from app.providers.base import ProviderTurn
|
|
seen = []
|
|
class Adapter:
|
|
async def complete(self, req):
|
|
seen.append(req.system)
|
|
return ProviderTurn(text="ok")
|
|
async def stream(self, req):
|
|
seen.append(req.system)
|
|
if False: yield
|
|
factory = ProviderFactory(None)
|
|
monkeypatch.setattr(factory, "_build", lambda _: Adapter())
|
|
adapter = factory.build(ProviderConfig(provider_id="p",name="test",provider_type="openai_compatible"))
|
|
save_persona(PersonaSettings(system_prompt="全局人设"))
|
|
async def run():
|
|
await adapter.complete(request())
|
|
async for _ in adapter.stream(request()): pass
|
|
asyncio.run(run())
|
|
assert len(seen) == 2
|
|
assert all(text.count("全局人设 / Global persona") == 1 for text in seen)
|
|
|
|
|
|
def test_desktop_persona_uses_bound_host_cas_and_retains_legacy(monkeypatch):
|
|
from app.services import persona_settings, desktop_notes
|
|
from app import host_bridge
|
|
save_persona(PersonaSettings(system_prompt="legacy global"))
|
|
monkeypatch.setattr(persona_settings, '_desktop', lambda: True)
|
|
calls = []
|
|
document = {'record': {'data': {'version': 7, 'name': 'Vault persona',
|
|
'system_prompt': 'Scoped prompt', 'dialogue_pairs': []}}, 'hash': 'a' * 64}
|
|
def call(method, **params):
|
|
calls.append((method, params))
|
|
if method == 'persona.get':
|
|
return document
|
|
if params['expected'] != document['hash']:
|
|
raise ApiError(409, 'REVISION_CONFLICT', 'controlled stale hash')
|
|
return {'record': params['record'], 'hash': 'b' * 64}
|
|
monkeypatch.setattr(desktop_notes, 'call', call)
|
|
loaded = load_persona()
|
|
assert loaded.revision == 'a' * 64
|
|
assert apply_global_persona(request()).system.endswith('Scoped prompt')
|
|
token = host_bridge.operation_id.set('controlled-operation')
|
|
try:
|
|
saved = save_persona(loaded.model_copy(update={'name': 'Edited'}))
|
|
finally:
|
|
host_bridge.operation_id.reset(token)
|
|
assert saved.version == 8 and saved.revision == 'b' * 64
|
|
method, params = calls[-1]
|
|
assert method == 'persona.write' and params['operation_id'] == 'controlled-operation'
|
|
assert 'revision' not in params['record']['data']
|
|
with pytest.raises(ApiError) as error:
|
|
save_persona(loaded.model_copy(update={'revision': 'c' * 64}))
|
|
assert error.value.code == 'PERSONA_VERSION_CONFLICT'
|
|
monkeypatch.setattr(persona_settings, '_desktop', lambda: False)
|
|
assert load_persona().system_prompt == 'legacy global'
|
|
|
|
|
|
def test_desktop_missing_persona_does_not_import_unowned_global_data(monkeypatch):
|
|
from app.services import persona_settings, desktop_notes
|
|
save_persona(PersonaSettings(system_prompt='unowned global data'))
|
|
monkeypatch.setattr(persona_settings, '_desktop', lambda: True)
|
|
monkeypatch.setattr(desktop_notes, 'call', lambda *args, **kwargs: None)
|
|
assert load_persona() == PersonaSettings()
|
|
|
|
|
|
def test_legacy_preview_requires_host_scope_and_never_mutates_source(monkeypatch):
|
|
from app.services import persona_settings, desktop_notes
|
|
original = save_persona(PersonaSettings(system_prompt='legacy preview', version=0))
|
|
monkeypatch.setattr(persona_settings, '_desktop', lambda: True)
|
|
calls = []
|
|
def allowed(method, **params):
|
|
calls.append((method, params))
|
|
return None
|
|
monkeypatch.setattr(desktop_notes, 'call', allowed)
|
|
preview = persona_settings.legacy_persona_preview()
|
|
assert preview['available'] is True
|
|
assert preview['persona']['system_prompt'] == 'legacy preview'
|
|
assert 'revision' not in preview['persona']
|
|
assert calls == [('persona.get', {'id': 'default'})]
|
|
def denied(*args, **kwargs):
|
|
raise ApiError(409, 'VAULT_PERMISSION_CHANGED', 'controlled')
|
|
monkeypatch.setattr(desktop_notes, 'call', denied)
|
|
with pytest.raises(ApiError):
|
|
persona_settings.legacy_persona_preview()
|
|
monkeypatch.setattr(persona_settings, '_desktop', lambda: False)
|
|
assert load_persona() == original
|
|
|
|
|
|
def test_legacy_preview_reports_no_source_without_creating_persona(monkeypatch):
|
|
from app.services import persona_settings, desktop_notes
|
|
monkeypatch.setattr(persona_settings, '_desktop', lambda: True)
|
|
monkeypatch.setattr(desktop_notes, 'call', lambda *args, **kwargs: None)
|
|
assert persona_settings.legacy_persona_preview() == {'available': False, 'persona': None}
|