完善报价业务问答与连续追问,补齐订单列表和人工转接
This commit is contained in:
@@ -1328,3 +1328,38 @@ test('an unavailable image gets one readable replacement instead of a broken ima
|
||||
app.element('chatDom').listeners.error({target:image});
|
||||
assert.equal(shown,first);
|
||||
});
|
||||
|
||||
test('an explicit typed human request opens one real queue without an AI question', async () => {
|
||||
const app=await customerPage({human:false});
|
||||
await app.context.window.fillInput('请帮我转人工客服');
|
||||
assert.equal(app.calls.filter(c=>c.url==='/api/visitor/human/request').length,1);
|
||||
assert.equal(app.calls.filter(c=>c.url==='/stream_chat').length,0);
|
||||
assert.equal(app.context.window.humanSid,7);
|
||||
assert.equal(app.element('input-text').value,'');
|
||||
});
|
||||
|
||||
test('a failed typed transfer keeps the command and an information question uses AI', async () => {
|
||||
const app=await customerPage({human:false});
|
||||
app.hooks['/api/visitor/human/request']=()=>response({ok:false},503);
|
||||
await app.context.window.fillInput('帮我转人工客服');
|
||||
assert.equal(app.element('input-text').value,'帮我转人工客服');
|
||||
assert.equal(app.context.window.humanSid,0);
|
||||
await app.context.window.fillInput('不需要人工客服,人工几点上班?');
|
||||
assert.equal(app.calls.filter(c=>c.url==='/api/visitor/human/request').length,1);
|
||||
assert.equal(app.calls.filter(c=>c.url==='/stream_chat').length,1);
|
||||
});
|
||||
|
||||
test('a typed transfer preserves a newer draft while connecting and cancelled offline input', async () => {
|
||||
const app=await customerPage({human:false});
|
||||
let resolve;
|
||||
app.hooks['/api/visitor/human/request']=()=>new Promise(done=>{resolve=done;});
|
||||
const sending=app.context.window.fillInput('帮我转人工客服');
|
||||
await flush();
|
||||
app.element('input-text').value='新输入的订单问题';
|
||||
resolve(response({ok:true,session_id:8}));await sending;
|
||||
assert.equal(app.element('input-text').value,'新输入的订单问题');
|
||||
const closed=await customerPage({human:false,serviceClosed:true});
|
||||
await closed.context.window.fillInput('帮我转人工客服');
|
||||
assert.equal(closed.element('input-text').value,'帮我转人工客服');
|
||||
assert.equal(closed.calls.filter(c=>c.url==='/api/ticket').length,0);
|
||||
});
|
||||
|
||||
@@ -46,6 +46,27 @@ class OrderCountTests(unittest.TestCase):
|
||||
self.assertTrue(self.events(question)[0]['login_required'])
|
||||
count.assert_not_called(); model.assert_not_called()
|
||||
|
||||
def test_order_list_is_login_scoped_status_filtered_and_protects_history(self):
|
||||
self.assertTrue(self.events('列出我的全部订单')[0]['login_required'])
|
||||
self.login()
|
||||
cid=self.a.post('/api/new_chat').json['id']
|
||||
text=self.events('列出我的全部订单',cid)[0]['t']
|
||||
self.assertIn('COUNT20260001',text);self.assertIn('COUNT20260002',text)
|
||||
self.assertNotIn('INB20260099',text);self.assertNotIn('COUNT20260003',text)
|
||||
text=self.events('列出已完成的订单')[0]['t']
|
||||
self.assertIn('COUNT20260002',text);self.assertNotIn('COUNT20260001',text)
|
||||
self.assertIn('暂不支持按这个条件',self.events('列出今天的订单')[0]['t'])
|
||||
self.a.post('/api/customer/logout')
|
||||
self.assertEqual(self.a.get('/api/get_messages',query_string={'conv_id':cid}).json,[])
|
||||
|
||||
def test_address_change_does_not_claim_a_mutation_or_bypass_login(self):
|
||||
message='帮我把INB20260001收件地址改成新地址'
|
||||
self.assertTrue(self.events(message)[0]['login_required'])
|
||||
self.login()
|
||||
events=self.events(message)
|
||||
self.assertIn('尚未修改订单',events[0]['t'])
|
||||
self.assertFalse(any('card' in e for e in events))
|
||||
|
||||
def test_logged_in_counts_accept_common_wording_and_refresh_after_data_changes(self):
|
||||
self.login()
|
||||
cid = self.a.post('/api/new_chat').json['id']
|
||||
|
||||
@@ -100,10 +100,11 @@ class OrderDataTests(unittest.TestCase):
|
||||
self.assertEqual(events[-1], {'d':1})
|
||||
model.assert_not_called()
|
||||
|
||||
def test_unimplemented_bulk_queries_ask_for_identifier_instead_of_inventing_records(self):
|
||||
def test_bulk_query_lists_only_existing_orders_without_model(self):
|
||||
with patch('logistics_chat.ai_client.post_completion') as model:
|
||||
events = self.events('列出所有订单')
|
||||
self.assertIn('请提供入仓码', events[0]['t'])
|
||||
self.assertIn(self.record['inbound_code'], events[0]['t'])
|
||||
self.assertIn(self.record['order_no'], events[0]['t'])
|
||||
model.assert_not_called()
|
||||
|
||||
def test_server_tenant_scope_and_ambiguous_identifiers(self):
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Customer-language regressions use synthetic amounts, never private tariffs."""
|
||||
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from logistics_chat import create_app, db
|
||||
from logistics_chat.quote_data import import_snapshot
|
||||
from logistics_chat.quote_engine import quantity, parse_intent
|
||||
from test_quotes import fixture_snapshot, frames
|
||||
|
||||
|
||||
class QuoteBusinessTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
folder=tempfile.TemporaryDirectory();self.addCleanup(folder.cleanup)
|
||||
root=Path(folder.name)
|
||||
self.app=create_app({'TESTING':True,'DATABASE':root/'test.db','UPLOAD_DIR':root/'uploads',
|
||||
'BACKUP_DIR':root/'backups','LOG_DIR':root/'logs','DEEPSEEK_API_KEY':'','RATE_LIMIT_PER_MIN':1000})
|
||||
self.client=self.app.test_client()
|
||||
self.client.post('/api/visitor/session',json={'key':'1'*64})
|
||||
book=fixture_snapshot()
|
||||
documents=[
|
||||
'铁铝制品另外加附加费¥5/KG',
|
||||
'正常货物 A.0KG<单件重量<22KG B.0CM<最长单边长度<120CM C.0CM<第二边<75CM D.最长边<260CM',
|
||||
'单件不能超22KG 超重收+70/件',
|
||||
'普通超尺寸加收90RMB/件',
|
||||
'大尺寸货物,任意2个条件,加收400RMB/件:22KG<单件重量<68KG;0<最长边<274CM;330CM<周长+单边长<419CM',
|
||||
'超大件货物,加收USD900/件',
|
||||
'保险费:保额的百分之0.25,min 13USD,投保金额是货值乘以1.2。',
|
||||
'海外进口清关,超过5个品名,续页加收USD7元RMB/个',
|
||||
'一般贸易出口报关费:RMB60/票',
|
||||
'偏远地址加收4元/kg;超偏远地址加收6元/kg',
|
||||
'禁止运输的货物:纯电池、液体、食品',
|
||||
]
|
||||
book['documents']=[{'sheet':'海派','cell':f'A{i+1}','text':text} for i,text in enumerate(documents)]
|
||||
book['documents'].append({'sheet':'美森加班海卡','cell':'A1','text':'不可接一般贸易'})
|
||||
with self.app.app_context():
|
||||
c=db.connect();import_snapshot(c,book,activate=True);c.commit();c.close()
|
||||
|
||||
def ask(self,text,cid=None):
|
||||
with patch('logistics_chat.ai_client.post_completion') as model:
|
||||
events=frames(self.client.post('/stream_chat',json={'msg':text,'conv_id':cid}))
|
||||
model.assert_not_called()
|
||||
self.assertEqual(events[-1],{'d':1},events)
|
||||
return events[0]['t']
|
||||
|
||||
def test_different_cartons_use_individual_minimums_and_dimensions(self):
|
||||
text=self.ask('美国美西,1箱10KG,1箱20KG,义乌普船海派')
|
||||
self.assertIn('32KG = 64元',text)
|
||||
text=self.ask('美国美西,2箱,1箱10KG,60x50x40cm,1箱20KG,40x30x20cm,义乌普船海派')
|
||||
self.assertIn('40KG = 80元',text)
|
||||
text=self.ask('美国美西,2箱,1箱10KG,60x50x40cm,1箱20KG,义乌普船海派')
|
||||
self.assertIn('40KG = 80元',text)
|
||||
self.assertIn('箱数与分箱明细不一致',self.ask('美国3箱,1箱10KG,1箱20KG海派'))
|
||||
|
||||
def test_weight_units_chinese_followup_and_invalid_postal(self):
|
||||
cid=self.client.post('/api/new_chat').json['id']
|
||||
self.ask('美国美西30KG,义乌海派',cid)
|
||||
for text in ('五十公斤呢','50000克呢','0.05吨呢'):
|
||||
self.assertIn('美国 50KG',self.ask(text,cid))
|
||||
self.assertIn('大于 0',self.ask('美国重量为-30KG海派'))
|
||||
self.assertIn('邮编格式不正确',self.ask('美国30KG,邮编518000海派'))
|
||||
self.assertIn('美国 30KG',self.ask('美国30000克海派'))
|
||||
self.assertIn('未找到目的仓码',self.ask('美国30KG发ABC999海派'))
|
||||
|
||||
def test_commodity_survives_weight_followups_and_new_commodity_replaces_it(self):
|
||||
cid=self.client.post('/api/new_chat').json['id']
|
||||
text=self.ask('美国美西铁制品30KG,义乌普船海派',cid)
|
||||
self.assertIn('铁铝附加费5元/KG',text)
|
||||
self.assertIn('30KG = 210元',text)
|
||||
self.assertIn('50KG = 350元',self.ask('50KG呢',cid))
|
||||
self.assertNotIn('铁铝附加费',self.ask('换成普通货50KG呢',cid))
|
||||
|
||||
def test_limit_fees_take_maximum_per_carton_and_do_not_guess_bad_threshold(self):
|
||||
text=self.ask('美国美西1箱30KG,130x50x20cm,义乌普船海派')
|
||||
self.assertIn('最高项90元/件',text)
|
||||
self.assertNotIn('160元',text)
|
||||
text=self.ask('美国美西1箱30KG,180x50x30cm,义乌普船海派')
|
||||
self.assertIn('最高项400元/件',text)
|
||||
text=self.ask('美国美西1箱30KG,40x30x20cm,义乌普船海派')
|
||||
self.assertIn('最高项70元/件',text)
|
||||
self.assertNotIn('400元/件',text)
|
||||
self.assertIn('人工确认是否接收',self.ask('美国美西1箱70KG,40x30x20cm,义乌普船海派'))
|
||||
|
||||
def test_cbm_uses_two_decimals_half_up_without_creating_free_shipments(self):
|
||||
rate={'unit':'cbm','kind':'truck','service':'洛杉矶普船海卡'}
|
||||
intent=parse_intent('美国1KG,1.005CBM',set())
|
||||
self.assertEqual(quantity(intent,rate),Decimal('1.01'))
|
||||
intent['cbm']='1.004';self.assertEqual(quantity(intent,rate),Decimal('1.00'))
|
||||
intent['weight']='0.1';intent['cbm']='0.004'
|
||||
self.assertIsNone(quantity(intent,rate))
|
||||
|
||||
def test_business_topics_use_current_documents_instead_of_generic_quotes(self):
|
||||
insurance=self.ask('美国保价1万美元,保险费怎么算')
|
||||
self.assertIn('0.25%',insurance);self.assertIn('=25USD',insurance)
|
||||
self.assertNotIn('元/KG',insurance)
|
||||
self.assertIn('大于0',self.ask('美国保价-10美元,保险费怎么算'))
|
||||
text=self.ask('美国30KG,7个品名清关续页收费')
|
||||
self.assertIn('7美元/个',text);self.assertIn('=14美元',text)
|
||||
text=self.ask('美国30KG还需要什么费用')
|
||||
self.assertIn('偏远地址加4元/KG',text);self.assertIn('超偏远地址加6元/KG',text)
|
||||
self.assertNotIn('250',text)
|
||||
self.assertIn('不可接一般贸易',self.ask('美国美森加班海卡一般贸易报关多少钱'))
|
||||
self.assertIn('不接收',self.ask('美国30KG纯电池可以发吗'))
|
||||
self.assertIn('不接收',self.ask('美国30KG食品,FDA申报费多少人民币'))
|
||||
|
||||
def test_destination_origin_comparison_time_and_english(self):
|
||||
cid=self.client.post('/api/new_chat').json['id']
|
||||
self.ask('美国30KG发ZZZ1,义乌海派',cid)
|
||||
text=self.ask('深圳交仓呢',cid)
|
||||
self.assertIn('美国 30KG',text);self.assertNotIn('· 义乌',text)
|
||||
text=self.ask('海运和空运哪个好,美国30KG')
|
||||
self.assertIn('没有美国空运费率',text)
|
||||
text=self.ask('美国美西海派多久')
|
||||
self.assertIn('提取不等于送达',text);self.assertNotIn('元/KG',text)
|
||||
text=self.ask('Please quote 30kg to US by sea')
|
||||
self.assertIn('CNY/kg',text)
|
||||
self.assertFalse(__import__('re').search(r'[\u4e00-\u9fff]',text),text)
|
||||
|
||||
def test_human_and_photo_questions_never_use_old_regional_model_facts(self):
|
||||
self.assertIn('联系人工',self.ask('帮我转人工客服'))
|
||||
text=self.ask('我可以发张货物照片让你判断报价吗')
|
||||
self.assertIn('不会自动识别',text);self.assertNotIn('21KG',text)
|
||||
Reference in New Issue
Block a user