Files
yawan-workbench/server/tests/test_segment_assignment.py
T

154 lines
9.5 KiB
Python

import copy
import json
from types import SimpleNamespace
import pytest
from fastapi import HTTPException
from sqlmodel import Session, SQLModel, create_engine, select
from app.main import segment_edit, get_campaign, segment_versions, resume_campaign_run, cancel_campaign_run
from app.models import Agent, AppUser, Campaign, CampaignRun, Canvas, CanvasGrant, Segment, Memory, ModelProvider
from app.segment_assignment import available_agents
from app.agents.snapshot import freeze_agent_snapshot
from app.agents import runner
from app.seed import upstream_anchor_cards
@pytest.fixture
def scope(tmp_path):
engine=create_engine('sqlite://')
SQLModel.metadata.create_all(engine)
with Session(engine) as db:
user=AppUser(username='owner',role='admin')
other=AppUser(username='other',role='editor')
db.add_all([user,other]);db.commit()
a=Agent(name='心法智能体',role='production',rules='OLD_HEART_RULES',content_tags=['心法'],kb_ids=[1],owner_user_id=user.id)
b=Agent(name='功法智能体',role='production',rules='NEW_PRACTICE_RULES',content_tags=['功法'],kb_ids=[2],owner_user_id=user.id)
db.add_all([a,b]);db.commit()
campaign=Campaign(name='循序训练',owner_user_id=user.id,status='parsed',qc_mode='off',mindmap_text='基础认知→练习→应用',canvas_snapshot={'nodes':[{'node_id':'n1','agent_id':a.id}], 'edges':[]})
db.add(campaign);db.commit()
segment=Segment(campaign_id=campaign.id,agent_id=a.id,node_id='n1',title='基础练习',content_tag='心法',mindmap_node='依照已学概念练习动作')
db.add(segment);db.commit()
yield db,user,other,a,b,campaign,segment,tmp_path
engine.dispose()
def request(user):return SimpleNamespace(state=SimpleNamespace(user=user))
def test_manual_assignment_refreshes_configuration_preserves_task_and_sticks_after_tag_edit(scope):
db,user,other,a,b,c,seg,_=scope
original=copy.deepcopy(c.canvas_snapshot)
seg.runtime_snapshot={'primary':{'id':a.id,'rules':'frozen old','kb_ids':[1]}}
seg.kb_category_ids=[10];seg.kb_category_recommendation={'category_ids':[10]};seg.fusion_agent_ids=[a.id]
db.add(seg);db.commit()
segment_edit(c.id,seg.id,{'agent_id':b.id},db,request(user))
assert seg.agent_id==b.id and seg.agent_assignment_mode=='manual'
assert seg.runtime_snapshot=={} and seg.kb_category_ids==[] and seg.kb_category_recommendation=={}
assert seg.fusion_agent_ids==[] and seg.node_id=='n1' and seg.mindmap_node=='依照已学概念练习动作'
assert c.canvas_snapshot==original
segment_edit(c.id,seg.id,{'content_tag':'心法进阶'},db,request(user))
assert seg.agent_id==b.id
detail=get_campaign(c.id,db,request(user))
assert detail['segments'][0]['agent_name']=='功法智能体'
assert detail['segments'][0]['agent_assignment_mode']=='manual' and detail['can_edit_agents']
@pytest.mark.parametrize('bad_id',[None,'abc',0,-2])
def test_invalid_agent_id_never_silently_succeeds(scope,bad_id):
db,user,_,a,b,c,seg,_=scope
with pytest.raises(HTTPException) as exc:segment_edit(c.id,seg.id,{'agent_id':bad_id},db,request(user))
assert exc.value.status_code==422 and seg.agent_id==a.id
def test_foreign_disabled_and_nonproduction_agents_are_rejected(scope):
db,user,_,a,b,c,seg,_=scope
for item in [Agent(name='foreign',role='production',organization_id=2,visibility='global'),
Agent(name='disabled',role='production',status='disabled'),Agent(name='QC',role='qc')]:
db.add(item);db.commit()
with pytest.raises(HTTPException) as exc:segment_edit(c.id,seg.id,{'agent_id':item.id},db,request(user))
assert exc.value.status_code==404
assert seg.agent_id==a.id
def test_selection_obeys_canvas_use_grant_without_exposing_other_private_agents(scope):
db,user,viewer,a,b,c,seg,_=scope
c.owner_user_id=viewer.id
a.visibility = b.visibility = 'private'
db.add_all([a, b])
canvas=Canvas(name='授权画布',owner_user_id=user.id,visibility='shared',nodes=[{'agent_id':b.id}])
db.add_all([c,canvas]);db.commit()
assert available_agents(db,c,viewer)==[]
db.add(CanvasGrant(canvas_id=canvas.id,user_id=viewer.id,permission='use'));db.commit()
assert [item.id for item in available_agents(db,c,viewer)]==[b.id]
segment_edit(c.id,seg.id,{'agent_id':b.id},db,request(viewer))
assert seg.agent_id==b.id
def test_existing_outputs_require_confirmation_archive_history_and_invalidate_later_drafts(scope):
db,user,_,a,b,c,seg,path=scope
old=path/'old.md';old.write_text('原心法正文',encoding='utf8')
later=path/'later.md';later.write_text('依赖旧心法的下文',encoding='utf8')
seg.status='approved';seg.artifact_path=str(old);seg.anchor_card={'ending_hook':'旧钩子'}
next_seg=Segment(campaign_id=c.id,agent_id=a.id,title='下游应用',status='approved',artifact_path=str(later),anchor_card={'ending_hook':'旧应用'})
skipped=Segment(campaign_id=c.id,agent_id=a.id,title='已放弃',status='skipped',artifact_path=str(later))
run=CampaignRun(campaign_id=c.id,status='completed',config_snapshot={'original':'immutable'})
db.add_all([seg,next_seg,skipped,run]);db.commit()
with pytest.raises(HTTPException) as exc:segment_edit(c.id,seg.id,{'agent_id':b.id},db,request(user))
assert exc.value.status_code==409 and seg.agent_id==a.id and next_seg.status=='approved'
result=segment_edit(c.id,seg.id,{'agent_id':b.id,'reset_generated':True},db,request(user))
assert result['reset_segment_ids']==[seg.id,next_seg.id]
assert seg.status==next_seg.status=='pending' and skipped.status=='skipped'
assert seg.artifact_path==next_seg.artifact_path=='' and seg.anchor_card==next_seg.anchor_card=={}
assert old.read_text()=='原心法正文' and later.read_text()=='依赖旧心法的下文'
assert seg.round==2 and next_seg.round==2
assert segment_versions(c.id,seg.id,db,request(user))[0]['artifact']=='原心法正文'
assert run.config_snapshot=={'original':'immutable'}
with pytest.raises(HTTPException) as exc:resume_campaign_run(run.id,db,request(user))
assert exc.value.status_code==409
assert runner.human_review(db,seg.id,'approve')['status']=='error'
@pytest.mark.parametrize('state',['running','paused'])
def test_active_run_blocks_switch_until_stopped(scope,state):
db,user,_,a,b,c,seg,_=scope
run=CampaignRun(campaign_id=c.id,status=state)
db.add(run);db.commit()
with pytest.raises(HTTPException) as exc:segment_edit(c.id,seg.id,{'agent_id':b.id},db,request(user))
assert exc.value.status_code==409 and seg.agent_id==a.id
if state=='paused':
assert cancel_campaign_run(run.id,db,request(user))['status']=='cancelled'
segment_edit(c.id,seg.id,{'agent_id':b.id},db,request(user))
assert seg.agent_id==b.id
def test_switch_uses_new_agent_rules_memory_and_kb_with_same_previous_and_next_context(scope,monkeypatch):
db,user,_,a,b,c,seg,path=scope
previous=path/'previous.md';previous.write_text('上一段已经确认:先分辨感受,再进行具体练习。',encoding='utf8')
seg.title='前置概念';seg.status='approved';seg.artifact_path=str(previous);seg.anchor_card={'ending_hook':'接下来进入练习'}
target=Segment(campaign_id=c.id,agent_id=a.id,title='基础练习',content_tag='心法',mindmap_node='运用前置概念做练习')
ignored=Segment(campaign_id=c.id,agent_id=a.id,title='不再采用的旁支',status='skipped',node_id='skip',anchor_card={'ending_hook':'不能采用'})
following=Segment(campaign_id=c.id,agent_id=a.id,title='成果应用',mindmap_node='运用练习的成果')
provider=ModelProvider(name='mock',base_url='https://example.invalid/v1',models=['mock-model'],default_model='mock-model')
db.add_all([seg,target,ignored,following,provider]);db.commit()
db.add_all([Memory(agent_id=a.id,content='OLD_HEART_MEMORY',owner_user_id=user.id),Memory(agent_id=b.id,content='NEW_PRACTICE_MEMORY',owner_user_id=user.id)])
db.commit()
target.runtime_snapshot={'primary':freeze_agent_snapshot(db,a,user=user)};db.add(target);db.commit()
segment_edit(c.id,target.id,{'agent_id':b.id},db,request(user))
monkeypatch.setattr(runner,'ARTIFACTS_DIR',path)
monkeypatch.setattr(runner,'_provider_model',lambda session,agent:(provider,'mock-model'))
monkeypatch.setattr(runner,'consume_reserved_chunks',lambda *args:None)
observed={}
def rag(session,agent,segment,kb_ids=None):
observed['kb_ids']=kb_ids
return '新智能体的知识材料',[],[{'id':'stub','kb_id':2}]
monkeypatch.setattr(runner,'_rag_materials',rag)
def model(session,**kwargs):
observed.update(kwargs)
anchor={field:'明确内容' for field in runner.ANCHOR_FIELDS}
return {'status':'completed','text':'承接前面确认的概念,进入练习。'+runner.ANCHOR_MARK+json.dumps(anchor,ensure_ascii=False)}
monkeypatch.setattr(runner,'run_agent',model)
result=runner.execute_segment(db,target,c)
prompt=observed['messages'][0]['content']
assert result['status']=='human_review' and observed['agent'].id==b.id
assert observed['kb_ids']==[2] and 'NEW_PRACTICE_RULES' in prompt and 'NEW_PRACTICE_MEMORY' in prompt
assert 'OLD_HEART_RULES' not in prompt and 'OLD_HEART_MEMORY' not in prompt
assert '上一段已经确认:先分辨感受' in prompt and '下一段标题:成果应用' in prompt
assert '标签只用于描述本段内容' in prompt and '不重复开场' in prompt
assert runner._next_segment_context(db,target,c).find('不再采用的旁支')==-1
assert upstream_anchor_cards(db,c.id,'end',[{'from':'skip','to':'end'}])==[]