Files

193 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Local order snapshots, exact lookup and conversational order queries."""
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from logistics_chat import create_app, db, order_data
from logistics_chat.tracking import CARD_PREFIX, card_to_text
class OrderDataTests(unittest.TestCase):
def setUp(self):
folder = tempfile.TemporaryDirectory()
self.addCleanup(folder.cleanup)
root = Path(folder.name)
self.app = create_app({'TESTING':True, 'DATABASE':root/'history.db',
'UPLOAD_DIR':root/'uploads', 'BACKUP_DIR':root/'backups', 'LOG_DIR':root/'logs',
'DEEPSEEK_API_KEY':'', 'ORDER_DATA_TENANT_ID':'', 'RATE_LIMIT_PER_MIN':1000})
self.client = self.app.test_client()
self.context = self.app.app_context()
self.context.push()
self.addCleanup(self.context.pop)
self.record = {'source_order_id':'999999999999999999', 'tenant_id':'tenant-a',
'order_no':'FL202610290001', 'inbound_code':'INB20261029001-01', 'shipment_id':'shipment00208',
'status':'PROCESSING', 'country_code':'US', 'transport_mode':'SEA', 'warehouse_code':'TEST1',
'order_role':'MASTER', 'package_count':3, 'gross_weight_kg':'30.000', 'volume_cbm':'0.2500',
'history_json':[{'time':'2026-10-28 10:00', 'status':'SUBMITTED'},
{'time':'2026-10-29 10:00', 'status':'PROCESSING'}]}
self.import_rows([self.record])
def import_rows(self, rows, replace=False):
connection = db.connect()
try:
with connection:
return order_data.import_records(connection, rows, replace=replace)
finally:
connection.close()
def events(self, message, cid=None):
response = self.client.post('/stream_chat', json={'msg':message, 'conv_id':cid})
self.assertEqual(response.status_code, 200)
return [json.loads(frame[6:]) for frame in response.get_data(as_text=True).split('\n\n') if frame.startswith('data: ')]
def test_identifiers_are_exact_case_insensitive_and_preserve_suffixes(self):
for value in ('fl202610290001', 'INB20261029001-01', 'shipment00208'):
self.assertEqual(order_data.lookup_order(value)[0]['order_no'], self.record['order_no'])
self.assertEqual(self.events('查询 '+value)[0]['card']['no'], self.record['order_no'])
self.assertEqual(order_data.lookup_order('FL20261029000'), [])
self.assertEqual(order_data.extract_reference('查询 INB20261029001-01'), ['INB20261029001-01'])
self.import_rows([{**self.record,'order_no':'BOX100x100x100'}])
self.assertEqual(self.events('查询 BOX100x100x100')[0]['card']['no'],'BOX100x100x100')
def test_weight_dates_and_attachment_urls_are_not_order_identifiers(self):
for text in ('2000KG海运报价', '100000 kg海运报价', '30.000000 KG', '2026-10-29',
'2026-10-29T16:53:00', '100x100x100cm', '1200 × 800 × 600 mm',
'1.2x0.8x0.6m', '[文件]/uploads/20261029100145_6787.txt'):
self.assertEqual(order_data.extract_reference(text), [])
events = self.events('帮我查询物流轨迹')
self.assertIn('订单号', events[0]['t'])
self.assertEqual(events[-1], {'d':1})
def test_order_identifiers_remain_queryable_alongside_quantities_and_dates(self):
for message in ('查询 FL202610290001,2026-10-29T16:53:00 提交的',
'FL202610290001 30.000000 KG', 'FL202610290001 100x100x100cm',
'FL202610290001 运费多少钱?'):
with self.subTest(message=message):
event = self.events(message)[0]
if '运费' in message:
self.assertIn('未记录实际运费', event['t'])
else:
self.assertEqual(event['card']['no'], self.record['order_no'])
def test_unknown_orders_never_produce_fake_tracking_or_call_model(self):
with patch('logistics_chat.ai_client.post_completion') as model:
events = self.events('查询 UNKNOWN20260001')
self.assertIn('未找到', events[0]['t'])
self.assertFalse(any('card' in event for event in events))
self.assertEqual(events[-1], {'d':1})
model.assert_not_called()
def test_unimplemented_bulk_queries_ask_for_identifier_instead_of_inventing_records(self):
with patch('logistics_chat.ai_client.post_completion') as model:
events = self.events('列出所有订单')
self.assertIn('请提供订单号', events[0]['t'])
model.assert_not_called()
def test_server_tenant_scope_and_ambiguous_identifiers(self):
self.import_rows([{**self.record, 'tenant_id':'tenant-b', 'gross_weight_kg':'99'}])
self.assertIn('多个订单', self.events(self.record['order_no'])[0]['t'])
self.app.config['ORDER_DATA_TENANT_ID'] = 'tenant-a'
rows = order_data.lookup_order(self.record['order_no'])
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]['gross_weight_kg'], '30.000')
self.app.config['ORDER_DATA_TENANT_ID'] = 'unknown-tenant'
self.assertEqual(order_data.lookup_order(self.record['order_no']), [])
def test_manual_changes_are_visible_and_reimport_preserves_them_by_default(self):
connection = db.connect()
connection.execute("UPDATE order_test_data SET gross_weight_kg='42.000' WHERE order_no=?", (self.record['order_no'],))
connection.commit()
connection.close()
self.assertEqual(self.import_rows([self.record]), {'imported':0, 'preserved':1})
fields = self.events(self.record['order_no'])[0]['card']['fields']
self.assertEqual(next(field['value'] for field in fields if field['key']=='weight'), '42.000 kg')
self.import_rows([self.record], replace=True)
self.assertEqual(order_data.lookup_order(self.record['order_no'])[0]['gross_weight_kg'], '30.000')
def test_new_table_records_can_be_added_without_touching_source_database(self):
self.import_rows([{**self.record, 'order_no':'LOCAL20260002', 'shipment_id':'localship002'}])
self.assertEqual(self.events('查询 LOCAL20260002')[0]['card']['no'], 'LOCAL20260002')
def test_business_status_history_does_not_claim_delivery_or_invent_transport_events(self):
self.import_rows([{**self.record, 'status':'COMPLETED'}], replace=True)
card = self.events(self.record['order_no'])[0]['card']
self.assertEqual(card['status'], '头程订单已完成')
self.assertNotIn('已签收', json.dumps(card, ensure_ascii=False))
self.assertEqual([node['time'] for node in card['nodes']], ['2026-10-29 10:00','2026-10-28 10:00'])
self.assertTrue(all(not node['place'] for node in card['nodes']))
def test_children_do_not_reuse_parent_total_weight(self):
self.import_rows([{**self.record, 'order_no':'CHILD20260001-01', 'order_role':'CHILD',
'gross_weight_kg':None, 'package_count':None, 'volume_cbm':None,
'parent_order_no':self.record['order_no'], 'data_note':'子单独立货物数据未提供'}])
card = self.events('CHILD20260001-01')[0]['card']
self.assertFalse(any(field['key']=='weight' for field in card['fields']))
self.assertEqual(next(field['value'] for field in card['fields'] if field['key']=='parent'), self.record['order_no'])
def test_followups_refresh_local_data_and_remain_within_conversation(self):
cid = self.client.post('/api/new_chat').json['id']
self.events(self.record['order_no'], cid)
connection = db.connect()
connection.execute("UPDATE order_test_data SET gross_weight_kg='45.000' WHERE order_no=?", (self.record['order_no'],))
connection.commit()
connection.close()
card = self.events('这票重量是多少?',cid)[0]['card']
self.assertEqual(card['no'], self.record['order_no'])
self.assertEqual(next(field['value'] for field in card['fields'] if field['key']=='weight'), '45.000 kg')
self.assertIn('未记录实际运费', self.events('这票运费多少钱?',cid)[0]['t'])
self.assertEqual(self.events('那状态呢?',cid)[0]['card']['no'], self.record['order_no'])
self.events('查询 UNKNOWN20260001',cid)
self.assertIn('未找到', self.events('这票重量呢?',cid)[0]['t'])
other_cid = self.client.post('/api/new_chat').json['id']
self.assertIsNone(order_data.answer_query('这票重量是多少?', other_cid))
def test_new_shipping_questions_do_not_reuse_previous_order(self):
cid = self.client.post('/api/new_chat').json['id']
self.events(self.record['order_no'], cid)
for message in ('现在有30KG货物想发美国,运费多少?', '美国海运费用怎么算?', '报价30公斤到德国', '物流状态是什么意思?'):
with self.subTest(message=message):
self.assertIsNone(order_data.answer_query(message, cid))
db.save_message(cid, 'user', '美国海运费用怎么算?')
db.save_message(cid, 'assistant', '海运费用与重量及目的地有关。')
self.assertIsNone(order_data.answer_query('那状态呢?', cid))
def test_short_order_followups_read_facts_without_calling_model(self):
cid = self.client.post('/api/new_chat').json['id']
self.events(self.record['order_no'], cid)
with patch('logistics_chat.ai_client.post_completion') as model:
for message in ('多少公斤?', '有几箱?', '体积呢?', '那状态呢?'):
with self.subTest(message=message):
self.assertEqual(self.events(message, cid)[0]['card']['no'], self.record['order_no'])
model.assert_not_called()
def test_card_storage_and_model_context_contain_only_selected_facts(self):
card = self.events(self.record['order_no'])[0]['card']
text = card_to_text(CARD_PREFIX+json.dumps(card))
self.assertIn('30.000', text)
self.assertIn('US', text)
self.assertNotIn('测试', text)
self.assertNotIn('tenant_id', text)
self.assertNotIn('customer_name', json.dumps(card))
def test_invalid_history_is_safe_and_read_failure_is_explicit(self):
connection = db.connect()
connection.execute("UPDATE order_test_data SET history_json='invalid'")
connection.commit()
self.assertEqual(self.events(self.record['order_no'])[0]['card']['nodes'], [])
connection.execute('DROP TABLE order_test_data')
connection.commit()
connection.close()
self.assertIn('暂时无法读取', self.events(self.record['order_no'])[0]['t'])
def test_import_failure_rolls_back_partial_rows(self):
with self.assertRaises(ValueError):
self.import_rows([{**self.record,'order_no':'ROLLBACK20260001'}, {'order_no':''}])
self.assertEqual(order_data.lookup_order('ROLLBACK20260001'), [])
if __name__ == '__main__':
unittest.main()