Files

188 lines
10 KiB
Python

"""HTTP replay keeps relative edits idempotent and identity scoped."""
import unittest
from unittest.mock import patch
import test_model_tool_loop as loop_fixtures
from test_model_tool_loop import tool_plan, quote_arguments, final_facts
from test_model_chat import plan
from test_quotes import frames
class ChatRequestTests(unittest.TestCase):
setUp = loop_fixtures.ModelToolLoopTests.setUp
saved = loop_fixtures.ModelToolLoopTests.saved
login = loop_fixtures.ModelToolLoopTests.login
def send(self, question, arguments, request_id):
with patch('logistics_chat.ai_client.post_completion', side_effect=[
tool_plan('quote_shipping', quote_arguments(**arguments), 'quote_a'), final_facts('fact_0')]):
return frames(self.client.post('/stream_chat', json={
'msg': question, 'conv_id': self.cid, 'request_id': request_id}))
def start_quote(self):
return self.send('美国1000KG义乌美东普船海派报价', {'services': ['普船海派'], 'shipment': {
'country': 'US', 'total_weight_kg': '1000', 'zone': '美东', 'origin': '义乌', 'kinds': ['parcel']}}, 'initial-request-0001')
def add_weight(self, request_id='weight-request-0001'):
return self.send('再加100KG', {'changes': [{'field': 'total_weight_kg', 'operation': 'add', 'value': '100'}]}, request_id)
def test_http_replay_is_read_only_and_an_identical_new_operation_still_adds(self):
self.start_quote()
original = self.add_weight()
before = self.saved()
with patch('logistics_chat.ai_client.post_completion') as model:
repeated = frames(self.client.post('/stream_chat', json={
'msg': '再加100KG', 'conv_id': self.cid, 'request_id': 'weight-request-0001'}))
model.assert_not_called()
self.assertEqual(repeated, original)
self.assertEqual(self.saved(), before)
self.assertIn('1100KG', repeated[0]['t'])
self.assertIn('1200KG', self.add_weight('weight-request-0002')[0]['t'])
with patch('logistics_chat.ai_client.post_completion') as model:
old_reply = frames(self.client.post('/stream_chat', json={
'msg': '再加100KG', 'conv_id': self.cid, 'request_id': 'weight-request-0001'}))
self.assertIn('1100KG', old_reply[0]['t'])
self.assertEqual(self.saved()[1]['intent']['weight'], '1200')
model.assert_not_called()
def test_duplicate_commits_return_the_original_result_before_state_cas(self):
self.start_quote()
original = self.add_weight()
before = self.saved()
with patch('logistics_chat.chat_requests.replay_result', return_value=None):
raced = self.add_weight()
self.assertEqual(raced, original)
self.assertEqual(self.saved(), before)
def test_request_id_cannot_be_reused_for_different_text(self):
self.start_quote()
with patch('logistics_chat.ai_client.post_completion') as model:
events = frames(self.client.post('/stream_chat', json={
'msg': '改为2000KG', 'conv_id': self.cid, 'request_id': 'initial-request-0001'}))
self.assertIn('e', events[0])
model.assert_not_called()
def test_invalid_ids_are_rejected_before_calling_a_model(self):
for value in ('short', 'x' * 81, ['request'], True):
with self.subTest(value=value), patch('logistics_chat.ai_client.post_completion') as model:
events = frames(self.client.post('/stream_chat', json={'msg': '报价', 'conv_id': self.cid, 'request_id': value}))
self.assertIn('e', events[0])
model.assert_not_called()
def test_deleting_a_conversation_removes_its_cached_results(self):
from logistics_chat import db
self.start_quote()
self.assertTrue(self.client.delete('/api/del_history', query_string={'id': self.cid}).json['ok'])
with self.app.app_context():
connection = db.connect()
self.assertEqual(connection.execute('SELECT COUNT(*) FROM chat_request_results WHERE conv_id=?', (self.cid,)).fetchone()[0], 0)
connection.close()
with patch('logistics_chat.ai_client.post_completion') as model:
events = frames(self.client.post('/stream_chat', json={
'msg': '美国1000KG义乌美东普船海派报价', 'conv_id': self.cid, 'request_id': 'initial-request-0001'}))
self.assertIn('e', events[0])
model.assert_not_called()
def test_guest_login_refreshes_orders_without_applying_the_quote_edit_again(self):
self.start_quote()
quote = tool_plan('quote_shipping', quote_arguments(changes=[
{'field': 'total_weight_kg', 'operation': 'add', 'value': '100'}]), 'quote_a').message['tool_calls'][0]
order = plan(('query_orders', '我的订单有多少个')).message['tool_calls'][0]
order['id'] = 'order_b'
from test_model_chat import Completion
question = '再加100KG,顺便查我的订单有多少个'
request_id = 'login-compound-0001'
def send():
with patch('logistics_chat.ai_client.post_completion', side_effect=[
Completion({'role': 'assistant', 'content': '', 'tool_calls': [quote, order]}, 'tool_calls'),
final_facts('fact_0', 'fact_1')]):
return frames(self.client.post('/stream_chat', json={
'msg': question, 'conv_id': self.cid, 'request_id': request_id}))
guest = send()
self.assertTrue(guest[0]['login_required'])
self.assertEqual(self.saved()[1]['intent']['weight'], '1100')
self.login()
authenticated = send()
self.assertNotIn('login_required', authenticated[0])
self.assertIn('1100KG', authenticated[0]['t'])
self.assertIn('1 票', authenticated[0]['t'])
self.assertEqual(self.saved()[1]['intent']['weight'], '1100')
self.client.post('/api/customer/logout', json={})
with patch('logistics_chat.ai_client.post_completion') as model:
denied = frames(self.client.post('/stream_chat', json={
'msg': question, 'conv_id': self.cid, 'request_id': request_id}))
self.assertTrue(denied[0]['login_required'])
model.assert_not_called()
def test_quote_replay_rejects_an_outdated_price_version(self):
from logistics_chat import db
from logistics_chat.quote_data import import_snapshot
self.start_quote()
newer = dict(self.snapshot, source_sha256='9' * 64)
with self.app.app_context():
connection = db.connect()
import_snapshot(connection, newer, activate=True)
connection.commit()
connection.close()
with patch('logistics_chat.ai_client.post_completion') as model:
events = frames(self.client.post('/stream_chat', json={
'msg': '美国1000KG义乌美东普船海派报价', 'conv_id': self.cid, 'request_id': 'initial-request-0001'}))
self.assertIn('报价资料已更新', events[0]['e'])
model.assert_not_called()
def test_login_model_timeout_reuses_confirmed_quote_in_local_fallback(self):
from logistics_chat import ai_client, db
from logistics_chat.quote_data import import_snapshot
from test_model_chat import Completion
self.start_quote()
question = '再加100KG报价,顺便查我的订单有多少个'
request_id = 'login-fallback-0001'
quote = tool_plan('quote_shipping', quote_arguments(changes=[
{'field': 'total_weight_kg', 'operation': 'add', 'value': '100'}]), 'quote_a').message['tool_calls'][0]
order = plan(('query_orders', '我的订单有多少个')).message['tool_calls'][0]
order['id'] = 'order_b'
with patch('logistics_chat.ai_client.post_completion', side_effect=[
Completion({'role': 'assistant', 'content': '', 'tool_calls': [quote, order]}, 'tool_calls'),
final_facts('fact_0', 'fact_1')]):
guest = frames(self.client.post('/stream_chat', json={
'msg': question, 'conv_id': self.cid, 'request_id': request_id}))
self.assertTrue(guest[0]['login_required'])
self.login()
with patch('logistics_chat.ai_client.post_completion', side_effect=ai_client.Timeout('test timeout')):
result = frames(self.client.post('/stream_chat', json={
'msg': question, 'conv_id': self.cid, 'request_id': request_id}))
self.assertIn('1100KG', result[0]['t'])
self.assertIn('1 票', result[0]['t'])
self.assertEqual(self.saved()[1]['intent']['weight'], '1100')
with self.app.app_context():
connection = db.connect()
import_snapshot(connection, dict(self.snapshot, source_sha256='8' * 64), activate=True)
connection.commit()
connection.close()
with patch('logistics_chat.ai_client.post_completion') as model:
expired = frames(self.client.post('/stream_chat', json={
'msg': question, 'conv_id': self.cid, 'request_id': request_id}))
self.assertIn('报价资料已更新', expired[0]['e'])
model.assert_not_called()
def test_legacy_rule_path_uses_the_same_replay_contract(self):
self.app.config['CHAT_USE_MODEL_TOOLS'] = False
self.start_quote()
original = self.add_weight()
before = self.saved()
repeated = self.add_weight()
self.assertEqual(repeated, original)
self.assertEqual(self.saved(), before)
def test_failed_state_commit_does_not_cache_a_partial_result(self):
from logistics_chat import db
with patch('logistics_chat.conversation_state.commit_state', return_value=False):
failed = self.start_quote()
self.assertIn('e', failed[0])
with self.app.app_context():
connection = db.connect()
self.assertEqual(connection.execute('SELECT COUNT(*) FROM chat_request_results').fetchone()[0], 0)
connection.close()
self.assertIn('1000KG', self.start_quote()[0]['t'])