feat(server): add workbench API, DSH planner worker and RAG pipeline
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""长期记忆人工确认、版本替代及管理边界的回归测试。"""
|
||||
from types import SimpleNamespace
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
from app.access import canvas_allows_resource
|
||||
from app.agents.snapshot import freeze_agent_snapshot
|
||||
from app.main import create_pending_memory, decide_memory, list_memories, update_memory
|
||||
from app.models import Agent, AppUser, Canvas, CanvasGrant, Memory, PendingMemory
|
||||
|
||||
@pytest.fixture
|
||||
def scope():
|
||||
engine = create_engine('sqlite://')
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
user = AppUser(username='memory-owner', role='admin', organization_id=1)
|
||||
other = AppUser(username='memory-other', role='editor', organization_id=1)
|
||||
foreign = AppUser(username='memory-foreign', role='admin', organization_id=2)
|
||||
session.add_all([user, other, foreign]); session.commit()
|
||||
agent = Agent(name='记忆测试', role='content', owner_user_id=user.id, organization_id=1)
|
||||
session.add(agent); session.commit()
|
||||
yield session, user, other, foreign, agent
|
||||
engine.dispose()
|
||||
|
||||
def request(user):
|
||||
return SimpleNamespace(state=SimpleNamespace(user=user))
|
||||
|
||||
def candidate(session, user, agent, content='偏好口语表达', **kwargs):
|
||||
row = PendingMemory(target_agent_id=agent.id, content=content, owner_user_id=user.id,
|
||||
organization_id=user.organization_id, **kwargs)
|
||||
session.add(row); session.commit(); session.refresh(row)
|
||||
return row
|
||||
|
||||
def test_metadata_edit_remains_recallable_and_get_does_not_count(scope):
|
||||
session, user, _, _, agent = scope
|
||||
row = Memory(agent_id=agent.id, title='旧标题', content='保持口语表达', owner_user_id=user.id)
|
||||
session.add(row); session.commit(); session.refresh(row)
|
||||
before = freeze_agent_snapshot(session, agent, user=user)
|
||||
update_memory(row.id, {'title': '新标题', 'description': '口播场景'}, session, request(user))
|
||||
after = freeze_agent_snapshot(session, agent, user=user)
|
||||
assert before['memories'][0]['title'] == '旧标题'
|
||||
assert after['memories'][0]['title'] == '新标题'
|
||||
assert after['memories'][0]['id'] == row.id
|
||||
listing = list_memories(q='口播场景', s=session, request=request(user))
|
||||
assert listing[0]['index_status'] == 'indexed'
|
||||
assert listing[0]['usage_count'] == 0 and listing[0]['last_used_at'] == ''
|
||||
assert listing[0]['can_manage']
|
||||
|
||||
def test_confirm_refines_candidate_and_replaces_immutable_old_content(scope):
|
||||
session, user, _, _, agent = scope
|
||||
old = Memory(agent_id=agent.id, content='多举促销例子', owner_user_id=user.id)
|
||||
session.add(old); session.commit(); session.refresh(old)
|
||||
pending = candidate(session, user, agent, '这段不要总说促销')
|
||||
result = decide_memory(pending.id, {'action': 'approve', 'content': '教学段优先用生活例子',
|
||||
'type': 'L2', 'replace_memory_id': old.id}, session, request(user))
|
||||
session.refresh(old); session.refresh(pending)
|
||||
new = session.get(Memory, result['memory_id'])
|
||||
assert old.status == 'expired' and old.content == '多举促销例子'
|
||||
assert new.content == '教学段优先用生活例子' and new.layer == 'L2' and new.supersedes_id == old.id
|
||||
assert pending.content == '这段不要总说促销' and pending.status == 'approved'
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
update_memory(old.id, {'status': 'active'}, session, request(user))
|
||||
assert exc.value.status_code == 409
|
||||
|
||||
def test_restoring_ancestor_cannot_conflict_with_active_later_revision(scope):
|
||||
session, user, _, _, agent = scope
|
||||
first = Memory(agent_id=agent.id, content='版本一', status='expired', owner_user_id=user.id)
|
||||
session.add(first); session.commit(); session.refresh(first)
|
||||
second = Memory(agent_id=agent.id, content='版本二', status='expired', owner_user_id=user.id, supersedes_id=first.id)
|
||||
session.add(second); session.commit(); session.refresh(second)
|
||||
third = Memory(agent_id=agent.id, content='版本三', owner_user_id=user.id, supersedes_id=second.id)
|
||||
session.add(third); session.commit()
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
update_memory(first.id, {'status': 'active'}, session, request(user))
|
||||
assert exc.value.status_code == 409
|
||||
session.rollback()
|
||||
update_memory(third.id, {'status': 'expired'}, session, request(user))
|
||||
update_memory(first.id, {'status': 'active'}, session, request(user))
|
||||
assert first.status == 'active'
|
||||
|
||||
def test_unknown_empty_and_repeat_approval_are_rejected(scope):
|
||||
session, user, _, _, agent = scope
|
||||
row = candidate(session, user, agent)
|
||||
for body in ({'action': 'unknown'}, {'action': 'approve', 'content': ' '},
|
||||
{'action': 'approve', 'type': 'other'}, {'action': 'approve', 'tags': 'oops'}):
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
decide_memory(row.id, body, session, request(user))
|
||||
assert exc.value.status_code == 422
|
||||
decide_memory(row.id, {'action': 'approve'}, session, request(user))
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
decide_memory(row.id, {'action': 'approve'}, session, request(user))
|
||||
assert exc.value.status_code == 409
|
||||
assert len(session.exec(select(Memory)).all()) == 1
|
||||
|
||||
def test_dedup_does_not_merge_users_private_preferences(scope):
|
||||
session, user, other, _, agent = scope
|
||||
first = candidate(session, user, agent)
|
||||
second = candidate(session, other, agent)
|
||||
third = candidate(session, user, agent)
|
||||
a = decide_memory(first.id, {'action': 'approve'}, session, request(user))
|
||||
b = decide_memory(second.id, {'action': 'approve'}, session, request(user))
|
||||
c = decide_memory(third.id, {'action': 'approve'}, session, request(user))
|
||||
assert a['memory_id'] != b['memory_id'] and a['memory_id'] == c['memory_id']
|
||||
|
||||
def test_replacement_cannot_expire_another_users_memory(scope):
|
||||
session, user, other, foreign, agent = scope
|
||||
old = Memory(agent_id=agent.id, content='他人的偏好', owner_user_id=other.id)
|
||||
session.add(old); session.commit(); session.refresh(old)
|
||||
row = candidate(session, user, agent)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
decide_memory(row.id, {'action': 'approve', 'replace_memory_id': old.id}, session, request(user))
|
||||
assert exc.value.status_code == 422
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
decide_memory(row.id, {'action': 'approve'}, session, request(foreign))
|
||||
assert exc.value.status_code == 404
|
||||
assert old.status == 'active' and row.status == 'pending'
|
||||
|
||||
def test_manual_memory_is_private_pending_and_manage_permission_required(scope):
|
||||
session, user, other, _, agent = scope
|
||||
payload = {'agent_id': agent.id, 'content': '用具体例子教学', 'type': 'L1', 'visibility': 'global'}
|
||||
created = create_pending_memory(payload, session, request(user))
|
||||
row = session.get(PendingMemory, created['id'])
|
||||
assert row.status == 'pending' and row.visibility == 'private' and row.owner_user_id == user.id
|
||||
assert create_pending_memory(payload, session, request(user))['id'] == created['id']
|
||||
assert session.exec(select(Memory)).all() == []
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
create_pending_memory(payload, session, request(other))
|
||||
assert exc.value.status_code == 404
|
||||
|
||||
def test_canvas_shares_configured_experience_without_other_users_private_memory(scope):
|
||||
session, owner, viewer, foreign, agent = scope
|
||||
canvas = Canvas(name='共享测试画布', owner_user_id=owner.id, visibility='shared', nodes=[{'agent_id': agent.id}])
|
||||
session.add(canvas); session.commit(); session.refresh(canvas)
|
||||
session.add(CanvasGrant(canvas_id=canvas.id, user_id=viewer.id, permission='use'))
|
||||
configured = Memory(agent_id=agent.id, content='配置者经验', owner_user_id=owner.id)
|
||||
private = Memory(agent_id=agent.id, content='另一使用者偏好', owner_user_id=999)
|
||||
cross_tenant = Memory(agent_id=agent.id, content='其他组织', owner_user_id=owner.id, organization_id=2)
|
||||
session.add_all([configured, private, cross_tenant]); session.commit()
|
||||
assert canvas_allows_resource(session, configured, viewer, 'use')
|
||||
assert not canvas_allows_resource(session, private, viewer, 'use')
|
||||
assert not canvas_allows_resource(session, cross_tenant, viewer, 'use')
|
||||
assert not canvas_allows_resource(session, configured, viewer, 'manage')
|
||||
|
||||
|
||||
def test_replacement_preserves_explicit_grants_and_visibility(scope):
|
||||
from app.models import MemoryGrant
|
||||
session, user, other, _, agent = scope
|
||||
old = Memory(agent_id=agent.id, content='旧共享经验', owner_user_id=user.id, visibility='shared', tags=['教学'])
|
||||
session.add(old); session.commit(); session.refresh(old)
|
||||
session.add(MemoryGrant(memory_id=old.id, user_id=other.id, permission='use'))
|
||||
session.commit()
|
||||
row = candidate(session, user, agent, '新共享经验', visibility='shared')
|
||||
response = decide_memory(row.id, {'action': 'approve', 'replace_memory_id': old.id}, session, request(user))
|
||||
new = session.get(Memory, response['memory_id'])
|
||||
grant = session.exec(select(MemoryGrant).where(MemoryGrant.memory_id == new.id)).one()
|
||||
assert grant.user_id == other.id and grant.permission == 'use' and grant.enabled
|
||||
assert new.visibility == 'shared' and new.tags == ['教学']
|
||||
Reference in New Issue
Block a user