fix(codex-bridge): close the gaps an independent review found
- One decision per real turn: a turn starts with a user message after the previous final answer; tool steps, messages typed mid-turn, a lone "." and compaction keep the combo. Works without prompt_cache_key too. - The virtual Otomatik name never reaches the gateway (stage off or error -> last choice or targets.zor); prefilter.target may not be it. - finishCheck: at most once per turn; a broken, empty or refused retry delivers the already approved first answer. - Layer window filter: only trusted windows, never the last layer. - Correction channel only on always-pass targets while checks are on; Sesli-Hizli-Zeki stays always-pass by default; a >256 KB SSE frame passes unfiltered instead of breaking the stream. - Turn-start questions wait in the local queue no longer than their timeout; decision-turn memory under a lock with refreshed order. - Dify fetch refuses redirects (the app key never moves host); the rules file write retries a Windows sharing violation. - An unreadable settings file no longer breaks every request. - TypeScript accepts only https:// URLs the bridge accepts; finishCheck.timeoutSeconds (unused) removed. Tests: 310 bridge tests, 36 electron tests; side-bridge live run for the turn, prefilter (incl. decision timeout) and finish-check paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c4ef18df0
commit
a7ba300448
10 files changed
+353
-91
No files matched your search
@@ -5,6 +5,7 @@ import copy
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import threading
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
@@ -205,10 +206,12 @@ class TurBasi(Ev):
|
||||
sahte_denetim.son_istek = jev_denetim.son_istek
|
||||
self.cevap = {'hedef': None, 'ret': None, 'puan': None}
|
||||
sahte_denetim.son_istek_sirasi = jev_denetim.son_istek_sirasi
|
||||
sahte_denetim.istek_metni = jev_denetim.istek_metni
|
||||
self.env = {'IDE_PREFIX': 'ide/', 'ON_KARAR': {}, 'ON_KARAR_SINIR': 3, 'jev_settings': jev_settings,
|
||||
'jev_denetim': sahte_denetim, 'model_gecis': model_gecis, 'hashlib': hashlib, 'time': time,
|
||||
'log': self.logs.append}
|
||||
exec(compile(ast.Module(body=[fonksiyon('karar_asamasi')], type_ignores=[]), 'kopru.py', 'exec'), self.env)
|
||||
'json': json, 'KARAR_KILIDI': threading.Lock(), 'BITIS_TURLARI': {}, 'log': self.logs.append}
|
||||
govde = [fonksiyon(a) for a in ('tur_bilgisi', 'karar_asamasi', 'bitis_kullan')]
|
||||
exec(compile(ast.Module(body=govde, type_ignores=[]), 'kopru.py', 'exec'), self.env)
|
||||
self.karar = self.env['karar_asamasi']
|
||||
|
||||
def on_karar(self, istek, oto, ret, key):
|
||||
@@ -220,8 +223,10 @@ class TurBasi(Ev):
|
||||
|
||||
def test_everything_off_changes_nothing_and_asks_nothing(self):
|
||||
self.save()
|
||||
req = self.istek('ide/Otomatik', user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Otomatik'), 'ide/Otomatik')
|
||||
for model in ('ide/Ajan', 'ide/codex-gpt-6-astra', 'ide/Chat-Model'):
|
||||
req = self.istek(model, user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', model), model)
|
||||
self.assertEqual(req['model'], model)
|
||||
self.assertEqual(self.kararlar, [])
|
||||
|
||||
def test_automatic_choice_is_kept_for_the_tool_steps_of_the_same_turn(self):
|
||||
@@ -235,7 +240,8 @@ class TurBasi(Ev):
|
||||
req2 = self.istek('ide/Otomatik', user('kaynaklari karsilastir'), {'type': 'function_call', 'call_id': 'c1'}, arac)
|
||||
self.assertEqual(self.karar(2, req2, 'sohbet-1', 'ide/Otomatik'), 'ide/Arastirma')
|
||||
self.assertEqual(len(self.kararlar), 1) # same turn: no second question
|
||||
req3 = self.istek('ide/Otomatik', user('kaynaklari karsilastir'), arac, user('simdi kodu duzelt'))
|
||||
son_cevap = {'type': 'message', 'role': 'assistant', 'content': [{'type': 'output_text', 'text': 'Rapor hazir.'}]}
|
||||
req3 = self.istek('ide/Otomatik', user('kaynaklari karsilastir'), arac, son_cevap, user('simdi kodu duzelt'))
|
||||
self.cevap = {'hedef': 'Ajan', 'ret': None, 'puan': {'zor': 0.9, 'arastirma': 0.1}}
|
||||
self.assertEqual(self.karar(3, req3, 'sohbet-1', 'ide/Otomatik'), 'ide/Ajan')
|
||||
self.assertEqual(len(self.kararlar), 2) # new user message: new decision
|
||||
@@ -272,9 +278,52 @@ class TurBasi(Ev):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
req = self.istek('ide/Otomatik', user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Otomatik'), 'ide/Otomatik')
|
||||
# The virtual name never leaves the bridge; nothing is asked while the main switch is off.
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Otomatik'), 'ide/' + self.settings['autoRoute']['targets']['zor'])
|
||||
self.assertEqual(self.kararlar, [])
|
||||
|
||||
def test_a_message_typed_mid_turn_or_a_dot_keeps_the_choice(self):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
self.cevap = {'hedef': 'Arastirma', 'ret': None, 'puan': {'zor': 0.2, 'arastirma': 0.9}}
|
||||
bas = user('kaynaklari karsilastir')
|
||||
cagri = {'type': 'function_call', 'call_id': 'c1', 'name': 'shell', 'arguments': '{}'}
|
||||
cikti = {'type': 'function_call_output', 'call_id': 'c1', 'output': 'ok'}
|
||||
self.assertEqual(self.karar(1, self.istek('ide/Otomatik', bas), 'sohbet-1', 'ide/Otomatik'), 'ide/Arastirma')
|
||||
self.cevap = {'hedef': 'Workflow-Kesif', 'ret': None, 'puan': {'zor': 0.1, 'arastirma': 0.1}}
|
||||
for ek in (user('.'), user('bir de sunu ekle'), {'type': 'compaction_trigger'}):
|
||||
req = self.istek('ide/Otomatik', bas, cagri, cikti, ek)
|
||||
self.assertEqual(self.karar(2, req, 'sohbet-1', 'ide/Otomatik'), 'ide/Arastirma', ek)
|
||||
self.assertEqual(len(self.kararlar), 1)
|
||||
|
||||
def test_a_disabled_automatic_model_never_reaches_the_gateway(self):
|
||||
self.save() # autoRoute off
|
||||
req = self.istek('ide/Otomatik', user('yap'))
|
||||
secilen = self.karar(1, req, 'sohbet-1', 'ide/Otomatik')
|
||||
self.assertEqual(secilen, 'ide/' + self.settings['autoRoute']['targets']['zor'])
|
||||
self.assertEqual(req['model'], secilen)
|
||||
self.assertEqual(self.kararlar, [])
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.settings['enabled'] = False # main switch off
|
||||
self.save()
|
||||
self.assertNotEqual(self.karar(2, self.istek('ide/Otomatik', user('yap')), 'sohbet-1', 'ide/Otomatik'), 'ide/Otomatik')
|
||||
|
||||
def test_without_a_conversation_key_the_tool_steps_still_keep_the_choice(self):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
self.cevap = {'hedef': 'Ajan', 'ret': None, 'puan': {'zor': 0.9, 'arastirma': 0.1}}
|
||||
bas = user('kodu duzelt')
|
||||
self.assertEqual(self.karar(1, {'model': 'ide/Otomatik', 'input': [bas]}, '', 'ide/Otomatik'), 'ide/Ajan')
|
||||
cikti = {'type': 'function_call_output', 'call_id': 'c1', 'output': 'ok'}
|
||||
req = {'model': 'ide/Otomatik', 'input': [bas, {'type': 'function_call', 'call_id': 'c1'}, cikti]}
|
||||
self.assertEqual(self.karar(2, req, '', 'ide/Otomatik'), 'ide/Ajan')
|
||||
self.assertEqual(len(self.kararlar), 1)
|
||||
|
||||
def test_the_finish_retry_is_used_once_per_turn(self):
|
||||
self.assertTrue(self.env['bitis_kullan']('tur-1'))
|
||||
self.assertFalse(self.env['bitis_kullan']('tur-1'))
|
||||
self.assertTrue(self.env['bitis_kullan']('tur-2'))
|
||||
|
||||
def test_turn_memory_is_bounded(self):
|
||||
self.settings['prefilter']['enabled'] = True
|
||||
self.save()
|
||||
@@ -285,15 +334,25 @@ class TurBasi(Ev):
|
||||
|
||||
|
||||
class KatmanPenceresi(unittest.TestCase):
|
||||
def test_a_layer_too_small_for_the_request_is_skipped(self):
|
||||
def suzgec(self, bilgi):
|
||||
logs = []
|
||||
env = {'IDE_PREFIX': 'ide/', 'IDE_BILGI': {'ide/Katman-2': {'pencere': 1000}, 'ide/Katman-3': {'pencere': 10 ** 6}},
|
||||
'pencere': pencere, 'log': logs.append}
|
||||
env = {'IDE_PREFIX': 'ide/', 'IDE_BILGI': bilgi, 'pencere': pencere, 'log': logs.append}
|
||||
exec(compile(ast.Module(body=[fonksiyon('sigan_katmanlar')], type_ignores=[]), 'kopru.py', 'exec'), env)
|
||||
return env['sigan_katmanlar'], logs
|
||||
|
||||
def test_a_layer_too_small_for_the_request_is_skipped(self):
|
||||
f, logs = self.suzgec({'ide/Katman-2': {'pencere': 1000, 'kaynak': 'katalog'}, 'ide/Katman-3': {'pencere': 10 ** 6}})
|
||||
req = {'input': [user('x' * 20000)]}
|
||||
self.assertEqual(env['sigan_katmanlar'](['Katman-2', 'Katman-3', 'Bilinmeyen'], req, '[1]'), ['Katman-3', 'Bilinmeyen'])
|
||||
self.assertEqual(f(['Katman-2', 'Katman-3', 'Bilinmeyen'], req, '[1]'), ['Katman-3', 'Bilinmeyen'])
|
||||
self.assertIn('Katman-2', logs[0])
|
||||
|
||||
def test_the_last_layer_and_untrusted_windows_are_never_skipped(self):
|
||||
req = {'input': [user('x' * 20000)]}
|
||||
f, _ = self.suzgec({'ide/Katman-2': {'pencere': 1000, 'kaynak': 'katalog'}, 'ide/Katman-3': {'pencere': 1000, 'kaynak': 'katalog'}})
|
||||
self.assertEqual(f(['Katman-2', 'Katman-3'], req, '[1]'), ['Katman-3']) # last one stays
|
||||
f, _ = self.suzgec({'ide/Katman-2': {'pencere': 1000, 'kaynak': 'varsayilan'}})
|
||||
self.assertEqual(f(['Katman-2', 'Katman-3'], req, '[1]'), ['Katman-2', 'Katman-3']) # placeholder window
|
||||
|
||||
|
||||
class BitisDenetimi(Ev):
|
||||
"""_denetimli_akis with finishCheck: an answer that stops early goes back to the same model once."""
|
||||
@@ -372,6 +431,17 @@ class BitisDenetimi(Ev):
|
||||
self.h._denetimli_akis(Yanit(sse('Bitti.')), '[1]', None, None, 'yap', [], Mock(), hedef='Ajan', devam=bozuk)
|
||||
self.assertIn(b'Bitti.', self.gonderilen())
|
||||
|
||||
def test_a_broken_or_refused_retry_delivers_the_approved_first_answer(self):
|
||||
for ikinci in (sse(), sse('Yapamam.')):
|
||||
with self.subTest(ikinci=ikinci[:40]):
|
||||
self.setUp()
|
||||
self.puanlar = [{'ret': 0.0, 'sart': 0.0, 'bitmedi': 0.9}, {'ret': 0.9, 'sart': 0.0}]
|
||||
ac = Mock(return_value=(Yanit(sse('katman')), None, '[K1]'))
|
||||
self.h._denetimli_akis(Yanit(sse('Ilk cevap.')), '[1]', None, None, 'yap', ['Katman-1'], ac,
|
||||
hedef='Ajan', devam=lambda ad, onceki: (Yanit(ikinci), None, '[devam]'))
|
||||
self.assertIn(b'Ilk cevap.', self.gonderilen())
|
||||
ac.assert_not_called()
|
||||
|
||||
def test_without_the_stage_the_finish_question_is_not_sent(self):
|
||||
govdeler = []
|
||||
|
||||
@@ -387,9 +457,6 @@ class BitisDenetimi(Ev):
|
||||
self.assertFalse(jev_denetim.bitmedi_mi(puan))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
class DogrulayiciElemesi(Ev):
|
||||
def test_off_by_default_and_asked_only_when_enabled(self):
|
||||
@@ -416,3 +483,68 @@ class DogrulayiciElemesi(Ev):
|
||||
import io as _io
|
||||
import karar_sor
|
||||
self.assertEqual(karar_sor.main(['baska'], _io.StringIO('{}'), _io.StringIO()), 2)
|
||||
|
||||
|
||||
class Inceleme(Ev):
|
||||
def test_the_correction_channel_needs_an_always_pass_target_while_checks_are_on(self):
|
||||
self.settings['watchModels'] = ['Chat-Model', 'Ajan']
|
||||
self.save()
|
||||
self.assertTrue(jev_denetim.izleme_mi('Chat-Model'))
|
||||
self.assertFalse(jev_denetim.izleme_mi('Ajan')) # checked target: no sideband
|
||||
self.settings['enabled'] = False
|
||||
self.save()
|
||||
self.assertTrue(jev_denetim.izleme_mi('Ajan'))
|
||||
|
||||
def test_a_turn_start_question_does_not_wait_in_the_queue_beyond_its_timeout(self):
|
||||
self.settings['maxConcurrent'] = 1
|
||||
self.settings['queueWaitSeconds'] = 3600
|
||||
self.save()
|
||||
s = jev_settings.read()[0]
|
||||
self.assertTrue(jev_settings.acquire(s)) # the only slot is taken
|
||||
try:
|
||||
t0 = time.monotonic()
|
||||
self.assertIsNone(jev_denetim.sorular({'user_request': 'x'}, {'zor': 'q'}, 'k', 1, gonder=lambda g, k: {}))
|
||||
self.assertLess(time.monotonic() - t0, 3)
|
||||
finally:
|
||||
jev_settings.release()
|
||||
|
||||
def test_the_prefilter_target_cannot_be_the_automatic_model(self):
|
||||
self.settings['prefilter']['target'] = self.settings['autoRoute']['model']
|
||||
self.save()
|
||||
with self.assertRaises(ValueError):
|
||||
jev_settings.read()
|
||||
|
||||
def test_dify_redirects_are_refused_so_the_key_never_moves(self):
|
||||
import urllib.error
|
||||
with self.assertRaises(urllib.error.HTTPError):
|
||||
karar_politikasi._YonlendirmeYok().redirect_request(
|
||||
Mock(full_url='https://dify.example/v1/workflows/run'), None, 302, 'Found', {}, 'http://evil.example/')
|
||||
|
||||
|
||||
class IzlemeSuzgeci(unittest.TestCase):
|
||||
def test_a_huge_frame_passes_through_instead_of_breaking_the_stream(self):
|
||||
tree_sinif = next(n for n in TREE.body if isinstance(n, ast.ClassDef) and n.name == 'WatchResponse')
|
||||
from bekci_watch import MetadataFilter
|
||||
logs = []
|
||||
env = {'MetadataFilter': MetadataFilter, 'log': logs.append}
|
||||
exec(compile(ast.Module(body=[tree_sinif], type_ignores=[]), 'kopru.py', 'exec'), env)
|
||||
buyuk = b'data: ' + b'x' * 300000
|
||||
parcalar = [buyuk[:200000], buyuk[200000:], b'\n\n', b'']
|
||||
|
||||
class Ham:
|
||||
def read1(self, n):
|
||||
return parcalar.pop(0)
|
||||
w = env['WatchResponse'](Ham(), lambda m: None)
|
||||
cikti = b''
|
||||
while True:
|
||||
b = w.read1(65536)
|
||||
if not b and w.done or not parcalar and not b:
|
||||
break
|
||||
cikti += b
|
||||
self.assertIn(b'x' * 1000, cikti)
|
||||
self.assertEqual(cikti.count(b'x'), 300000)
|
||||
self.assertTrue(logs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user