Separate encrypted reasoning from actual compaction, stop migration after native return, and retain full history and recent exchanges. Add typed settings, Gemini fallback, verified provider windows, bounded summaries and durable concurrent checkpoints. Validation: 245 bridge tests plus focused concurrency tests; 71 settings/connection tests; 66 supplementary tests; TypeScript checks; renderer and Electron bundles; real primary/native/fallback summary probes and 700K input acceptance.
171 lines
9.5 KiB
Python
171 lines
9.5 KiB
Python
import copy
|
||
import json
|
||
import tempfile
|
||
import threading
|
||
import io
|
||
import time
|
||
from types import SimpleNamespace
|
||
import unittest
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
from unittest.mock import patch
|
||
|
||
import compression_engine as engine
|
||
import compression_settings as cs
|
||
import gecis_context as gc
|
||
|
||
|
||
class Compression(unittest.TestCase):
|
||
def setUp(self):
|
||
self.temp = tempfile.TemporaryDirectory()
|
||
self.store = cs.Checkpoints(self.temp.name)
|
||
self.settings = dict(cs.DEFAULTS, keepTurns=2, primaryModel='ide/primary', fallbackModel='ide/backup')
|
||
self.req = {'model': 'ide/target', 'prompt_cache_key': 'same-chat', 'input': [
|
||
{'role': 'user', 'content': 'DECISION-4827 ' + 'x' * 60000},
|
||
{'role': 'assistant', 'content': 'Done'},
|
||
{'role': 'user', 'content': 'Recent one'}, {'role': 'assistant', 'content': 'Latest answer'},
|
||
{'role': 'user', 'content': 'Recent two'}]}
|
||
self.summary = 'Amaç: DECISION-4827. Kararlar: preserve. Kısıtlar: same ID. Tamamlanan işler: done. Kalan adımlar: test. '
|
||
|
||
def tearDown(self):
|
||
self.temp.cleanup()
|
||
|
||
def test_strict_retention_never_shrinks_ten_turns_to_one(self):
|
||
req = copy.deepcopy(self.req)
|
||
settings = dict(self.settings, keepTurns=10)
|
||
with self.assertRaises(gc.TransferError):
|
||
gc.Preparer().prepare(req, 32768, lambda *_: self.fail('Must not summarize protected exchanges'), settings=settings)
|
||
self.assertEqual(req, self.req)
|
||
|
||
def test_percent_token_and_output_reserve_budgets(self):
|
||
for mode, expected in [('percent', 70000), ('tokens', 64000)]:
|
||
self.assertEqual(gc.input_budget({}, 100000, dict(self.settings, budgetMode=mode)), expected)
|
||
self.assertEqual(gc.input_budget({}, 100000, self.settings, automatic=True), 50000)
|
||
self.assertEqual(gc.input_budget({'max_output_tokens': 80000}, 100000, self.settings), 20000)
|
||
|
||
def test_boundary_equal_does_not_summarize(self):
|
||
size = gc.tokens(self.req)
|
||
settings = dict(self.settings, budgetMode='tokens', maxTokens=size)
|
||
self.assertEqual(gc.Preparer().prepare(self.req, 100000, lambda *_: self.fail('At boundary'), settings=settings), self.req)
|
||
|
||
def test_checkpoint_survives_restart_and_appended_message(self):
|
||
first = gc.Preparer().prepare(self.req, 32768, lambda *_: self.summary, 'account-chat', settings=self.settings, checkpoints=self.store)
|
||
later = copy.deepcopy(self.req)
|
||
later['input'].extend([{'role':'assistant','content':'new answer'}, {'role':'user','content':'continue'}])
|
||
second = gc.Preparer().prepare(later, 32768, lambda *_: self.fail('Should reuse saved prefix'), 'account-chat', settings=self.settings, checkpoints=cs.Checkpoints(self.temp.name))
|
||
self.assertEqual(second['prompt_cache_key'], 'same-chat')
|
||
self.assertIn('DECISION-4827', json.dumps(second))
|
||
self.assertEqual(second['input'][-1], later['input'][-1])
|
||
self.assertLessEqual(gc.tokens(first), gc.input_budget(self.req, 32768, self.settings))
|
||
|
||
def test_failed_or_oversized_summary_never_commits(self):
|
||
for text in ['', 'x' * 60000]:
|
||
with self.assertRaises(gc.TransferError):
|
||
gc.Preparer().prepare(self.req, 32768, lambda *_: text, 'scope', settings=self.settings, checkpoints=self.store)
|
||
old = gc.split_history(self.req['input'], 2)[1]
|
||
self.assertIsNone(self.store.get('scope', old, self.settings))
|
||
|
||
def test_scopes_and_changed_sources_never_share_summary(self):
|
||
old = gc.split_history(self.req['input'], 2)[1]
|
||
self.store.save('a', old, self.settings, self.summary)
|
||
self.assertIsNone(self.store.get('b', old, self.settings))
|
||
changed = copy.deepcopy(old); changed[0]['content'] = 'different'
|
||
self.assertIsNone(self.store.get('a', changed, self.settings))
|
||
|
||
def test_restore_checks_scope_and_preserves_all_versions(self):
|
||
old = [{'role':'user','content':'decision'}]
|
||
self.store.save('a', old, self.settings, 'version one')
|
||
self.store.save('a', old, self.settings, 'version two')
|
||
self.store.restore('a', 1)
|
||
self.assertEqual(self.store.get('a', old, self.settings)[1], 'version one')
|
||
with self.assertRaises(ValueError): self.store.restore('b', 1)
|
||
|
||
def test_duplicate_requests_coalesce(self):
|
||
entered, release = threading.Event(), threading.Event()
|
||
count = []
|
||
def run():
|
||
count.append(1); entered.set(); release.wait(5)
|
||
return {'value':'same'}
|
||
with ThreadPoolExecutor(2) as pool:
|
||
one = pool.submit(self.store.coalesce, 'same-key', run)
|
||
self.assertTrue(entered.wait(2))
|
||
two = pool.submit(self.store.coalesce, 'same-key', run)
|
||
# Synchronize entry under the store lock before releasing owner.
|
||
threading.Event().wait(.05)
|
||
release.set()
|
||
self.assertEqual(one.result(), two.result())
|
||
self.assertEqual(len(count), 1)
|
||
|
||
def test_fallback_sequence_and_unverified_million_rejected(self):
|
||
class Handler:
|
||
def _transfer_window(self, slug): return 1000000 if slug == 'ide/backup' else 128000
|
||
calls = []
|
||
def mock_call(_h,_m,slug,*args):
|
||
calls.append(slug)
|
||
if slug == 'ide/primary': raise TimeoutError()
|
||
return self.summary
|
||
with patch.object(engine, 'call', side_effect=mock_call):
|
||
self.assertEqual(engine.summarize(Handler(), None, '[]', 500, self.settings, 'ide/current'), self.summary)
|
||
self.assertEqual(calls, ['ide/primary', 'ide/backup'])
|
||
calls.clear()
|
||
with patch.object(engine, 'call', side_effect=mock_call):
|
||
engine.summarize(Handler(), None, '[]', 500, dict(self.settings, fallbackModel='ide/unverified'), 'ide/current')
|
||
self.assertEqual(calls, ['ide/primary', 'ide/current'])
|
||
|
||
def test_tool_pairs_cannot_be_split_between_chunks(self):
|
||
items = [{'type':'function_call','call_id':'a','arguments':'x'*500}, {'role':'user','content':'interleaved'}, {'type':'function_call_output','call_id':'a','output':'result'}]
|
||
grouped = engine.chunks(gc.encoded(items), 2000)
|
||
self.assertEqual(len(grouped), 1)
|
||
with self.assertRaises(gc.TransferError): engine.chunks(gc.encoded(items), 100)
|
||
|
||
def test_invalid_settings_fail_without_defaults(self):
|
||
for patch_value in [{'keepTurns':0}, {'targetPercent':70}, {'timeoutSeconds':0}, {'autoEnabled':'yes'}]:
|
||
with self.assertRaises(ValueError): cs.validate(dict(self.settings, **patch_value))
|
||
|
||
def test_structured_unicode_messages_and_reduce_text_split_without_loss(self):
|
||
content = 'Karar 🧭 ' * 3000
|
||
item = {'role':'user', 'content':[{'type':'input_text', 'text':content}]}
|
||
parts = [x for chunk in engine.chunks(gc.encoded([item]), 2000) for x in json.loads(chunk)]
|
||
self.assertEqual(''.join(x['content'][0]['text'] for x in parts), content)
|
||
self.assertTrue(all(gc.tokens([x]) <= 2000 for x in parts))
|
||
reduced = [x for chunk in engine.chunks(gc.encoded([content]), 2000) for x in json.loads(chunk)]
|
||
self.assertEqual(''.join(reduced), content)
|
||
|
||
def test_late_older_checkpoint_cannot_replace_newer_prefix(self):
|
||
old = [{'role':'user', 'content':'old'}]
|
||
newer = old + [{'role':'assistant', 'content':'new'}]
|
||
self.store.save('scope', newer, self.settings, 'new snapshot')
|
||
self.store.save('scope', old, self.settings, 'late old snapshot')
|
||
self.assertEqual(self.store.get('scope', newer, self.settings), (2, 'new snapshot'))
|
||
|
||
def test_late_summary_cannot_replace_edited_history_of_same_length(self):
|
||
old = [{'role':'user', 'content':'old'}]
|
||
edited = [{'role':'user', 'content':'edited'}]
|
||
self.store.begin('scope', old, self.settings)
|
||
self.store.begin('scope', edited, self.settings)
|
||
self.store.save('scope', edited, self.settings, 'edited snapshot')
|
||
self.store.save('scope', old, self.settings, 'late old snapshot')
|
||
self.assertEqual(self.store.get('scope', edited, self.settings), (1, 'edited snapshot'))
|
||
|
||
def test_tool_free_high_summary_reserves_thinking_and_requires_completion(self):
|
||
module = SimpleNamespace(ek_model_mu=lambda _: True,
|
||
MODEL_MAP={'ide/primary':('route', 'gemini-3.8-flash-high')},
|
||
ROUTES={'route':{'url':'https://example.invalid/responses', 'key':'synthetic'}}, UA='test')
|
||
handler = SimpleNamespace(_transfer_window=lambda _: 1000000)
|
||
event = {'type':'response.completed', 'response':{'output':[{'type':'message','content':[{'type':'output_text','text':self.summary}]}]}}
|
||
bodies = []
|
||
def response(request, **_kwargs):
|
||
bodies.append(json.loads(request.data))
|
||
return io.BytesIO(('data: '+json.dumps(event)+'\n\n').encode())
|
||
with patch.object(engine.urllib.request, 'urlopen', side_effect=response):
|
||
result = engine.call(handler,module,'ide/primary','["history"]',1024,self.settings,time.monotonic()+5)
|
||
self.assertEqual(result.strip(), self.summary.strip())
|
||
self.assertEqual(bodies[0]['tools'], [])
|
||
self.assertEqual(bodies[0]['tool_choice'], 'none')
|
||
self.assertEqual(bodies[0]['max_output_tokens'], 25600)
|
||
event['type'] = 'response.incomplete'
|
||
with patch.object(engine.urllib.request, 'urlopen', side_effect=response), self.assertRaises(gc.TransferError):
|
||
engine.call(handler,module,'ide/primary','["history"]',1024,self.settings,time.monotonic()+5)
|
||
|
||
|
||
if __name__ == '__main__': unittest.main()
|