cct-cli 0.7.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT AI Mode Reliability & Small-Model Compatibility Test Suite.
|
|
3
|
+
|
|
4
|
+
Verifies all 42 sections and Acceptance Criteria of the
|
|
5
|
+
CAT AI Mode Reliability Contract:
|
|
6
|
+
- Test A: Normal chat greeting (no context leak)
|
|
7
|
+
- Test B: Context contamination regression (Research SMTP -> Chat hello)
|
|
8
|
+
- Test C: Mode switching isolation (Notebook -> Research -> Plan -> Debugger -> Chat)
|
|
9
|
+
- Test D: Ollama streaming parser & deduplication
|
|
10
|
+
- Test E: Request isolation, cancellation, and stream reset
|
|
11
|
+
- Test F: Small/base model capability detection (deepseek-coder:1.3b-base-q8_0)
|
|
12
|
+
- Test G: Context token budgeting & priority trimming
|
|
13
|
+
- Test H: Repetition detection (GenerationGuard)
|
|
14
|
+
- Test I: Provider failure handling
|
|
15
|
+
- Test J: Model-vs-CAT diagnostic test (HELLO_TEST)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import unittest
|
|
19
|
+
import uuid
|
|
20
|
+
import time
|
|
21
|
+
from unittest.mock import MagicMock, patch
|
|
22
|
+
|
|
23
|
+
from calc_terminal.ai_context import (
|
|
24
|
+
AIContextManager,
|
|
25
|
+
AIMessage,
|
|
26
|
+
AIRequest,
|
|
27
|
+
AIMode,
|
|
28
|
+
NotebookCell,
|
|
29
|
+
ResearchContext,
|
|
30
|
+
DebuggerContext,
|
|
31
|
+
PlanContext,
|
|
32
|
+
ContextBudget,
|
|
33
|
+
get_context_manager,
|
|
34
|
+
simple_token_estimate,
|
|
35
|
+
)
|
|
36
|
+
from calc_terminal.models.profiles import (
|
|
37
|
+
ModelCapabilities,
|
|
38
|
+
ModelProfile,
|
|
39
|
+
detect_capabilities,
|
|
40
|
+
get_model_profile,
|
|
41
|
+
)
|
|
42
|
+
from calc_terminal.providers.ollama_adapter import (
|
|
43
|
+
OllamaProvider,
|
|
44
|
+
GenerationGuard,
|
|
45
|
+
OutputSanitizer,
|
|
46
|
+
RequestState,
|
|
47
|
+
)
|
|
48
|
+
from calc_terminal.session import ChatSession, Turn
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestAIModeReliability(unittest.TestCase):
|
|
52
|
+
|
|
53
|
+
def setUp(self):
|
|
54
|
+
self.ctx_mgr = AIContextManager()
|
|
55
|
+
|
|
56
|
+
# -------------------------------------------------------------------------
|
|
57
|
+
# Test A: Normal Chat Greeting
|
|
58
|
+
# -------------------------------------------------------------------------
|
|
59
|
+
def test_a_normal_chat_greeting_has_no_unrelated_context(self):
|
|
60
|
+
"""User sends 'hello' in Chat mode: context contains only concise instructions and user prompt."""
|
|
61
|
+
profile = get_model_profile("ollama", "deepseek-coder:1.3b-base-q8_0")
|
|
62
|
+
req = AIRequest(
|
|
63
|
+
session_id="test-sess-1",
|
|
64
|
+
request_id=str(uuid.uuid4()),
|
|
65
|
+
mode="chat",
|
|
66
|
+
user_message="hello",
|
|
67
|
+
history=[],
|
|
68
|
+
model=profile.model,
|
|
69
|
+
provider=profile.provider,
|
|
70
|
+
)
|
|
71
|
+
ctx = self.ctx_mgr.build_context(req, profile=profile)
|
|
72
|
+
|
|
73
|
+
# Context must contain minimal system prompt and user message 'hello'
|
|
74
|
+
self.assertTrue(ctx.is_minimal, "Small base model must use minimal prompt mode")
|
|
75
|
+
self.assertEqual(len(ctx.messages), 2, "Greeting with no history must have exactly system + user")
|
|
76
|
+
self.assertEqual(ctx.messages[0]["role"], "system")
|
|
77
|
+
self.assertEqual(ctx.messages[1]["role"], "user")
|
|
78
|
+
self.assertEqual(ctx.messages[1]["content"], "hello")
|
|
79
|
+
|
|
80
|
+
# Must NOT contain research, notebook, or tool contamination
|
|
81
|
+
full_content = " ".join(m["content"] for m in ctx.messages)
|
|
82
|
+
self.assertNotIn("Conversation so far:", full_content)
|
|
83
|
+
self.assertNotIn("SMTP", full_content)
|
|
84
|
+
self.assertNotIn("IMAP", full_content)
|
|
85
|
+
self.assertNotIn("Notebook", full_content)
|
|
86
|
+
|
|
87
|
+
# -------------------------------------------------------------------------
|
|
88
|
+
# Test B: Context Contamination Regression Test (SMTP Research -> Chat Hello)
|
|
89
|
+
# -------------------------------------------------------------------------
|
|
90
|
+
def test_b_previous_research_does_not_leak_into_chat_greeting(self):
|
|
91
|
+
"""Large research response (SMTP/IMAP) must NOT contaminate subsequent Chat 'hello' prompt."""
|
|
92
|
+
session = ChatSession()
|
|
93
|
+
|
|
94
|
+
# 1. Simulate prior Research mode turn with massive SMTP/IMAP documentation
|
|
95
|
+
large_research_text = (
|
|
96
|
+
"## 4.17 Sending and receiving email with Python\n"
|
|
97
|
+
"SMTP (Simple Mail Transfer Protocol) and IMAP (Internet Message Access Protocol) "
|
|
98
|
+
"are standard protocols for email transfer in Python. smtplib.SMTP('localhost') "
|
|
99
|
+
"connects to the server, and email.mime constructs RFC 2822 messages..."
|
|
100
|
+
* 20
|
|
101
|
+
)
|
|
102
|
+
t_user = Turn("turn-1", "user", "research SMTP in Python", mode="research")
|
|
103
|
+
t_asst = Turn("turn-2", "assistant", large_research_text, mode="research")
|
|
104
|
+
session.turns.extend([t_user, t_asst])
|
|
105
|
+
|
|
106
|
+
# 2. User switches to Chat mode and sends "hello"
|
|
107
|
+
chat_history = session.as_prompt_history(mode="chat")
|
|
108
|
+
|
|
109
|
+
# Mode isolation: Research turns must NOT be in chat history
|
|
110
|
+
history_roles = [r for r, _ in chat_history]
|
|
111
|
+
history_texts = [t for _, t in chat_history]
|
|
112
|
+
combined_history = " ".join(history_texts)
|
|
113
|
+
|
|
114
|
+
self.assertNotIn("SMTP", combined_history, "Chat history must not contain SMTP research dump")
|
|
115
|
+
self.assertNotIn("IMAP", combined_history, "Chat history must not contain IMAP research dump")
|
|
116
|
+
|
|
117
|
+
# 3. Build context for 'hello' with deepseek-coder:1.3b-base-q8_0
|
|
118
|
+
profile = get_model_profile("ollama", "deepseek-coder:1.3b-base-q8_0")
|
|
119
|
+
messages_hist = [
|
|
120
|
+
AIMessage(id="m1", role=r, content=t, mode="chat") for r, t in chat_history
|
|
121
|
+
]
|
|
122
|
+
req = AIRequest(
|
|
123
|
+
session_id="test-sess-b",
|
|
124
|
+
request_id=str(uuid.uuid4()),
|
|
125
|
+
mode="chat",
|
|
126
|
+
user_message="hello",
|
|
127
|
+
history=messages_hist,
|
|
128
|
+
model=profile.model,
|
|
129
|
+
provider=profile.provider,
|
|
130
|
+
)
|
|
131
|
+
ctx = self.ctx_mgr.build_context(req, profile=profile)
|
|
132
|
+
|
|
133
|
+
# Assert no research text in built prompt
|
|
134
|
+
full_prompt = " ".join(m["content"] for m in ctx.messages)
|
|
135
|
+
self.assertNotIn("SMTP", full_prompt)
|
|
136
|
+
self.assertNotIn("IMAP", full_prompt)
|
|
137
|
+
self.assertNotIn("4.17 Sending and receiving email", full_prompt)
|
|
138
|
+
self.assertNotIn("Conversation so far:", full_prompt)
|
|
139
|
+
|
|
140
|
+
# -------------------------------------------------------------------------
|
|
141
|
+
# Test C: Mode Switching Isolation
|
|
142
|
+
# -------------------------------------------------------------------------
|
|
143
|
+
def test_c_mode_switching_isolates_contexts(self):
|
|
144
|
+
"""Mode switching across Notebook -> Research -> Plan -> Debugger -> Chat preserves strict isolation."""
|
|
145
|
+
session = ChatSession()
|
|
146
|
+
|
|
147
|
+
# Add turns across different modes
|
|
148
|
+
session.turns.append(Turn("t1", "user", "calculate derivative", mode="notebook"))
|
|
149
|
+
session.turns.append(Turn("t2", "assistant", "derivative is 2x", mode="notebook"))
|
|
150
|
+
session.turns.append(Turn("t3", "user", "research quantum physics", mode="research"))
|
|
151
|
+
session.turns.append(Turn("t4", "assistant", "quantum theory details...", mode="research"))
|
|
152
|
+
session.turns.append(Turn("t5", "user", "create roadmap", mode="plan"))
|
|
153
|
+
session.turns.append(Turn("t6", "assistant", "Phase 1: architecture...", mode="plan"))
|
|
154
|
+
session.turns.append(Turn("t7", "user", "trace IndexError", mode="debugger"))
|
|
155
|
+
session.turns.append(Turn("t8", "assistant", "line 42 index out of range", mode="debugger"))
|
|
156
|
+
|
|
157
|
+
# In Chat mode: only conversational pleasantries, no research or debug traces
|
|
158
|
+
chat_hist = session.as_prompt_history(mode="chat")
|
|
159
|
+
chat_text = " ".join(t for _, t in chat_hist)
|
|
160
|
+
self.assertNotIn("quantum theory", chat_text)
|
|
161
|
+
self.assertNotIn("Phase 1: architecture", chat_text)
|
|
162
|
+
self.assertNotIn("line 42 index out of range", chat_text)
|
|
163
|
+
|
|
164
|
+
# In Debugger mode: only debugger turns
|
|
165
|
+
dbg_hist = session.as_prompt_history(mode="debugger")
|
|
166
|
+
dbg_text = " ".join(t for _, t in dbg_hist)
|
|
167
|
+
self.assertIn("line 42 index out of range", dbg_text)
|
|
168
|
+
self.assertNotIn("quantum theory", dbg_text)
|
|
169
|
+
self.assertNotIn("Phase 1: architecture", dbg_text)
|
|
170
|
+
|
|
171
|
+
# In Plan mode: only plan turns
|
|
172
|
+
plan_hist = session.as_prompt_history(mode="plan")
|
|
173
|
+
plan_text = " ".join(t for _, t in plan_hist)
|
|
174
|
+
self.assertIn("Phase 1: architecture", plan_text)
|
|
175
|
+
self.assertNotIn("line 42 index out of range", plan_text)
|
|
176
|
+
|
|
177
|
+
# -------------------------------------------------------------------------
|
|
178
|
+
# Test D: Ollama Streaming Parser & Output Sanitizer
|
|
179
|
+
# -------------------------------------------------------------------------
|
|
180
|
+
def test_d_ollama_streaming_and_sanitizer(self):
|
|
181
|
+
"""Ollama stream yields only clean content, and OutputSanitizer strips stray prompt artifacts."""
|
|
182
|
+
sanitizer = OutputSanitizer()
|
|
183
|
+
|
|
184
|
+
# Strips accidental prefix
|
|
185
|
+
self.assertEqual(sanitizer.sanitize_first_chunk("Assistant: Hello world"), "Hello world")
|
|
186
|
+
self.assertEqual(sanitizer.sanitize_first_chunk("CAT AI: Here is the code"), "Here is the code")
|
|
187
|
+
self.assertEqual(sanitizer.sanitize_first_chunk("Normal text"), "Normal text")
|
|
188
|
+
|
|
189
|
+
# Preserves markdown code blocks without breaking syntax
|
|
190
|
+
code_block = "```python\ndef foo():\n return 42\n```"
|
|
191
|
+
self.assertEqual(sanitizer.sanitize_final(code_block), code_block)
|
|
192
|
+
|
|
193
|
+
# Mock Ollama streaming response chunks
|
|
194
|
+
mock_chunks = [
|
|
195
|
+
b'{"message": {"content": "Assistant: Hel"}, "done": false}\n',
|
|
196
|
+
b'{"message": {"content": "lo "}, "done": false}\n',
|
|
197
|
+
b'{"message": {"content": "there!"}, "done": true}\n',
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
mock_resp = MagicMock()
|
|
201
|
+
mock_resp.status_code = 200
|
|
202
|
+
mock_resp.iter_lines.return_value = [c.decode("utf-8") for c in mock_chunks]
|
|
203
|
+
|
|
204
|
+
with patch("requests.post", return_value=mock_resp):
|
|
205
|
+
provider = OllamaProvider("http://localhost:11434")
|
|
206
|
+
streamed = list(provider.stream_chat("llama3.2", [{"role": "user", "content": "hi"}]))
|
|
207
|
+
|
|
208
|
+
result_text = "".join(streamed)
|
|
209
|
+
self.assertEqual(result_text, "Hello there!")
|
|
210
|
+
self.assertNotIn("Assistant:", result_text)
|
|
211
|
+
self.assertNotIn('{"message"', result_text)
|
|
212
|
+
|
|
213
|
+
# -------------------------------------------------------------------------
|
|
214
|
+
# Test E: Request Isolation, Stream Reset, & Cancellation
|
|
215
|
+
# -------------------------------------------------------------------------
|
|
216
|
+
def test_e_request_isolation_and_cancellation(self):
|
|
217
|
+
"""New request allocates fresh request ID, resets stream, and cancellation terminates cleanly."""
|
|
218
|
+
provider = OllamaProvider("http://localhost:11434")
|
|
219
|
+
self.assertEqual(provider.state, RequestState.IDLE)
|
|
220
|
+
|
|
221
|
+
# Reset stream for Request A
|
|
222
|
+
id_a = provider.reset_stream("req-A")
|
|
223
|
+
self.assertEqual(id_a, "req-A")
|
|
224
|
+
self.assertEqual(provider.state, RequestState.STARTING)
|
|
225
|
+
|
|
226
|
+
# Reset stream for Request B (cancels A)
|
|
227
|
+
id_b = provider.reset_stream("req-B")
|
|
228
|
+
self.assertEqual(id_b, "req-B")
|
|
229
|
+
self.assertNotEqual(id_a, id_b)
|
|
230
|
+
|
|
231
|
+
# Cancel active request
|
|
232
|
+
cancelled = provider.cancel_active()
|
|
233
|
+
self.assertTrue(cancelled)
|
|
234
|
+
self.assertEqual(provider.state, RequestState.CANCELLED)
|
|
235
|
+
|
|
236
|
+
# -------------------------------------------------------------------------
|
|
237
|
+
# Test F: Small Model Profile (deepseek-coder:1.3b-base-q8_0)
|
|
238
|
+
# -------------------------------------------------------------------------
|
|
239
|
+
def test_f_small_model_capabilities_and_profile(self):
|
|
240
|
+
"""deepseek-coder:1.3b-base-q8_0 is classified as small/base model with minimal prompting."""
|
|
241
|
+
model_name = "deepseek-coder:1.3b-base-q8_0"
|
|
242
|
+
caps = detect_capabilities("ollama", model_name)
|
|
243
|
+
|
|
244
|
+
self.assertTrue(caps.is_small_model, "1.3b must be identified as small model")
|
|
245
|
+
self.assertTrue(caps.is_base_model, "base-q8_0 must be identified as base model")
|
|
246
|
+
self.assertFalse(caps.instruction_following, "Base model does not have full instruction-following capability")
|
|
247
|
+
self.assertFalse(caps.tool_calling, "Small base model must not be expected to execute agent tool loops")
|
|
248
|
+
|
|
249
|
+
profile = get_model_profile("ollama", model_name)
|
|
250
|
+
self.assertEqual(profile.prompt_template, "minimal")
|
|
251
|
+
self.assertEqual(profile.max_context_tokens, 1536)
|
|
252
|
+
self.assertEqual(profile.reserved_output_tokens, 512)
|
|
253
|
+
self.assertEqual(profile.recommended_temperature, 0.3)
|
|
254
|
+
self.assertGreaterEqual(profile.generation_options.get("repeat_penalty", 1.0), 1.15)
|
|
255
|
+
|
|
256
|
+
# -------------------------------------------------------------------------
|
|
257
|
+
# Test G: Context Token Budgeting & Priority Trimming
|
|
258
|
+
# -------------------------------------------------------------------------
|
|
259
|
+
def test_g_context_token_budgeting_trims_lowest_priority(self):
|
|
260
|
+
"""When history exceeds available budget, older turns are trimmed while user prompt is preserved."""
|
|
261
|
+
profile = get_model_profile("ollama", "deepseek-coder:1.3b-base-q8_0")
|
|
262
|
+
|
|
263
|
+
# Create 10 history turns with 200 characters each
|
|
264
|
+
history = [
|
|
265
|
+
AIMessage(id=f"h{i}", role="user" if i % 2 == 0 else "assistant",
|
|
266
|
+
content=f"This is historical turn number {i} " * 5, mode="chat")
|
|
267
|
+
for i in range(10)
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
req = AIRequest(
|
|
271
|
+
session_id="sess-budget",
|
|
272
|
+
request_id=str(uuid.uuid4()),
|
|
273
|
+
mode="chat",
|
|
274
|
+
user_message="My most important current request",
|
|
275
|
+
history=history,
|
|
276
|
+
model=profile.model,
|
|
277
|
+
provider=profile.provider,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
ctx = self.ctx_mgr.build_context(req, profile=profile)
|
|
281
|
+
|
|
282
|
+
# User request MUST be present
|
|
283
|
+
self.assertEqual(ctx.messages[-1]["content"], "My most important current request")
|
|
284
|
+
# System prompt MUST be present
|
|
285
|
+
self.assertEqual(ctx.messages[0]["role"], "system")
|
|
286
|
+
# Total tokens must not exceed maximum budget
|
|
287
|
+
self.assertLessEqual(ctx.token_estimate, profile.max_context_tokens)
|
|
288
|
+
|
|
289
|
+
# -------------------------------------------------------------------------
|
|
290
|
+
# Test H: Repetition Detection (GenerationGuard)
|
|
291
|
+
# -------------------------------------------------------------------------
|
|
292
|
+
def test_h_repetition_detection_stops_runaway_loops(self):
|
|
293
|
+
"""GenerationGuard halts repetitive output like 'hihihihihihi...' or identical sentences."""
|
|
294
|
+
guard = GenerationGuard(max_repeated_chars=16, max_sentence_repeats=3)
|
|
295
|
+
|
|
296
|
+
# 1. Feed normal tokens
|
|
297
|
+
ok, chunk = guard.feed("Hello there! How can I help you today? ")
|
|
298
|
+
self.assertTrue(ok)
|
|
299
|
+
self.assertEqual(chunk, "Hello there! How can I help you today? ")
|
|
300
|
+
|
|
301
|
+
# 2. Feed repetitive token loop 'hihihihihihihihihi...'
|
|
302
|
+
ok, chunk = guard.feed("hihihihihihihihihihihihihihihihihi")
|
|
303
|
+
self.assertFalse(ok, "Guard must detect runaway repetitive loop")
|
|
304
|
+
self.assertIn("repetitive output pattern detected", chunk)
|
|
305
|
+
|
|
306
|
+
# 3. Test sentence loop with a new guard
|
|
307
|
+
guard2 = GenerationGuard(max_repeated_chars=20, max_sentence_repeats=3)
|
|
308
|
+
guard2.feed("The quick brown fox jumps over the lazy dog. ")
|
|
309
|
+
guard2.feed("The quick brown fox jumps over the lazy dog. ")
|
|
310
|
+
ok, chunk = guard2.feed("The quick brown fox jumps over the lazy dog. ")
|
|
311
|
+
self.assertFalse(ok, "Guard must detect repeating identical sentences")
|
|
312
|
+
self.assertIn("sentence repetition loop detected", chunk)
|
|
313
|
+
|
|
314
|
+
# -------------------------------------------------------------------------
|
|
315
|
+
# Test I: Provider Failure Handling
|
|
316
|
+
# -------------------------------------------------------------------------
|
|
317
|
+
def test_i_provider_failure_returns_explicit_error_without_corrupting(self):
|
|
318
|
+
"""Connection failure produces clean, user-friendly error string."""
|
|
319
|
+
with patch("requests.post", side_effect=Exception("Connection refused")):
|
|
320
|
+
provider = OllamaProvider("http://localhost:99999")
|
|
321
|
+
streamed = list(provider.stream_chat("model", [{"role": "user", "content": "hi"}]))
|
|
322
|
+
|
|
323
|
+
result = "".join(streamed)
|
|
324
|
+
self.assertIn("Ollama streaming failure", result)
|
|
325
|
+
self.assertEqual(provider.state, RequestState.FAILED)
|
|
326
|
+
|
|
327
|
+
# -------------------------------------------------------------------------
|
|
328
|
+
# Test J: Model-vs-CAT Diagnostic Test (HELLO_TEST)
|
|
329
|
+
# -------------------------------------------------------------------------
|
|
330
|
+
def test_j_model_vs_cat_diagnostic_test(self):
|
|
331
|
+
"""run_hello_test verifies whether Ollama produces expected exact token."""
|
|
332
|
+
mock_resp = MagicMock()
|
|
333
|
+
mock_resp.status_code = 200
|
|
334
|
+
mock_resp.json.return_value = {"message": {"content": "HELLO_TEST"}}
|
|
335
|
+
|
|
336
|
+
with patch("requests.post", return_value=mock_resp):
|
|
337
|
+
provider = OllamaProvider("http://localhost:11434")
|
|
338
|
+
ok, reply = provider.run_hello_test("llama3.2")
|
|
339
|
+
self.assertTrue(ok)
|
|
340
|
+
self.assertEqual(reply, "HELLO_TEST")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == "__main__":
|
|
344
|
+
unittest.main()
|