"""Oral-language, US-region isolation and scoped-page regressions using synthetic facts.""" from decimal import Decimal from pathlib import Path import re import tempfile import unittest from unittest.mock import patch from logistics_chat import create_app,db from logistics_chat.order_data import import_records from logistics_chat.pricing import detect_dim_unit,dims_to_cm from logistics_chat.quote_data import import_snapshot from logistics_chat.quote_intent import parse_intent from logistics_chat.quote_engine import quantity from test_quotes import fixture_snapshot,frames class CustomerGapTests(unittest.TestCase): def setUp(self): folder=tempfile.TemporaryDirectory();self.addCleanup(folder.cleanup) root=Path(folder.name) self.app=create_app({'TESTING':True,'DATABASE':root/'data.db','UPLOAD_DIR':root/'uploads', 'BACKUP_DIR':root/'backups','LOG_DIR':root/'logs','DEEPSEEK_API_KEY':'', 'CUSTOMER_DEMO_TENANT_ID':'qa-a','RATE_LIMIT_PER_MIN':1000}) self.client=self.app.test_client();self.client.post('/api/visitor/session',json={'key':'1'*64}) book=fixture_snapshot() book['warehouses'].append({'code':'YYY2','zone':'西1区','state':'AA','postal':'90001', 'address':'','source_sheet':'合成分区','source_cell':'A8'}) texts=['偏远地址加收4元/kg;超偏远地址加收6元/kg','铁铝制品另外加附加费¥5/KG', '正常货物 0KG<单件重量<22KG 0CM<最长单边长度<120CM 0CM<第二边<75CM 最长边<260CM', '超重收+70/件','客户自行交货,不提供上门提货服务', '禁止运输的货物:纯电池、液体','需熏蒸的货物由客户在备货阶段自行完成'] book['documents']=[{'sheet':'合成渠道','cell':f'A{i+1}','text':text} for i,text in enumerate(texts)] self.order={'tenant_id':'qa-a','order_no':'FOCUS20260001','inbound_code':'QAFOCUS20260001', 'country_code':'US','status':'COMPLETED','package_count':2, 'tracking_json':[{'code':'IN_TRANSIT','actual_at':'2026-10-01 10:00'}]} with self.app.app_context(): c=db.connect();import_snapshot(c,book,activate=True) import_records(c,[self.order,{**self.order,'tenant_id':'qa-b','order_no':'OTHER20260099','inbound_code':'QAOTHER20260099'}]) c.commit();c.close() def conversation(self):return self.client.post('/api/new_chat').json['id'] def events(self,q,cid=None,client=None): with patch('logistics_chat.ai_client.post_completion') as model: result=frames((client or self.client).post('/stream_chat',json={'msg':q,'conv_id':cid})) model.assert_not_called() self.assertEqual(result[-1],{'d':1},result) return result def ask(self,q,cid=None):return self.events(q,cid)[0]['t'] def login(self): self.assertTrue(self.client.post('/api/customer/login',json={'username':'admin','password':'123456'}).json['ok']) def test_us_natural_questions_never_enter_old_regional_model_faqs(self): cases=('你们这个报价是按毛重还是净重?','运费能便宜一点吗?','这个价格含税含派送吗?', '一共多少钱,给我最终总价','这个物流服务范围具体怎么执行?') for q in cases: with self.subTest(q=q): cid=self.conversation();self.ask('美国美西30KG,义乌普船海派',cid) text=self.ask(q,cid) self.assertNotIn('21KG',text);self.assertNotIn('250',text);self.assertNotIn('7元/KG',text) text=self.ask('我想发点货到美国,怎么收费?') self.assertIn('元/KG',text);self.assertIn('多少公斤',text) def test_final_total_respects_tax_labels_and_keeps_unconfirmed_fees_separate(self): cid=self.conversation();self.ask('美国美西2箱每箱15KG,40x30x20cm,义乌普船海派',cid) text=self.ask('一共多少钱,给我最终总价',cid) self.assertIn('30KG = 60元',text);self.assertIn('· 包税',text) self.assertNotIn('价格不含税',text);self.assertIn('最终应付总价',text) self.assertIn('折扣或合作价格需人工确认',self.ask('运费能便宜一点吗',cid)) def test_negative_warehouse_and_origin_edits_do_not_keep_rejected_values(self): cid=self.conversation();self.ask('美国30KG发ZZZ1,深圳普船海派',cid) text=self.ask('不是发ZZZ1,是YYY2',cid) self.assertNotIn('ZZZ1',text);self.assertIn('YYY2',text) text=self.ask('不要深圳,还是义乌交仓',cid) self.assertIn('· 义乌',text);self.assertNotIn('· 华南',text) text=self.ask('50KG呢',cid) self.assertIn('YYY2',text);self.assertNotIn('ZZZ1',text);self.assertIn('美国 50KG',text) def test_inches_convert_before_charge_weight_and_acceptance_limits(self): intent=parse_intent('美国30KG,60x40x30英寸,1箱普船海派',set()) self.assertEqual(intent['dim_unit'],'in') self.assertEqual(dims_to_cm([60,40,30],'in'),[152.4,101.6,76.2]) self.assertGreater(quantity(intent,{'kind':'parcel','unit':'kg'}),Decimal('190')) text=self.ask('美国美西30KG,60x40x30英寸,1箱普船海派,义乌') self.assertIn('人工确认是否接收',text) for text in ('60x40x30 inches','60x40x30in','60x40x30"'): with self.subTest(text=text): self.assertEqual(detect_dim_unit(text),'in') self.assertIn('人工确认是否接收',self.ask('美国美西30KG,'+text+',1箱普船海派,义乌')) self.assertEqual(detect_dim_unit('60x40x30,合计1立方米'),'cm') self.assertEqual(detect_dim_unit('60x40x30,合计1m3'),'cm') self.assertEqual(detect_dim_unit('1.2x0.8x0.6m呢'),'m') def test_spoken_carton_and_dimensions_are_used_without_reasking(self): intent=parse_intent('美国30KG一个箱子,60长40宽30高,怎么算?',set()) self.assertEqual(intent['boxes'],1);self.assertEqual(intent['dims'],[60,40,30]) text=self.ask('美国美西30KG一个箱子,60长40宽30高,义乌普船海派') self.assertIn('最高项70元/件',text) self.assertNotIn('请补充箱数',text) def test_comparing_two_weights_is_not_treated_as_two_cartons(self): cid=self.conversation();self.ask('美国30KG海派报价',cid) text=self.ask('单箱12.5KG会不会按13KG收费?',cid) self.assertIn('按原小数计费,不向上取整',text) self.assertIn('12.5KG',text) self.assertNotIn('请明确总重',text) def test_confirmed_fractional_weight_and_partial_metal_basis_are_used(self): self.assertIn('12.5KG = 25元',self.ask('美国美西1箱12.5KG,40x30x20cm,义乌普船海派')) self.assertIn('12.3456KG = 24.69元',self.ask('美国美西1箱12.3456KG,40x30x20cm,义乌普船海派')) cid=self.conversation() text=self.ask('美国美西30KG,部分铁制品,义乌普船海派',cid) self.assertIn('30KG = 60元',text);self.assertIn('不能按整票重量加费',text) self.assertNotIn('30KG = 210元',text) text=self.ask('铁铝部分计费重10KG',cid) self.assertIn('美国 30KG',text);self.assertIn('30KG = 110元',text) self.assertIn('×10KG=50元',text) self.assertIn('30KG = 110元',self.ask('一共多少钱,给我最终总价',cid)) text=self.ask('改成40KG',cid) self.assertIn('当前金额未包含该项',text);self.assertNotIn('×10KG',text) text=self.ask('全部换成铁制品40KG',cid) self.assertIn('40KG = 280元',text) def test_unknown_metal_and_cbm_quotes_do_not_spread_partial_fee_over_the_whole_load(self): text=self.ask('美国美西30KG,只是金属扣子,整票都按铁铝加费吗,义乌普船海派') self.assertIn('只按铁铝货物的计费重',text);self.assertIn('确认金属是否属于铁铝',text) text=self.ask('美国30KG发ZZZ1,1方不包税海卡,部分铁制品,铁铝部分计费重10KG,义乌') self.assertIn('1CBM = 150元',text);self.assertIn('×10KG=50元',text) self.assertIn('大于0',self.ask('美国30KG,部分铁制品,铁铝部分计费重-1KG,义乌海派')) text=self.ask('美国20KG发ZZZ1,10KG发YYY2,部分铁制品,铁铝部分计费重10KG,义乌普船海派') self.assertIn('不能重复套用到每个仓',text) self.assertIn('20KG = 100元',text);self.assertIn('12KG = 24元',text) self.assertNotIn('铁铝附加费5元/KG×10KG',text) def test_booking_phone_invoice_and_file_capabilities_do_not_invent_workflows(self): cases=(('就按刚才方案帮我下单','尚未创建订单'),('手机号能查吗','暂不支持手机号'), ('你们可以开发票吗,开票加几个点','开票税点'),('付款账号发我一下','收款账号'), ('可以月结吗','月结资格'),('帮我导出订单Excel','暂不提供订单Excel导出'), ('Excel能直接读里面箱规报价吗','不会自动读取Excel'), ('我换了个手机,之前聊天记录还在吗','不会自动同步恢复')) for q,expected in cases: with self.subTest(q=q):self.assertIn(expected,self.ask(q)) with patch('logistics_chat.order_data.lookup_order') as lookup: self.assertIn('暂不支持手机号',self.ask('手机号19900000000能查吗')) lookup.assert_not_called() with self.app.app_context(): c=db.connect();self.assertEqual(c.execute('SELECT COUNT(*) FROM order_test_data').fetchone()[0],2);c.close() def test_address_and_pickup_questions_are_not_prices_and_english_stays_english(self): cid=self.conversation();self.ask('美国30KG海派报价',cid) self.assertIn('地址、联系人和收货时间',self.ask('义乌仓具体地址是什么?几点可以送?',cid)) text=self.ask('Can you pick up my goods in Shenzhen for US sea freight?') self.assertIn('does not offer pickup',text);self.assertNotIn('CNY/kg',text) self.assertFalse(re.search(r'[\u4e00-\u9fff]',text)) def test_air_battery_question_does_not_apply_sea_battery_requirements(self): text=self.ask('想走美国空运,包税带电的,有吗?') self.assertIn('空运费率当前资料没有列出',text);self.assertNotIn('MSDS',text) self.assertIn('备货阶段',self.ask('美国木制品需要熏蒸吗')) def seed_pages(self): with self.app.app_context(): c=db.connect();import_records(c,[{'tenant_id':'qa-a','order_no':f'PAGE2026{i:06d}', 'inbound_code':f'QAPAGE2026{i:06d}','status':'PROCESSING'} for i in range(1,26)]) c.commit();c.close() def test_pages_use_fresh_authorized_count_and_preserve_filter_and_private_history(self): self.seed_pages() self.assertTrue(self.events('下一页')[0]['login_required']) self.login();cid=self.conversation() text=self.ask('列出我的全部订单',cid) self.assertIn('共26票',text);self.assertEqual(text.count('- 入仓码:'),20) self.assertNotIn('QAOTHER',text) text=self.ask('下一页',cid) self.assertIn('第2页',text);self.assertEqual(text.count('- 入仓码:'),6);self.assertIn('共26票',text) self.assertIn('最后一页',self.ask('下一页',cid)) self.assertIn('第1页',self.ask('上一页',cid)) self.ask('列出履约中的订单',cid) text=self.ask('下一页',cid) self.assertIn('共25票',text);self.assertEqual(text.count('- 入仓码:'),5);self.assertNotIn('QAFOCUS',text) self.assertIn('第1页',self.ask('第1页',cid)) self.client.post('/api/customer/logout') self.assertEqual(self.client.get('/api/get_messages',query_string={'conv_id':cid}).json,[]) def test_page_commands_cannot_reuse_another_topic_or_unbounded_offset(self): self.login();cid=self.conversation();self.ask('列出我的订单',cid) self.assertIn('有效页码',self.ask('第0页',cid)) self.ask('列出我的订单',cid) self.ask('美国30KG海派报价',cid) self.assertIn('请先',self.ask('下一页',cid)) def test_clearance_followup_modification_and_unknown_payment_do_not_call_model(self): self.assertTrue(self.events('我的订单已清关了吗')[0]['login_required']) self.login();cid=self.conversation();self.events('QAFOCUS20260001',cid) event=self.events('我的订单已清关了吗?',cid)[0] self.assertEqual(event['card']['no'],'FOCUS20260001') self.assertEqual(event['card']['tracking_status'],'海运途中') self.assertIn('尚未修改订单',self.ask('帮我把收货地址改一下',cid)) self.assertIn('未接入付款状态',self.ask('这票订单付过款了吗',cid)) self.assertIn('不能直接当作快递单号',self.ask('这票对应的UPS快递单号是多少',cid)) self.assertIn('暂不支持按这个物流条件',self.ask('哪些订单还没开船',cid))