comparison mrjunejune/inference/copilot_sidecar_test.py @ 265:056790c4fb0d

add role-aware Epi assistant prompts Add verified June knowledge, guest/member/admin Copilot profiles, profile-isolated session recovery, animated Epi greetings, and a single authoritative runtime config workflow for inference. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 10:50:30 -0700
parents 1f9877b637e9
children
comparison
equal deleted inserted replaced
264:04fee26ecce0 265:056790c4fb0d
1 import asyncio 1 import asyncio
2 import hashlib
2 import types 3 import types
3 import unittest 4 import unittest
5 import uuid
4 from dataclasses import replace 6 from dataclasses import replace
5 7
6 from mrjunejune.inference.copilot_sidecar import Sidecar, SidecarConfig 8 from mrjunejune.inference.copilot_sidecar import (
9 CompiledProfile,
10 Sidecar,
11 SidecarConfig,
12 _derive_sdk_session_id,
13 _SDK_SESSION_NAMESPACE,
14 _validate_history,
15 _HISTORY_MAX_ENTRIES,
16 _HISTORY_MAX_BYTES,
17 )
7 18
8 19
9 def event(event_type, **data): 20 def event(event_type, **data):
10 return types.SimpleNamespace( 21 return types.SimpleNamespace(
11 type=types.SimpleNamespace(value=event_type), 22 type=types.SimpleNamespace(value=event_type),
12 data=types.SimpleNamespace(**data), 23 data=types.SimpleNamespace(**data),
13 ) 24 )
25
26
27 # ---------------------------------------------------------------------------
28 # Fake compiled-profile infrastructure
29 # ---------------------------------------------------------------------------
30
31 def _make_content(profile: str) -> str:
32 return f"You are the {profile} assistant. Common rules apply."
33
34
35 _FAKE_PROFILES: dict = {
36 profile: {
37 "content": _make_content(profile),
38 "version": 1,
39 "hash": hashlib.sha256(_make_content(profile).encode()).hexdigest(),
40 }
41 for profile in ("public_visitor", "invited_friend", "june_admin")
42 }
43
44
45 def _fake_compile_fn(profile: str) -> dict:
46 if profile not in _FAKE_PROFILES:
47 raise ValueError(f"Unknown profile: {profile!r}")
48 return _FAKE_PROFILES[profile]
49
50
51 def _fake_sdk_id(conversation_id: str, profile: str) -> str:
52 """Compute the deterministic SDK session ID for use in test assertions."""
53 p = _FAKE_PROFILES[profile]
54 compiled = CompiledProfile(
55 profile=profile,
56 content=p["content"],
57 prompt_version=1,
58 knowledge_version=p["version"],
59 hash=p["hash"],
60 )
61 return _derive_sdk_session_id(conversation_id, compiled)
14 62
15 63
16 class FakeSession: 64 class FakeSession:
17 def __init__(self, session_id, behavior="complete"): 65 def __init__(self, session_id, behavior="complete"):
18 self.session_id = session_id 66 self.session_id = session_id
140 model="test-model", 188 model="test-model",
141 wire_api="responses", 189 wire_api="responses",
142 base_directory="/not-used-by-fake", 190 base_directory="/not-used-by-fake",
143 ) 191 )
144 192
145 async def make_sidecar(self, behavior="complete"): 193 async def make_sidecar(self, behavior="complete", compile_fn=_fake_compile_fn):
146 client = FakeClient(behavior) 194 client = FakeClient(behavior)
147 sidecar = Sidecar(client, self.config, self.capture) 195 sidecar = Sidecar(client, self.config, self.capture, compile_fn=compile_fn)
148 await sidecar.start() 196 await sidecar.start()
149 return sidecar, client 197 return sidecar, client
150 198
151 async def test_health_and_invalid_protocol(self): 199 async def test_health_and_invalid_protocol(self):
152 sidecar, _ = await self.make_sidecar() 200 sidecar, _ = await self.make_sidecar()
169 { 217 {
170 "command": "turn.start", 218 "command": "turn.start",
171 "request_id": request_id, 219 "request_id": request_id,
172 "conversation_id": "conversation-a", 220 "conversation_id": "conversation-a",
173 "prompt": prompt, 221 "prompt": prompt,
222 "prompt_profile": "public_visitor",
223 "prompt_version": 1,
224 "knowledge_version": 1,
174 } 225 }
175 ) 226 )
176 await sidecar.drain_events() 227 await sidecar.drain_events()
177 228
178 self.assertEqual(len(client.create_calls), 1) 229 self.assertEqual(len(client.create_calls), 1)
179 self.assertEqual(len(client.resume_calls), 1) 230 self.assertEqual(len(client.resume_calls), 1)
180 session = client.sessions["conversation-a"] 231 sdk_id = _fake_sdk_id("conversation-a", "public_visitor")
232 session = client.sessions[sdk_id]
181 self.assertEqual(session.prompts, ["first", "second"]) 233 self.assertEqual(session.prompts, ["first", "second"])
182 options = client.create_calls[0][1] 234 options = client.create_calls[0][1]
183 self.assertEqual(options["provider"]["type"], "openai") 235 self.assertEqual(options["provider"]["type"], "openai")
184 self.assertEqual( 236 self.assertEqual(
185 options["provider"]["base_url"], "http://litellm.invalid/v1" 237 options["provider"]["base_url"], "http://litellm.invalid/v1"
200 { 252 {
201 "command": "turn.start", 253 "command": "turn.start",
202 "request_id": "left-request", 254 "request_id": "left-request",
203 "conversation_id": "left", 255 "conversation_id": "left",
204 "prompt": "left", 256 "prompt": "left",
257 "prompt_profile": "public_visitor",
258 "prompt_version": 1,
259 "knowledge_version": 1,
205 } 260 }
206 ), 261 ),
207 sidecar.dispatch( 262 sidecar.dispatch(
208 { 263 {
209 "command": "turn.start", 264 "command": "turn.start",
210 "request_id": "right-request", 265 "request_id": "right-request",
211 "conversation_id": "right", 266 "conversation_id": "right",
212 "prompt": "right", 267 "prompt": "right",
268 "prompt_profile": "public_visitor",
269 "prompt_version": 1,
270 "knowledge_version": 1,
213 } 271 }
214 ), 272 ),
215 ) 273 )
216 await sidecar.drain_events() 274 await sidecar.drain_events()
217 275
231 { 289 {
232 "command": "turn.start", 290 "command": "turn.start",
233 "request_id": "turn-request", 291 "request_id": "turn-request",
234 "conversation_id": "abort-me", 292 "conversation_id": "abort-me",
235 "prompt": "wait", 293 "prompt": "wait",
294 "prompt_profile": "public_visitor",
295 "prompt_version": 1,
296 "knowledge_version": 1,
236 } 297 }
237 ) 298 )
238 await sidecar.dispatch( 299 await sidecar.dispatch(
239 { 300 {
240 "command": "turn.abort", 301 "command": "turn.abort",
242 "conversation_id": "abort-me", 303 "conversation_id": "abort-me",
243 } 304 }
244 ) 305 )
245 await sidecar.drain_events() 306 await sidecar.drain_events()
246 307
247 self.assertEqual(client.sessions["abort-me"].abort_calls, 1) 308 self.assertEqual(client.sessions[_fake_sdk_id("abort-me", "public_visitor")].abort_calls, 1)
248 done = [ 309 done = [
249 item 310 item
250 for item in self.output 311 for item in self.output
251 if item["type"] == "turn.done" 312 if item["type"] == "turn.done"
252 and item["request_id"] == "turn-request" 313 and item["request_id"] == "turn-request"
266 { 327 {
267 "command": "turn.start", 328 "command": "turn.start",
268 "request_id": "error-request", 329 "request_id": "error-request",
269 "conversation_id": "error-conversation", 330 "conversation_id": "error-conversation",
270 "prompt": "fail", 331 "prompt": "fail",
332 "prompt_profile": "public_visitor",
333 "prompt_version": 1,
334 "knowledge_version": 1,
271 } 335 }
272 ) 336 )
273 await sidecar.drain_events() 337 await sidecar.drain_events()
274 338
275 errors = [item for item in self.output if item["type"] == "turn.error"] 339 errors = [item for item in self.output if item["type"] == "turn.error"]
285 { 349 {
286 "command": "turn.start", 350 "command": "turn.start",
287 "request_id": "active-request", 351 "request_id": "active-request",
288 "conversation_id": "abort-error", 352 "conversation_id": "abort-error",
289 "prompt": "wait", 353 "prompt": "wait",
354 "prompt_profile": "public_visitor",
355 "prompt_version": 1,
356 "knowledge_version": 1,
290 } 357 }
291 ) 358 )
292 await sidecar.dispatch( 359 await sidecar.dispatch(
293 { 360 {
294 "command": "turn.abort", 361 "command": "turn.abort",
312 { 379 {
313 "command": "turn.start", 380 "command": "turn.start",
314 "request_id": "active", 381 "request_id": "active",
315 "conversation_id": "delete-me", 382 "conversation_id": "delete-me",
316 "prompt": "wait", 383 "prompt": "wait",
317 } 384 "prompt_profile": "public_visitor",
318 ) 385 "prompt_version": 1,
319 session = client.sessions["delete-me"] 386 "knowledge_version": 1,
387 }
388 )
389 _delete_sdk_id = _fake_sdk_id("delete-me", "public_visitor")
390 session = client.sessions[_delete_sdk_id]
320 await sidecar.dispatch( 391 await sidecar.dispatch(
321 { 392 {
322 "command": "conversation.delete", 393 "command": "conversation.delete",
323 "request_id": "delete-request", 394 "request_id": "delete-request",
324 "conversation_id": "delete-me", 395 "conversation_id": "delete-me",
331 "conversation_id": None, 402 "conversation_id": None,
332 } 403 }
333 ) 404 )
334 405
335 self.assertEqual(session.disconnect_calls, 1) 406 self.assertEqual(session.disconnect_calls, 1)
336 self.assertEqual(client.delete_calls, ["delete-me"]) 407 self.assertEqual(client.delete_calls[0], _delete_sdk_id)
408 self.assertEqual(
409 set(client.delete_calls),
410 {
411 _fake_sdk_id("delete-me", "public_visitor"),
412 _fake_sdk_id("delete-me", "invited_friend"),
413 _fake_sdk_id("delete-me", "june_admin"),
414 },
415 )
337 self.assertTrue(client.stopped) 416 self.assertTrue(client.stopped)
338 self.assertTrue(sidecar.shutting_down) 417 self.assertTrue(sidecar.shutting_down)
339 shutdown = [ 418 shutdown = [
340 item 419 item
341 for item in self.output 420 for item in self.output
355 { 434 {
356 "command": "turn.start", 435 "command": "turn.start",
357 "request_id": f"request-{conversation_id}", 436 "request_id": f"request-{conversation_id}",
358 "conversation_id": conversation_id, 437 "conversation_id": conversation_id,
359 "prompt": conversation_id, 438 "prompt": conversation_id,
439 "prompt_profile": "public_visitor",
440 "prompt_version": 1,
441 "knowledge_version": 1,
360 } 442 }
361 ) 443 )
362 await sidecar.drain_events() 444 await sidecar.drain_events()
363 await asyncio.sleep(0.01) 445 await asyncio.sleep(0.01)
364 446
365 old_session = client.sessions["old"] 447 old_session = client.sessions[_fake_sdk_id("old", "public_visitor")]
366 old_session.disconnect_error = True 448 old_session.disconnect_error = True
367 await sidecar.evict_idle_sessions() 449 await sidecar.evict_idle_sessions()
368 self.assertEqual(old_session.disconnect_calls, 1) 450 self.assertEqual(old_session.disconnect_calls, 1)
369 self.assertNotIn("old", sidecar._conversations) 451 self.assertNotIn("old", sidecar._conversations)
370 self.assertIn("new", sidecar._conversations) 452 self.assertIn("new", sidecar._conversations)
371 453
372 sidecar._conversations["new"].last_used -= 4000 454 sidecar._conversations["new"].last_used -= 4000
373 await sidecar.evict_idle_sessions() 455 await sidecar.evict_idle_sessions()
374 self.assertEqual(client.sessions["new"].disconnect_calls, 1) 456 self.assertEqual(client.sessions[_fake_sdk_id("new", "public_visitor")].disconnect_calls, 1)
375 self.assertEqual(sidecar._conversations, {}) 457 self.assertEqual(sidecar._conversations, {})
376 await sidecar.dispatch( 458 await sidecar.dispatch(
377 { 459 {
378 "command": "shutdown", 460 "command": "shutdown",
379 "request_id": "shutdown-eviction", 461 "request_id": "shutdown-eviction",
383 465
384 async def test_shutdown_waits_for_inflight_session_creation(self): 466 async def test_shutdown_waits_for_inflight_session_creation(self):
385 client = FakeClient("pending") 467 client = FakeClient("pending")
386 client.resume_started = asyncio.Event() 468 client.resume_started = asyncio.Event()
387 client.resume_release = asyncio.Event() 469 client.resume_release = asyncio.Event()
388 sidecar = Sidecar(client, self.config, self.capture) 470 sidecar = Sidecar(client, self.config, self.capture, compile_fn=_fake_compile_fn)
389 await sidecar.start() 471 await sidecar.start()
390 472
391 start_task = asyncio.create_task( 473 start_task = asyncio.create_task(
392 sidecar.dispatch( 474 sidecar.dispatch(
393 { 475 {
394 "command": "turn.start", 476 "command": "turn.start",
395 "request_id": "starting", 477 "request_id": "starting",
396 "conversation_id": "race", 478 "conversation_id": "race",
397 "prompt": "wait", 479 "prompt": "wait",
480 "prompt_profile": "public_visitor",
481 "prompt_version": 1,
482 "knowledge_version": 1,
398 } 483 }
399 ) 484 )
400 ) 485 )
401 await client.resume_started.wait() 486 await client.resume_started.wait()
402 shutdown_task = asyncio.create_task( 487 shutdown_task = asyncio.create_task(
412 self.assertTrue(sidecar.shutting_down) 497 self.assertTrue(sidecar.shutting_down)
413 client.resume_release.set() 498 client.resume_release.set()
414 await asyncio.gather(start_task, shutdown_task) 499 await asyncio.gather(start_task, shutdown_task)
415 500
416 self.assertTrue(client.stopped) 501 self.assertTrue(client.stopped)
417 self.assertEqual(client.sessions["race"].disconnect_calls, 1) 502 self.assertEqual(client.sessions[_fake_sdk_id("race", "public_visitor")].disconnect_calls, 1)
418 await sidecar.dispatch( 503 await sidecar.dispatch(
419 { 504 {
420 "command": "turn.start", 505 "command": "turn.start",
421 "request_id": "too-late", 506 "request_id": "too-late",
422 "conversation_id": "late", 507 "conversation_id": "late",
423 "prompt": "no", 508 "prompt": "no",
509 "prompt_profile": "public_visitor",
510 "prompt_version": 1,
511 "knowledge_version": 1,
424 } 512 }
425 ) 513 )
426 late_error = [ 514 late_error = [
427 item 515 item
428 for item in self.output 516 for item in self.output
440 "conversation_id": f"conversation-{index}", 528 "conversation_id": f"conversation-{index}",
441 } 529 }
442 ) 530 )
443 self.assertEqual(sidecar._conversation_gates, {}) 531 self.assertEqual(sidecar._conversation_gates, {})
444 532
533 # ------------------------------------------------------------------
534 # New: profile-aware tests
535 # ------------------------------------------------------------------
536
537 async def test_three_distinct_profiles_produce_distinct_append_prompts(self):
538 sidecar, client = await self.make_sidecar()
539 for profile in ("public_visitor", "invited_friend", "june_admin"):
540 await sidecar.dispatch(
541 {
542 "command": "turn.start",
543 "request_id": f"req-{profile}",
544 "conversation_id": f"conv-{profile}",
545 "prompt": "hello",
546 "prompt_profile": profile,
547 "prompt_version": 1,
548 "knowledge_version": 1,
549 }
550 )
551 await sidecar.drain_events()
552
553 contents = [opts["system_message"]["content"] for _, opts in client.create_calls]
554 self.assertEqual(len(contents), 3)
555 self.assertEqual(len(set(contents)), 3, "all three profiles must produce distinct content")
556 for _, opts in client.create_calls:
557 self.assertEqual(opts["system_message"]["mode"], "append")
558
559 async def test_session_options_memory_disabled_and_no_tools(self):
560 sidecar, client = await self.make_sidecar()
561 await sidecar.dispatch(
562 {
563 "command": "turn.start",
564 "request_id": "r-opts",
565 "conversation_id": "conv-opts",
566 "prompt": "test",
567 "prompt_profile": "public_visitor",
568 "prompt_version": 1,
569 "knowledge_version": 1,
570 }
571 )
572 await sidecar.drain_events()
573 _, opts = client.create_calls[0]
574 self.assertEqual(opts["memory"], {"enabled": False})
575 self.assertEqual(opts["tools"], [])
576 self.assertEqual(opts["available_tools"], [])
577 self.assertEqual(opts["mcp_servers"], {})
578 self.assertTrue(opts["enable_session_store"])
579
580 async def test_missing_profile_rejected_before_client_calls(self):
581 sidecar, client = await self.make_sidecar()
582 await sidecar.dispatch(
583 {
584 "command": "turn.start",
585 "request_id": "r-missing",
586 "conversation_id": "conv-missing",
587 "prompt": "hello",
588 }
589 )
590 errors = [e for e in self.output if e["type"] == "turn.error"]
591 self.assertEqual(errors[0]["error"]["code"], "invalid_prompt_profile")
592 done = [e for e in self.output if e["type"] == "turn.done"]
593 self.assertTrue(done[0]["failed"])
594 self.assertEqual(len(client.create_calls), 0)
595 self.assertEqual(len(client.resume_calls), 0)
596
597 async def test_unknown_profile_rejected_before_client_calls(self):
598 sidecar, client = await self.make_sidecar()
599 await sidecar.dispatch(
600 {
601 "command": "turn.start",
602 "request_id": "r-unknown",
603 "conversation_id": "conv-unknown",
604 "prompt": "hello",
605 "prompt_profile": "hacker",
606 "prompt_version": 1,
607 "knowledge_version": 1,
608 }
609 )
610 errors = [e for e in self.output if e["type"] == "turn.error"]
611 self.assertEqual(errors[0]["error"]["code"], "invalid_prompt_profile")
612 self.assertEqual(len(client.create_calls), 0)
613
614 async def test_stale_prompt_version_rejected_before_client_calls(self):
615 sidecar, client = await self.make_sidecar()
616 await sidecar.dispatch(
617 {
618 "command": "turn.start",
619 "request_id": "r-stale-pv",
620 "conversation_id": "conv-stale-pv",
621 "prompt": "hello",
622 "prompt_profile": "public_visitor",
623 "prompt_version": 999,
624 "knowledge_version": 1,
625 }
626 )
627 errors = [e for e in self.output if e["type"] == "turn.error"]
628 self.assertEqual(errors[0]["error"]["code"], "invalid_prompt_profile")
629 self.assertEqual(len(client.create_calls), 0)
630
631 async def test_stale_knowledge_version_rejected_before_client_calls(self):
632 sidecar, client = await self.make_sidecar()
633 await sidecar.dispatch(
634 {
635 "command": "turn.start",
636 "request_id": "r-stale-kv",
637 "conversation_id": "conv-stale-kv",
638 "prompt": "hello",
639 "prompt_profile": "public_visitor",
640 "prompt_version": 1,
641 "knowledge_version": 999,
642 }
643 )
644 errors = [e for e in self.output if e["type"] == "turn.error"]
645 self.assertEqual(errors[0]["error"]["code"], "invalid_prompt_profile")
646 self.assertEqual(len(client.create_calls), 0)
647
648 async def test_bool_prompt_version_rejected_before_client_calls(self):
649 sidecar, client = await self.make_sidecar()
650 await sidecar.dispatch(
651 {
652 "command": "turn.start",
653 "request_id": "r-bool-pv",
654 "conversation_id": "conv-bool-pv",
655 "prompt": "hello",
656 "prompt_profile": "public_visitor",
657 "prompt_version": True,
658 "knowledge_version": 1,
659 }
660 )
661 errors = [e for e in self.output if e["type"] == "turn.error"]
662 self.assertEqual(errors[0]["error"]["code"], "invalid_prompt_profile")
663 self.assertEqual(len(client.create_calls), 0)
664
665 async def test_bool_knowledge_version_rejected_before_client_calls(self):
666 sidecar, client = await self.make_sidecar()
667 await sidecar.dispatch(
668 {
669 "command": "turn.start",
670 "request_id": "r-bool-kv",
671 "conversation_id": "conv-bool-kv",
672 "prompt": "hello",
673 "prompt_profile": "public_visitor",
674 "prompt_version": 1,
675 "knowledge_version": True,
676 }
677 )
678 errors = [e for e in self.output if e["type"] == "turn.error"]
679 self.assertEqual(errors[0]["error"]["code"], "invalid_prompt_profile")
680 self.assertEqual(len(client.create_calls), 0)
681
682 async def test_same_profile_reuses_existing_session(self):
683 sidecar, client = await self.make_sidecar()
684 for i in range(3):
685 await sidecar.dispatch(
686 {
687 "command": "turn.start",
688 "request_id": f"r-reuse-{i}",
689 "conversation_id": "conv-reuse",
690 "prompt": f"message {i}",
691 "prompt_profile": "public_visitor",
692 "prompt_version": 1,
693 "knowledge_version": 1,
694 }
695 )
696 await sidecar.drain_events()
697
698 self.assertEqual(len(client.create_calls), 1)
699 self.assertEqual(len(client.resume_calls), 1)
700 session = client.sessions[_fake_sdk_id("conv-reuse", "public_visitor")]
701 self.assertEqual(session.prompts, ["message 0", "message 1", "message 2"])
702
703 async def test_profile_switch_disconnects_and_resumes_with_new_prompt(self):
704 sidecar, client = await self.make_sidecar()
705
706 await sidecar.dispatch(
707 {
708 "command": "turn.start",
709 "request_id": "r-switch-1",
710 "conversation_id": "conv-switch",
711 "prompt": "first",
712 "prompt_profile": "public_visitor",
713 "prompt_version": 1,
714 "knowledge_version": 1,
715 }
716 )
717 await sidecar.drain_events()
718 visitor_sdk_id = _fake_sdk_id("conv-switch", "public_visitor")
719 friend_sdk_id = _fake_sdk_id("conv-switch", "invited_friend")
720 first_session = client.sessions[visitor_sdk_id]
721 self.assertEqual(len(client.create_calls), 1)
722
723 await sidecar.dispatch(
724 {
725 "command": "turn.start",
726 "request_id": "r-switch-2",
727 "conversation_id": "conv-switch",
728 "prompt": "second",
729 "prompt_profile": "invited_friend",
730 "prompt_version": 1,
731 "knowledge_version": 1,
732 }
733 )
734 await sidecar.drain_events()
735
736 # Old session is disconnected and permanently deleted before new one opens.
737 self.assertEqual(first_session.disconnect_calls, 1)
738 self.assertIn(visitor_sdk_id, client.delete_calls)
739
740 # The new session uses the invited_friend derived ID, not the visitor one.
741 resume_id, resume_opts = client.resume_calls[-1]
742 self.assertEqual(resume_id, friend_sdk_id)
743 self.assertNotEqual(friend_sdk_id, visitor_sdk_id)
744 self.assertEqual(resume_opts["system_message"]["mode"], "append")
745 expected_content = _FAKE_PROFILES["invited_friend"]["content"]
746 self.assertEqual(resume_opts["system_message"]["content"], expected_content)
747
748 # The newly opened session is a distinct object.
749 second_session = client.sessions[friend_sdk_id]
750 self.assertIsNot(second_session, first_session)
751
752 done = [e for e in self.output if e["type"] == "turn.done" and e["request_id"] == "r-switch-2"]
753 self.assertFalse(done[0].get("failed", False))
754
755 async def test_concurrent_conversations_profile_isolation(self):
756 sidecar, client = await self.make_sidecar()
757 await asyncio.gather(
758 sidecar.dispatch(
759 {
760 "command": "turn.start",
761 "request_id": "req-visitor",
762 "conversation_id": "conv-visitor",
763 "prompt": "hello visitor",
764 "prompt_profile": "public_visitor",
765 "prompt_version": 1,
766 "knowledge_version": 1,
767 }
768 ),
769 sidecar.dispatch(
770 {
771 "command": "turn.start",
772 "request_id": "req-admin",
773 "conversation_id": "conv-admin",
774 "prompt": "hello admin",
775 "prompt_profile": "june_admin",
776 "prompt_version": 1,
777 "knowledge_version": 1,
778 }
779 ),
780 )
781 await sidecar.drain_events()
782
783 visitor_sdk_id = _fake_sdk_id("conv-visitor", "public_visitor")
784 admin_sdk_id = _fake_sdk_id("conv-admin", "june_admin")
785 options_by_sdk_id = {sdk_id: opts for sdk_id, opts in client.create_calls}
786 visitor_content = options_by_sdk_id[visitor_sdk_id]["system_message"]["content"]
787 admin_content = options_by_sdk_id[admin_sdk_id]["system_message"]["content"]
788 self.assertNotEqual(visitor_content, admin_content)
789 self.assertEqual(visitor_content, _FAKE_PROFILES["public_visitor"]["content"])
790 self.assertEqual(admin_content, _FAKE_PROFILES["june_admin"]["content"])
791
792 async def test_profile_switch_while_active_is_rejected(self):
793 sidecar, client = await self.make_sidecar("pending")
794
795 await sidecar.dispatch(
796 {
797 "command": "turn.start",
798 "request_id": "active-req",
799 "conversation_id": "conv-active-switch",
800 "prompt": "wait",
801 "prompt_profile": "public_visitor",
802 "prompt_version": 1,
803 "knowledge_version": 1,
804 }
805 )
806
807 await sidecar.dispatch(
808 {
809 "command": "turn.start",
810 "request_id": "switch-req",
811 "conversation_id": "conv-active-switch",
812 "prompt": "switch",
813 "prompt_profile": "invited_friend",
814 "prompt_version": 1,
815 "knowledge_version": 1,
816 }
817 )
818
819 errors = [e for e in self.output if e["type"] == "turn.error" and e["request_id"] == "switch-req"]
820 self.assertTrue(len(errors) > 0)
821 done = [e for e in self.output if e["type"] == "turn.done" and e["request_id"] == "switch-req"]
822 self.assertTrue(done[0]["failed"])
823 # Active-turn switch must not trigger any cleanup at all.
824 sdk_id = _fake_sdk_id("conv-active-switch", "public_visitor")
825 self.assertEqual(client.sessions[sdk_id].disconnect_calls, 0)
826 self.assertEqual(client.delete_calls, [])
827
828 async def test_startup_compilation_failure_prevents_readiness(self):
829 call_count = {"n": 0}
830
831 def failing_compile_fn(profile: str) -> dict:
832 call_count["n"] += 1
833 raise ValueError(f"corrupted assets for {profile!r}")
834
835 client = FakeClient()
836 sidecar = Sidecar(client, self.config, self.capture, compile_fn=failing_compile_fn)
837 with self.assertRaises(ValueError) as ctx:
838 await sidecar.start()
839 self.assertIn("corrupted", str(ctx.exception))
840 self.assertFalse(client.started)
841 self.assertGreater(call_count["n"], 0)
842
843 # ------------------------------------------------------------------
844 # Derived session ID security tests
845 # ------------------------------------------------------------------
846
847 async def test_distinct_profiles_produce_distinct_sdk_session_ids(self):
848 """Different profiles on the same conversation must never share an SDK ID."""
849 ids = {
850 profile: _fake_sdk_id("conv-same", profile)
851 for profile in ("public_visitor", "invited_friend", "june_admin")
852 }
853 self.assertEqual(len(set(ids.values())), 3, "each profile needs a unique SDK ID")
854 for sid in ids.values():
855 uuid.UUID(sid) # every value must be a valid UUID
856
857 async def test_same_profile_produces_same_sdk_id_across_restarts(self):
858 """The derived ID is deterministic: a sidecar restart resumes the same session."""
859 client = FakeClient()
860 sidecar1 = Sidecar(client, self.config, self.capture, compile_fn=_fake_compile_fn)
861 await sidecar1.start()
862 await sidecar1.dispatch({
863 "command": "turn.start",
864 "request_id": "restart-1",
865 "conversation_id": "conv-restart",
866 "prompt": "hello",
867 "prompt_profile": "public_visitor",
868 "prompt_version": 1,
869 "knowledge_version": 1,
870 })
871 await sidecar1.drain_events()
872
873 # Simulate sidecar restart: new Sidecar instance, same FakeClient (SDK store).
874 sidecar2 = Sidecar(client, self.config, self.capture, compile_fn=_fake_compile_fn)
875 await sidecar2.start()
876 await sidecar2.dispatch({
877 "command": "turn.start",
878 "request_id": "restart-2",
879 "conversation_id": "conv-restart",
880 "prompt": "still here",
881 "prompt_profile": "public_visitor",
882 "prompt_version": 1,
883 "knowledge_version": 1,
884 })
885 await sidecar2.drain_events()
886
887 # Sidecar1 tries resume (fails - no session yet) then creates.
888 # Sidecar2 tries resume (succeeds - session persists in SDK store).
889 # Therefore exactly one create, two resume attempts, both on the same derived ID.
890 self.assertEqual(len(client.create_calls), 1)
891 self.assertEqual(len(client.resume_calls), 2)
892 derived = _fake_sdk_id("conv-restart", "public_visitor")
893 self.assertEqual(client.create_calls[0][0], derived)
894 self.assertTrue(all(r == derived for r, _ in client.resume_calls))
895
896 async def test_profile_switch_permanently_deletes_old_sdk_session(self):
897 """Switching profile must delete the old derived SDK session before creating new."""
898 sidecar, client = await self.make_sidecar()
899 await sidecar.dispatch({
900 "command": "turn.start",
901 "request_id": "admin-turn",
902 "conversation_id": "conv-priv",
903 "prompt": "admin question",
904 "prompt_profile": "june_admin",
905 "prompt_version": 1,
906 "knowledge_version": 1,
907 })
908 await sidecar.drain_events()
909
910 admin_sdk_id = _fake_sdk_id("conv-priv", "june_admin")
911 visitor_sdk_id = _fake_sdk_id("conv-priv", "public_visitor")
912 self.assertNotEqual(admin_sdk_id, visitor_sdk_id)
913
914 await sidecar.dispatch({
915 "command": "turn.start",
916 "request_id": "visitor-turn",
917 "conversation_id": "conv-priv",
918 "prompt": "public question",
919 "prompt_profile": "public_visitor",
920 "prompt_version": 1,
921 "knowledge_version": 1,
922 })
923 await sidecar.drain_events()
924
925 # Admin session must be permanently deleted before visitor session opens.
926 self.assertIn(admin_sdk_id, client.delete_calls)
927 # Admin session must not be in the live sessions map.
928 self.assertNotIn(admin_sdk_id, client.sessions)
929 # Visitor session is a distinct object.
930 self.assertIn(visitor_sdk_id, client.sessions)
931
932 async def test_delete_conversation_uses_derived_sdk_id_not_conversation_id(self):
933 """conversation.delete must call delete_session with the derived UUID."""
934 sidecar, client = await self.make_sidecar()
935 await sidecar.dispatch({
936 "command": "turn.start",
937 "request_id": "setup-turn",
938 "conversation_id": "conv-del-check",
939 "prompt": "hi",
940 "prompt_profile": "june_admin",
941 "prompt_version": 1,
942 "knowledge_version": 1,
943 })
944 await sidecar.drain_events()
945
946 admin_sdk_id = _fake_sdk_id("conv-del-check", "june_admin")
947
948 await sidecar.dispatch({
949 "command": "conversation.delete",
950 "request_id": "del-req",
951 "conversation_id": "conv-del-check",
952 })
953
954 self.assertEqual(client.delete_calls[0], admin_sdk_id)
955 self.assertEqual(
956 set(client.delete_calls),
957 {
958 _fake_sdk_id("conv-del-check", "public_visitor"),
959 _fake_sdk_id("conv-del-check", "invited_friend"),
960 _fake_sdk_id("conv-del-check", "june_admin"),
961 },
962 )
963 self.assertNotIn("conv-del-check", client.delete_calls)
964
965 async def test_delete_nonexistent_conversation_cleans_known_profile_ids(self):
966 """An uncached delete purges every currently known derived session ID."""
967 sidecar, client = await self.make_sidecar()
968 await sidecar.dispatch({
969 "command": "conversation.delete",
970 "request_id": "del-ghost",
971 "conversation_id": "ghost-conv",
972 })
973
974 self.assertEqual(
975 set(client.delete_calls),
976 {
977 _fake_sdk_id("ghost-conv", "public_visitor"),
978 _fake_sdk_id("ghost-conv", "invited_friend"),
979 _fake_sdk_id("ghost-conv", "june_admin"),
980 },
981 )
982 self.assertNotIn("ghost-conv", client.delete_calls)
983 done = [e for e in self.output if e["type"] == "turn.done" and e["request_id"] == "del-ghost"]
984 self.assertEqual(done[0]["action"], "conversation.delete")
985
986 async def test_admin_session_not_reachable_via_visitor_derived_id(self):
987 """A visitor SDK ID must differ from the admin one for the same conversation."""
988 admin_id = _fake_sdk_id("shared-conv", "june_admin")
989 visitor_id = _fake_sdk_id("shared-conv", "public_visitor")
990 self.assertNotEqual(admin_id, visitor_id)
991
992 sidecar, client = await self.make_sidecar()
993 # Establish an admin session.
994 await sidecar.dispatch({
995 "command": "turn.start",
996 "request_id": "admin-req",
997 "conversation_id": "shared-conv",
998 "prompt": "secret",
999 "prompt_profile": "june_admin",
1000 "prompt_version": 1,
1001 "knowledge_version": 1,
1002 })
1003 await sidecar.drain_events()
1004
1005 # The admin session object must NOT be accessible under the visitor-derived ID.
1006 admin_session = client.sessions.get(admin_id)
1007 self.assertIsNotNone(admin_session)
1008 self.assertIsNone(client.sessions.get(visitor_id),
1009 "visitor SDK ID must not map to any session object at this point")
1010
1011
1012 # ---------------------------------------------------------------------------
1013 # _validate_history unit tests
1014 # ---------------------------------------------------------------------------
1015
1016 class ValidateHistoryTest(unittest.TestCase):
1017 def test_none_returns_empty_list(self):
1018 self.assertEqual(_validate_history(None), [])
1019
1020 def test_empty_list_accepted(self):
1021 self.assertEqual(_validate_history([]), [])
1022
1023 def test_valid_two_entries(self):
1024 hist = [
1025 {"role": "user", "content": "hello"},
1026 {"role": "assistant", "content": "hi"},
1027 ]
1028 result = _validate_history(hist)
1029 self.assertEqual(result, hist)
1030
1031 def test_not_a_list_raises(self):
1032 for bad in (42, "string", True, False, {}, object()):
1033 with self.assertRaises(ValueError, msg=f"should reject {bad!r}"):
1034 _validate_history(bad)
1035
1036 def test_too_many_entries_raises(self):
1037 entries = [{"role": "user", "content": "x"}] * (_HISTORY_MAX_ENTRIES + 1)
1038 with self.assertRaises(ValueError):
1039 _validate_history(entries)
1040
1041 def test_exactly_max_entries_accepted(self):
1042 entries = [
1043 {"role": "user" if i % 2 == 0 else "assistant", "content": "x"}
1044 for i in range(_HISTORY_MAX_ENTRIES)
1045 ]
1046 result = _validate_history(entries)
1047 self.assertEqual(len(result), _HISTORY_MAX_ENTRIES)
1048
1049 def test_non_object_entry_raises(self):
1050 for bad_entry in (42, "string", True, None, []):
1051 with self.assertRaises(ValueError):
1052 _validate_history([bad_entry])
1053
1054 def test_invalid_role_raises(self):
1055 for bad_role in ("system", "SYSTEM", "User", "ASSISTANT", "", " user"):
1056 with self.assertRaises(ValueError, msg=f"role {bad_role!r} must be rejected"):
1057 _validate_history([{"role": bad_role, "content": "x"}])
1058
1059 def test_bool_role_raises(self):
1060 with self.assertRaises(ValueError):
1061 _validate_history([{"role": True, "content": "x"}])
1062
1063 def test_none_content_raises(self):
1064 with self.assertRaises(ValueError):
1065 _validate_history([{"role": "user", "content": None}])
1066
1067 def test_bool_content_raises(self):
1068 with self.assertRaises(ValueError):
1069 _validate_history([{"role": "user", "content": True}])
1070
1071 def test_int_content_raises(self):
1072 with self.assertRaises(ValueError):
1073 _validate_history([{"role": "user", "content": 42}])
1074
1075 def test_extra_key_raises(self):
1076 with self.assertRaises(ValueError):
1077 _validate_history([{"role": "user", "content": "hi", "injected": "bad"}])
1078
1079 def test_oversized_total_raises(self):
1080 # One entry with content just over the byte limit.
1081 big = "x" * (_HISTORY_MAX_BYTES + 1)
1082 with self.assertRaises(ValueError):
1083 _validate_history([{"role": "user", "content": big}])
1084
1085 def test_total_at_limit_accepted(self):
1086 # Two entries whose combined bytes sit at or under the limit.
1087 chunk_size = _HISTORY_MAX_BYTES // 2 - 10 # under limit
1088 entries = [
1089 {"role": "user", "content": "a" * chunk_size},
1090 {"role": "assistant", "content": "b" * chunk_size},
1091 ]
1092 result = _validate_history(entries)
1093 self.assertEqual(len(result), 2)
1094
1095 def test_special_characters_accepted(self):
1096 entry = {"role": "user", "content": "hello \"world\" \\ \n"}
1097 result = _validate_history([entry])
1098 self.assertEqual(result[0]["content"], entry["content"])
1099
1100 def test_empty_content_string_accepted(self):
1101 result = _validate_history([{"role": "user", "content": ""}])
1102 self.assertEqual(result[0]["content"], "")
1103
1104
1105 # ---------------------------------------------------------------------------
1106 # Sidecar history integration tests
1107 # ---------------------------------------------------------------------------
1108
1109 class SidecarHistoryTest(unittest.IsolatedAsyncioTestCase):
1110 async def asyncSetUp(self):
1111 self.output = []
1112
1113 async def capture(payload):
1114 self.output.append(payload)
1115
1116 self.capture = capture
1117 self.config = SidecarConfig(
1118 base_url="http://litellm.invalid/v1",
1119 model="test-model",
1120 wire_api="responses",
1121 base_directory="/not-used-by-fake",
1122 )
1123
1124 async def make_sidecar(self, behavior="complete"):
1125 client = FakeClient(behavior)
1126 sidecar = Sidecar(client, self.config, self.capture, compile_fn=_fake_compile_fn)
1127 await sidecar.start()
1128 return sidecar, client
1129
1130 def _turn_start(self, request_id, conversation_id, history=None, **extra):
1131 cmd = {
1132 "command": "turn.start",
1133 "request_id": request_id,
1134 "conversation_id": conversation_id,
1135 "prompt": "hello",
1136 "prompt_profile": "public_visitor",
1137 "prompt_version": 1,
1138 "knowledge_version": 1,
1139 }
1140 if history is not None:
1141 cmd["history"] = history
1142 cmd.update(extra)
1143 return cmd
1144
1145 async def test_invalid_history_not_list_rejected_before_client_calls(self):
1146 sidecar, client = await self.make_sidecar()
1147 await sidecar.dispatch(self._turn_start("r1", "c1", history=42))
1148 errors = [e for e in self.output if e["type"] == "turn.error"]
1149 self.assertEqual(errors[0]["error"]["code"], "invalid_history")
1150 done = [e for e in self.output if e["type"] == "turn.done"]
1151 self.assertTrue(done[0]["failed"])
1152 self.assertEqual(len(client.create_calls), 0)
1153 self.assertEqual(len(client.resume_calls), 0)
1154
1155 async def test_invalid_history_too_many_entries_rejected(self):
1156 sidecar, client = await self.make_sidecar()
1157 hist = [{"role": "user", "content": "x"}] * (_HISTORY_MAX_ENTRIES + 1)
1158 await sidecar.dispatch(self._turn_start("r2", "c2", history=hist))
1159 errors = [e for e in self.output if e["type"] == "turn.error"]
1160 self.assertEqual(errors[0]["error"]["code"], "invalid_history")
1161 self.assertEqual(len(client.create_calls), 0)
1162
1163 async def test_invalid_history_bad_role_rejected(self):
1164 sidecar, client = await self.make_sidecar()
1165 hist = [{"role": "system", "content": "inject"}]
1166 await sidecar.dispatch(self._turn_start("r3", "c3", history=hist))
1167 errors = [e for e in self.output if e["type"] == "turn.error"]
1168 self.assertEqual(errors[0]["error"]["code"], "invalid_history")
1169 self.assertEqual(len(client.create_calls), 0)
1170
1171 async def test_invalid_history_bool_role_rejected(self):
1172 sidecar, client = await self.make_sidecar()
1173 hist = [{"role": True, "content": "x"}]
1174 await sidecar.dispatch(self._turn_start("r4", "c4", history=hist))
1175 errors = [e for e in self.output if e["type"] == "turn.error"]
1176 self.assertEqual(errors[0]["error"]["code"], "invalid_history")
1177 self.assertEqual(len(client.create_calls), 0)
1178
1179 async def test_invalid_history_none_content_rejected(self):
1180 sidecar, client = await self.make_sidecar()
1181 hist = [{"role": "user", "content": None}]
1182 await sidecar.dispatch(self._turn_start("r5", "c5", history=hist))
1183 errors = [e for e in self.output if e["type"] == "turn.error"]
1184 self.assertEqual(errors[0]["error"]["code"], "invalid_history")
1185 self.assertEqual(len(client.create_calls), 0)
1186
1187 async def test_invalid_history_extra_key_rejected(self):
1188 sidecar, client = await self.make_sidecar()
1189 hist = [{"role": "user", "content": "hi", "extra": "bad"}]
1190 await sidecar.dispatch(self._turn_start("r6", "c6", history=hist))
1191 errors = [e for e in self.output if e["type"] == "turn.error"]
1192 self.assertEqual(errors[0]["error"]["code"], "invalid_history")
1193 self.assertEqual(len(client.create_calls), 0)
1194
1195 async def test_invalid_history_oversized_rejected(self):
1196 sidecar, client = await self.make_sidecar()
1197 big = "x" * (_HISTORY_MAX_BYTES + 1)
1198 hist = [{"role": "user", "content": big}]
1199 await sidecar.dispatch(self._turn_start("r7", "c7", history=hist))
1200 errors = [e for e in self.output if e["type"] == "turn.error"]
1201 self.assertEqual(errors[0]["error"]["code"], "invalid_history")
1202 self.assertEqual(len(client.create_calls), 0)
1203
1204 async def test_valid_empty_history_accepted(self):
1205 sidecar, client = await self.make_sidecar()
1206 await sidecar.dispatch(self._turn_start("r8", "c8", history=[]))
1207 await sidecar.drain_events()
1208 done = [e for e in self.output if e["type"] == "turn.done" and e["request_id"] == "r8"]
1209 self.assertFalse(done[0].get("failed", False))
1210 self.assertEqual(len(client.create_calls), 1)
1211
1212 async def test_fresh_create_receives_history_in_system_message(self):
1213 """When resume_session fails (no persisted session), create_session must
1214 include the PRIOR OWNED CONVERSATION TRANSCRIPT in system_message."""
1215 sidecar, client = await self.make_sidecar()
1216 hist = [
1217 {"role": "user", "content": "prior question"},
1218 {"role": "assistant", "content": "prior answer"},
1219 ]
1220 await sidecar.dispatch(self._turn_start("r-create", "c-create", history=hist))
1221 await sidecar.drain_events()
1222
1223 self.assertEqual(len(client.create_calls), 1)
1224 _, create_opts = client.create_calls[0]
1225 sys_content = create_opts["system_message"]["content"]
1226 self.assertIn("PRIOR OWNED CONVERSATION TRANSCRIPT", sys_content)
1227 self.assertIn("prior question", sys_content)
1228 self.assertIn("prior answer", sys_content)
1229 self.assertIn("untrusted", sys_content.lower())
1230 self.assertEqual(create_opts["system_message"]["mode"], "append")
1231
1232 async def test_resume_does_not_inject_history(self):
1233 """An existing persisted session must not receive history in system_message."""
1234 sidecar, client = await self.make_sidecar()
1235 hist = [{"role": "user", "content": "prior"}]
1236
1237 # First turn: creates the session.
1238 await sidecar.dispatch(self._turn_start("r-resume-1", "c-resume", history=hist))
1239 await sidecar.drain_events()
1240 self.assertEqual(len(client.create_calls), 1)
1241 first_content = client.create_calls[0][1]["system_message"]["content"]
1242
1243 # Second turn on same conversation: must resume (SDK session persists).
1244 await sidecar.dispatch(self._turn_start("r-resume-2", "c-resume", history=hist))
1245 await sidecar.drain_events()
1246 self.assertEqual(len(client.resume_calls), 1)
1247 # resume_session does not receive options from create_session call.
1248 resume_opts = client.resume_calls[0][1]
1249 resume_sys = resume_opts["system_message"]["content"]
1250 # The resume options (base options) must not contain the transcript block.
1251 self.assertNotIn("PRIOR OWNED CONVERSATION TRANSCRIPT", resume_sys)
1252 # The create call had the transcript; verify only one create happened.
1253 self.assertEqual(len(client.create_calls), 1)
1254
1255 async def test_history_with_no_field_uses_base_system_message_on_create(self):
1256 """Absent history field: create_session uses base system_message without transcript."""
1257 sidecar, client = await self.make_sidecar()
1258 await sidecar.dispatch(self._turn_start("r-nofield", "c-nofield"))
1259 await sidecar.drain_events()
1260 self.assertEqual(len(client.create_calls), 1)
1261 _, opts = client.create_calls[0]
1262 self.assertNotIn("PRIOR OWNED CONVERSATION TRANSCRIPT", opts["system_message"]["content"])
1263
1264 async def test_profile_switch_create_fallback_gets_history(self):
1265 """After a profile switch the new derived session is a fresh create;
1266 the history must be injected into the new session's system_message only."""
1267 sidecar, client = await self.make_sidecar()
1268
1269 # First turn: public_visitor session created; no prior history yet.
1270 await sidecar.dispatch(self._turn_start("r-sw-1", "c-switch"))
1271 await sidecar.drain_events()
1272 visitor_sdk_id = _fake_sdk_id("c-switch", "public_visitor")
1273 friend_sdk_id = _fake_sdk_id("c-switch", "invited_friend")
1274 self.assertNotEqual(visitor_sdk_id, friend_sdk_id)
1275 first_create_content = client.create_calls[0][1]["system_message"]["content"]
1276 # No transcript on initial create (no history provided).
1277 self.assertNotIn("PRIOR OWNED CONVERSATION TRANSCRIPT", first_create_content)
1278
1279 # Second turn: invited_friend profile — old session deleted, new fresh create.
1280 # Now we send history representing the prior visitor exchange.
1281 friend_hist = [
1282 {"role": "user", "content": "visitor msg"},
1283 {"role": "assistant", "content": "answer"},
1284 ]
1285 await sidecar.dispatch({
1286 "command": "turn.start",
1287 "request_id": "r-sw-2",
1288 "conversation_id": "c-switch",
1289 "prompt": "switch question",
1290 "prompt_profile": "invited_friend",
1291 "prompt_version": 1,
1292 "knowledge_version": 1,
1293 "history": friend_hist,
1294 })
1295 await sidecar.drain_events()
1296
1297 # Old visitor session must be deleted.
1298 self.assertIn(visitor_sdk_id, client.delete_calls)
1299 # New session created under invited_friend derived ID.
1300 create_ids = [sid for sid, _ in client.create_calls]
1301 self.assertIn(friend_sdk_id, create_ids)
1302
1303 # Second create must include the history transcript.
1304 second_create_opts = dict(client.create_calls)[friend_sdk_id]
1305 second_content = second_create_opts["system_message"]["content"]
1306 self.assertIn("PRIOR OWNED CONVERSATION TRANSCRIPT", second_content)
1307 self.assertIn("visitor msg", second_content)
1308 self.assertIn("answer", second_content)
1309 self.assertIn("untrusted", second_content.lower())
1310
1311 done = [e for e in self.output if e["type"] == "turn.done" and e["request_id"] == "r-sw-2"]
1312 self.assertFalse(done[0].get("failed", False))
1313
1314 async def test_history_not_exposed_in_events(self):
1315 """History must not appear in any turn.accepted, assistant.delta, or
1316 turn.done events emitted to the client."""
1317 sidecar, client = await self.make_sidecar()
1318 sensitive = "SENSITIVE_TRANSCRIPT_DATA_XYZ"
1319 hist = [{"role": "user", "content": sensitive}]
1320 await sidecar.dispatch(self._turn_start("r-safe", "c-safe", history=hist))
1321 await sidecar.drain_events()
1322
1323 for ev in self.output:
1324 for field in ("delta", "content", "prompt"):
1325 val = ev.get(field, "")
1326 if isinstance(val, str):
1327 self.assertNotIn(sensitive, val,
1328 f"history leaked into event[{field}]: {ev}")
1329
1330 async def test_existing_conversation_history_compatibility(self):
1331 """Conversations without history (pre-change) work normally: no
1332 transcript block is injected when history is absent/empty."""
1333 sidecar, client = await self.make_sidecar()
1334 # Simulate an old-style command with no history key.
1335 await sidecar.dispatch({
1336 "command": "turn.start",
1337 "request_id": "r-compat",
1338 "conversation_id": "c-compat",
1339 "prompt": "legacy",
1340 "prompt_profile": "june_admin",
1341 "prompt_version": 1,
1342 "knowledge_version": 1,
1343 })
1344 await sidecar.drain_events()
1345 done = [e for e in self.output if e["type"] == "turn.done" and e["request_id"] == "r-compat"]
1346 self.assertFalse(done[0].get("failed", False))
1347 _, opts = client.create_calls[0]
1348 self.assertNotIn("PRIOR OWNED CONVERSATION TRANSCRIPT", opts["system_message"]["content"])
1349
445 1350
446 if __name__ == "__main__": 1351 if __name__ == "__main__":
447 unittest.main() 1352 unittest.main()