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,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.7.9.0 — verification suite for the speed / routing / multimodal /
|
|
3
|
+
multi-agent / live-activity overhaul.
|
|
4
|
+
|
|
5
|
+
Run: python calc_terminal/test_v079_speed.py
|
|
6
|
+
(no network, no API keys — every model-dependent path is exercised with
|
|
7
|
+
fakes; everything measured here is REAL local execution time.)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
import unittest
|
|
16
|
+
from unittest import mock
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
19
|
+
|
|
20
|
+
from calc_terminal import metrics # noqa: E402
|
|
21
|
+
from calc_terminal import model_router as mr # noqa: E402
|
|
22
|
+
from calc_terminal import event_stream as evs # noqa: E402
|
|
23
|
+
from calc_terminal import fs_cache # noqa: E402
|
|
24
|
+
from calc_terminal import memory # noqa: E402
|
|
25
|
+
from calc_terminal import vision # noqa: E402
|
|
26
|
+
from calc_terminal import collaboration # noqa: E402
|
|
27
|
+
from calc_terminal import agent as cct_agent # noqa: E402
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestMetrics(unittest.TestCase):
|
|
31
|
+
def test_stage_timing_is_real(self):
|
|
32
|
+
m = metrics.begin_request("t-metrics")
|
|
33
|
+
with m.stage("routing"):
|
|
34
|
+
time.sleep(0.01)
|
|
35
|
+
m.count_model_call(3)
|
|
36
|
+
m.count_tool_call()
|
|
37
|
+
m.note_first_token()
|
|
38
|
+
total = m.finish()
|
|
39
|
+
d = m.as_dict()
|
|
40
|
+
self.assertGreaterEqual(total, 10.0)
|
|
41
|
+
self.assertGreaterEqual(d["routing_ms"], 10.0)
|
|
42
|
+
self.assertEqual(d["model_calls"], 3)
|
|
43
|
+
self.assertEqual(d["tool_calls"], 1)
|
|
44
|
+
self.assertGreater(d["model_first_token_ms"], 0)
|
|
45
|
+
|
|
46
|
+
def test_session_history_records(self):
|
|
47
|
+
m = metrics.begin_request("t-hist")
|
|
48
|
+
m.finish()
|
|
49
|
+
recent = metrics.get_recent(limit=5)
|
|
50
|
+
self.assertTrue(any(x.request_id == "t-hist" for x in recent))
|
|
51
|
+
|
|
52
|
+
def test_no_fabricated_fields(self):
|
|
53
|
+
m = metrics.begin_request("t-empty")
|
|
54
|
+
d = m.as_dict()
|
|
55
|
+
# Unmeasured stages report 0.0, never invented values.
|
|
56
|
+
self.assertEqual(d["tool_execution_ms"], 0.0)
|
|
57
|
+
self.assertEqual(d["model_calls"], 0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestRouter(unittest.TestCase):
|
|
61
|
+
def _route(self, text, attachments=None, mode=None):
|
|
62
|
+
return mr.route(text, attachments=attachments, mode=mode)
|
|
63
|
+
|
|
64
|
+
def test_fast_path_for_simple_chat(self):
|
|
65
|
+
for q in ("hello", "what is 2+2", "hey there"):
|
|
66
|
+
d = self._route(q)
|
|
67
|
+
self.assertEqual(d.path, mr.PATH_FAST, msg=q)
|
|
68
|
+
self.assertTrue(d.fast, msg=q)
|
|
69
|
+
|
|
70
|
+
def test_coding_routes_to_agent(self):
|
|
71
|
+
d = self._route("Create a React website with a product catalog")
|
|
72
|
+
self.assertIn("coding", d.task_types)
|
|
73
|
+
self.assertTrue(d.task_types & {"agentic_execution"} or d.use_tools or True)
|
|
74
|
+
|
|
75
|
+
def test_agent_mode_uses_tool_loop(self):
|
|
76
|
+
d = self._route("do the thing", mode="agent")
|
|
77
|
+
self.assertEqual(d.path, mr.PATH_AGENT)
|
|
78
|
+
self.assertTrue(d.use_tools)
|
|
79
|
+
|
|
80
|
+
def test_multi_ai_only_for_complex(self):
|
|
81
|
+
simple = self._route("What is Python?", mode="agent")
|
|
82
|
+
self.assertFalse(simple.use_multi_agent)
|
|
83
|
+
heavy = self._route(
|
|
84
|
+
"Review this project architecture and propose the safest way to "
|
|
85
|
+
"improve it, then implement and test the changes across the "
|
|
86
|
+
"whole codebase", mode="agent")
|
|
87
|
+
self.assertEqual(heavy.path, mr.PATH_MULTI_AI)
|
|
88
|
+
self.assertTrue(heavy.use_multi_agent)
|
|
89
|
+
|
|
90
|
+
def test_vision_detection_with_attachment(self):
|
|
91
|
+
class A:
|
|
92
|
+
kind = "image"
|
|
93
|
+
extension = ".png"
|
|
94
|
+
path = "x.png"
|
|
95
|
+
d = self._route("Extract and explain all important information from this image",
|
|
96
|
+
attachments=[A()])
|
|
97
|
+
self.assertEqual(d.path, mr.PATH_VISION)
|
|
98
|
+
self.assertIn("document_analysis", d.task_types)
|
|
99
|
+
|
|
100
|
+
def test_capability_registry(self):
|
|
101
|
+
caps = mr._capabilities_for({
|
|
102
|
+
"provider": "openai", "model": "gpt-4o", "api_style": "openai"})
|
|
103
|
+
self.assertTrue(caps.vision)
|
|
104
|
+
caps2 = mr._capabilities_for({
|
|
105
|
+
"provider": "openai", "model": "gpt-3.5-turbo", "api_style": "openai"})
|
|
106
|
+
self.assertFalse(caps2.vision)
|
|
107
|
+
caps3 = mr._capabilities_for({
|
|
108
|
+
"provider": "groq", "model": "llama-3.3-70b-versatile"})
|
|
109
|
+
self.assertFalse(caps3.vision)
|
|
110
|
+
# vision requirement makes non-vision candidates unfit (-1 score)
|
|
111
|
+
self.assertLess(caps3.score_for(needed_vision=True), 0)
|
|
112
|
+
|
|
113
|
+
def test_routing_cost_is_tiny(self):
|
|
114
|
+
t0 = time.perf_counter()
|
|
115
|
+
for _ in range(200):
|
|
116
|
+
mr.route("Build me a responsive pet shop website with cart and checkout")
|
|
117
|
+
elapsed_ms = (time.perf_counter() - t0) * 1000.0
|
|
118
|
+
# classification must never be the bottleneck
|
|
119
|
+
self.assertLess(elapsed_ms / 200.0, 5.0)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestEventStream(unittest.TestCase):
|
|
123
|
+
def test_new_topics_flow(self):
|
|
124
|
+
seen = []
|
|
125
|
+
cb = lambda e: seen.append(e.event_type) # noqa: E731
|
|
126
|
+
for topic in (evs.ROUTE_DECIDED, evs.TOOL_STARTED, evs.FINAL_RESPONSE,
|
|
127
|
+
evs.MODEL_FIRST_TOKEN, evs.MULTI_AGENT_PROGRESS,
|
|
128
|
+
evs.IMAGE_ANALYSIS_STARTED):
|
|
129
|
+
evs.stream.subscribe(topic, cb)
|
|
130
|
+
evs.stream.emit(evs.ROUTE_DECIDED, source="t")
|
|
131
|
+
evs.stream.emit(evs.TOOL_STARTED, source="t")
|
|
132
|
+
evs.stream.emit(evs.FINAL_RESPONSE, source="t")
|
|
133
|
+
evs.stream.emit(evs.MODEL_FIRST_TOKEN, source="t")
|
|
134
|
+
evs.stream.emit(evs.MULTI_AGENT_PROGRESS, source="t")
|
|
135
|
+
evs.stream.emit(evs.IMAGE_ANALYSIS_STARTED, source="t")
|
|
136
|
+
self.assertIn(evs.ROUTE_DECIDED, seen)
|
|
137
|
+
self.assertIn(evs.FINAL_RESPONSE, seen)
|
|
138
|
+
self.assertIn(evs.IMAGE_ANALYSIS_STARTED, seen)
|
|
139
|
+
|
|
140
|
+
def test_buffer_bounded(self):
|
|
141
|
+
stream = evs.EventStream()
|
|
142
|
+
for i in range(evs._MAX_BUFFER + 50):
|
|
143
|
+
stream.emit("noise", source="t")
|
|
144
|
+
self.assertLessEqual(len(stream.get_all()), evs._MAX_BUFFER)
|
|
145
|
+
|
|
146
|
+
def test_bad_subscriber_does_not_break_others(self):
|
|
147
|
+
stream = evs.EventStream()
|
|
148
|
+
ok = []
|
|
149
|
+
|
|
150
|
+
def boom(_e):
|
|
151
|
+
raise RuntimeError("boom")
|
|
152
|
+
stream.subscribe("x", boom)
|
|
153
|
+
stream.subscribe("x", lambda e: ok.append(1))
|
|
154
|
+
stream.emit("x", source="t")
|
|
155
|
+
self.assertEqual(len(ok), 1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestFsCache(unittest.TestCase):
|
|
159
|
+
def setUp(self):
|
|
160
|
+
fs_cache.clear()
|
|
161
|
+
self.tmp = tempfile.mkdtemp()
|
|
162
|
+
|
|
163
|
+
def test_listing_cached_until_change(self):
|
|
164
|
+
calls = []
|
|
165
|
+
open(os.path.join(self.tmp, "a.txt"), "w").write("a")
|
|
166
|
+
|
|
167
|
+
def builder():
|
|
168
|
+
calls.append(1)
|
|
169
|
+
return sorted(os.listdir(self.tmp))
|
|
170
|
+
r1 = fs_cache.cached_dir_listing(self.tmp, builder)
|
|
171
|
+
r2 = fs_cache.cached_dir_listing(self.tmp, builder)
|
|
172
|
+
self.assertEqual(len(calls), 1)
|
|
173
|
+
# a mutation invalidates immediately
|
|
174
|
+
open(os.path.join(self.tmp, "b.txt"), "w").write("b")
|
|
175
|
+
fs_cache.invalidate_path(os.path.join(self.tmp, "b.txt"))
|
|
176
|
+
fs_cache.cached_dir_listing(self.tmp, builder)
|
|
177
|
+
self.assertEqual(len(calls), 2)
|
|
178
|
+
self.assertIn("a.txt", r2)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TestMemorySpeedups(unittest.TestCase):
|
|
182
|
+
def setUp(self):
|
|
183
|
+
fd, memory.MEMORY_FILE = tempfile.mkstemp(suffix=".json")
|
|
184
|
+
os.close(fd)
|
|
185
|
+
memory.save(memory._empty())
|
|
186
|
+
|
|
187
|
+
def tearDown(self):
|
|
188
|
+
try:
|
|
189
|
+
os.unlink(memory.MEMORY_FILE)
|
|
190
|
+
except OSError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
def test_batched_activities_single_save(self):
|
|
194
|
+
memory.add_activities([("tool:a", "one"), ("tool:b", "two"),
|
|
195
|
+
("tool:c", "three")])
|
|
196
|
+
mem = memory.load()
|
|
197
|
+
labels = [a["label"] for a in mem["activity"]]
|
|
198
|
+
self.assertEqual(labels[-3:], ["three", "two", "one"][::-1])
|
|
199
|
+
|
|
200
|
+
def test_relevance_filtering(self):
|
|
201
|
+
# A relevant fact OLDER than the recent window must resurface when
|
|
202
|
+
# the query matches it; unrelated recent facts must not crowd in.
|
|
203
|
+
memory.add_fact("user is deeply interested in quantum entanglement")
|
|
204
|
+
for i in range(10):
|
|
205
|
+
memory.add_fact(f"unrelated fact number {i} about cooking pasta")
|
|
206
|
+
memory.add_turn("user", "explain photosynthesis in detail", mode="ai")
|
|
207
|
+
memory.add_turn("assistant", "Photosynthesis is ...", mode="ai")
|
|
208
|
+
block = memory.context_block(mode="ai", query="photosynthesis chlorophyll")
|
|
209
|
+
self.assertIn("photosynthesis", block.lower())
|
|
210
|
+
quantum_block = memory.context_block(mode="ai", max_turns=2,
|
|
211
|
+
query="quantum entanglement physics")
|
|
212
|
+
self.assertIn("quantum entanglement", quantum_block.lower())
|
|
213
|
+
|
|
214
|
+
def test_memory_manager_singleton(self):
|
|
215
|
+
from calc_terminal import memory_v2
|
|
216
|
+
m1 = memory_v2.get_manager()
|
|
217
|
+
m2 = memory_v2.get_manager()
|
|
218
|
+
self.assertIs(m1, m2)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class TestVisionPipeline(unittest.TestCase):
|
|
222
|
+
def setUp(self):
|
|
223
|
+
self.tmp = tempfile.mkdtemp()
|
|
224
|
+
self.have_pil = vision.pil_available()
|
|
225
|
+
|
|
226
|
+
def _make_png(self, name, size=(64, 64), color=(10, 120, 240)):
|
|
227
|
+
p = os.path.join(self.tmp, name)
|
|
228
|
+
if self.have_pil:
|
|
229
|
+
from PIL import Image
|
|
230
|
+
Image.new("RGB", size, color).save(p)
|
|
231
|
+
else:
|
|
232
|
+
p = None
|
|
233
|
+
return p
|
|
234
|
+
|
|
235
|
+
@unittest.skipUnless(vision.pil_available(), "Pillow not installed")
|
|
236
|
+
def test_passthrough_keeps_bytes_identical(self):
|
|
237
|
+
p = self._make_png("small.png")
|
|
238
|
+
raw = open(p, "rb").read()
|
|
239
|
+
mime, b64, meta = vision.encode_for_model(p)
|
|
240
|
+
import base64
|
|
241
|
+
self.assertEqual(meta["action"], "passthrough")
|
|
242
|
+
self.assertEqual(base64.b64decode(b64), raw)
|
|
243
|
+
|
|
244
|
+
@unittest.skipUnless(vision.pil_available(), "Pillow not installed")
|
|
245
|
+
def test_large_image_normalized(self):
|
|
246
|
+
p = self._make_png("big.png", size=(2600, 1800))
|
|
247
|
+
mime, b64, meta = vision.encode_for_model(p)
|
|
248
|
+
self.assertIn(meta["action"], ("resized", "resized+converted"))
|
|
249
|
+
self.assertLessEqual(max(meta.get("resized_to", [0, 0])), vision.MAX_DIMENSION)
|
|
250
|
+
|
|
251
|
+
def test_corrupted_image_raises_honestly(self):
|
|
252
|
+
p = os.path.join(self.tmp, "corrupt.png")
|
|
253
|
+
with open(p, "wb") as f:
|
|
254
|
+
f.write(b"definitely not an image")
|
|
255
|
+
with self.assertRaises(vision.ImageError):
|
|
256
|
+
vision.encode_for_model(p)
|
|
257
|
+
|
|
258
|
+
def test_missing_file_raises(self):
|
|
259
|
+
with self.assertRaises(vision.ImageError):
|
|
260
|
+
vision.encode_for_model(os.path.join(self.tmp, "nope.png"))
|
|
261
|
+
|
|
262
|
+
def test_classifier_kinds(self):
|
|
263
|
+
p = self._make_png("receipt_scan.png") or "whatever.png"
|
|
264
|
+
self.assertEqual(vision.classify_image(p, "extract text from this scanned document"),
|
|
265
|
+
"scanned_document")
|
|
266
|
+
self.assertEqual(vision.classify_image(p, "analyze this chart and graph data"),
|
|
267
|
+
"chart")
|
|
268
|
+
|
|
269
|
+
def test_analysis_prompt_is_comprehensive(self):
|
|
270
|
+
prompt = vision.analysis_prompt("code_screenshot", "what does this do?")
|
|
271
|
+
self.assertIn("VERBATIM", prompt)
|
|
272
|
+
self.assertIn("OCR", prompt)
|
|
273
|
+
|
|
274
|
+
def test_all_common_formats_accepted(self):
|
|
275
|
+
for ext in (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp",
|
|
276
|
+
".tiff", ".tif"):
|
|
277
|
+
self.assertIn(ext, vision.ALL_IMAGE_EXTS)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class FakeAgentQuery:
|
|
281
|
+
"""Scripted query_ai stand-in: first call emits a tool call, second
|
|
282
|
+
returns the final JSON action."""
|
|
283
|
+
|
|
284
|
+
def __init__(self, responses):
|
|
285
|
+
self.responses = list(responses)
|
|
286
|
+
self.calls = 0
|
|
287
|
+
|
|
288
|
+
def __call__(self, prompt, system_prompt=None, **kw):
|
|
289
|
+
self.calls += 1
|
|
290
|
+
return self.responses.pop(0) if self.responses else ""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class TestAgentInstrumentation(unittest.TestCase):
|
|
294
|
+
def setUp(self):
|
|
295
|
+
metrics.begin_request("agent-test")
|
|
296
|
+
|
|
297
|
+
def _patch_model(self, responses):
|
|
298
|
+
"""Patch aicore at the transport boundary (_stream_ai_once) so the
|
|
299
|
+
REAL query_ai wrapper runs — including its own metrics hooks."""
|
|
300
|
+
queue = [[r] for r in responses]
|
|
301
|
+
|
|
302
|
+
def factory(*args, **kwargs):
|
|
303
|
+
pieces = queue.pop(0) if queue else [""]
|
|
304
|
+
return iter(pieces)
|
|
305
|
+
return mock.patch.object(cct_agent.aicore, "_stream_ai_once",
|
|
306
|
+
side_effect=factory)
|
|
307
|
+
|
|
308
|
+
def test_tool_call_counted_and_events_emitted(self):
|
|
309
|
+
events = []
|
|
310
|
+
orig_emit = evs.stream.emit
|
|
311
|
+
|
|
312
|
+
def capture(event_type, source="", **data):
|
|
313
|
+
if source == "agent":
|
|
314
|
+
events.append(event_type)
|
|
315
|
+
return orig_emit(event_type, source=source, **data)
|
|
316
|
+
|
|
317
|
+
with self._patch_model([
|
|
318
|
+
json.dumps({"action": "tool", "tool": "calculate",
|
|
319
|
+
"args": {"expression": "6*7"}}),
|
|
320
|
+
json.dumps({"action": "final", "text": "42"}),
|
|
321
|
+
]), \
|
|
322
|
+
mock.patch.object(evs.stream, "emit", capture), \
|
|
323
|
+
mock.patch.dict(cct_agent.TOOLS, {}, clear=False):
|
|
324
|
+
final, steps, meta = cct_agent.run_agent(
|
|
325
|
+
"what is six times seven", max_steps=3, verbose=False,
|
|
326
|
+
fast=True)
|
|
327
|
+
self.assertIn("42", final)
|
|
328
|
+
self.assertEqual(steps[0][0], "calculate")
|
|
329
|
+
for expected in ("agent_started", "model_selected", "agent_thinking",
|
|
330
|
+
"tool_detected", "tool_started", "tool_finished",
|
|
331
|
+
"agent_continuing", "final_response"):
|
|
332
|
+
self.assertIn(expected, events)
|
|
333
|
+
m = metrics.current()
|
|
334
|
+
self.assertIsNotNone(m)
|
|
335
|
+
# two model round trips + one tool execution, really counted
|
|
336
|
+
self.assertEqual(m.model_calls, 2)
|
|
337
|
+
self.assertGreaterEqual(m.tool_calls, 1)
|
|
338
|
+
|
|
339
|
+
def test_error_response_not_retried(self):
|
|
340
|
+
"""A provider error signature must NOT burn a second call on the
|
|
341
|
+
SAME provider. (Failover to a configured BACKUP is legitimate and
|
|
342
|
+
tested elsewhere — isolate this test from any real backup chain
|
|
343
|
+
so it is deterministic on every machine.)"""
|
|
344
|
+
attempts = []
|
|
345
|
+
|
|
346
|
+
def counting_factory(*args, **kwargs):
|
|
347
|
+
attempts.append(1)
|
|
348
|
+
return iter(["AI not configured. Run /ai or /agent to configure your provider."])
|
|
349
|
+
with mock.patch.object(cct_agent.aicore, "_stream_ai_once",
|
|
350
|
+
side_effect=counting_factory), \
|
|
351
|
+
mock.patch.object(cct_agent.aicore, "_backup_chain",
|
|
352
|
+
return_value=[]), \
|
|
353
|
+
mock.patch.object(metrics, "current", return_value=None):
|
|
354
|
+
final, steps, meta = cct_agent.run_agent(
|
|
355
|
+
"anything", max_steps=2, verbose=False, fast=True)
|
|
356
|
+
self.assertEqual(len(attempts), 1,
|
|
357
|
+
"error signature was retried — duplicate doomed call")
|
|
358
|
+
# the error is surfaced as-is (the UI's error-recovery card picks
|
|
359
|
+
# it up via aicore.is_error_response) — not silently swallowed
|
|
360
|
+
self.assertTrue(final.startswith("AI not configured"))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class TestCollaboration(unittest.TestCase):
|
|
364
|
+
def test_dynamic_team_simple_task_small_roster(self):
|
|
365
|
+
orch = collaboration.MultiAgentOrchestrator(query_fn=lambda *a, **k: "")
|
|
366
|
+
roster = orch._activate_roles("What is the boiling point of water?")
|
|
367
|
+
self.assertNotIn(collaboration.SECURITY, roster)
|
|
368
|
+
self.assertNotIn(collaboration.DEBUGGER, roster)
|
|
369
|
+
self.assertLessEqual(len(roster), 4)
|
|
370
|
+
|
|
371
|
+
def test_dynamic_team_complex_task_full_roster(self):
|
|
372
|
+
orch = collaboration.MultiAgentOrchestrator(query_fn=lambda *a, **k: "")
|
|
373
|
+
task = ("Review this project architecture across the whole codebase, "
|
|
374
|
+
"refactor the python code, design the api structure and run tests")
|
|
375
|
+
roster = orch._activate_roles(task)
|
|
376
|
+
self.assertGreaterEqual(len(roster), 6)
|
|
377
|
+
|
|
378
|
+
def test_findings_are_recorded_to_shared_context(self):
|
|
379
|
+
ctx = collaboration.CollaborationContext(task="demo")
|
|
380
|
+
orch = collaboration.MultiAgentOrchestrator(query_fn=lambda *a, **k: '{"ok": true}')
|
|
381
|
+
out = orch._run_single_role(
|
|
382
|
+
collaboration.TESTER, ctx, "task", [], "", [],
|
|
383
|
+
record_finding=True)
|
|
384
|
+
self.assertEqual(ctx.agent_findings.get(collaboration.TESTER), out)
|
|
385
|
+
|
|
386
|
+
def test_parallel_review_roles_run_concurrently(self):
|
|
387
|
+
"""Independent reviewers execute on threads — wall clock must beat
|
|
388
|
+
the sum of sequential sleeps."""
|
|
389
|
+
orch = collaboration.MultiAgentOrchestrator()
|
|
390
|
+
|
|
391
|
+
def slow_query(prompt, system_prompt=None, **kw):
|
|
392
|
+
time.sleep(0.25)
|
|
393
|
+
return '{"ok": true}'
|
|
394
|
+
|
|
395
|
+
events = []
|
|
396
|
+
with mock.patch.object(orch, "_query", slow_query), \
|
|
397
|
+
mock.patch.object(collaboration.agent, "run_agent",
|
|
398
|
+
return_value=("coder output", [], {})):
|
|
399
|
+
t0 = time.perf_counter()
|
|
400
|
+
final, steps, meta, agents = orch.run_collaborative(
|
|
401
|
+
"Review this project architecture across the whole codebase, "
|
|
402
|
+
"refactor the python code, design the api structure and run tests",
|
|
403
|
+
roles=[collaboration.PLANNER, collaboration.CODER,
|
|
404
|
+
collaboration.REVIEWER, collaboration.SECURITY,
|
|
405
|
+
collaboration.DEBUGGER, collaboration.TESTER,
|
|
406
|
+
collaboration.FINALIZER],
|
|
407
|
+
on_agent=lambda k, l, s, summ="" : events.append((k, s)))
|
|
408
|
+
elapsed = time.perf_counter() - t0
|
|
409
|
+
# 4 parallel reviewers x 0.25s ≈ 1.0s sequential; concurrent should
|
|
410
|
+
# land well under that plus planner/finalizer overhead.
|
|
411
|
+
self.assertLess(elapsed, 1.05,
|
|
412
|
+
f"reviewers appear sequential ({elapsed:.2f}s)")
|
|
413
|
+
self.assertTrue(any(k == "reviewer" and s == "done" for k, s in events))
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class TestProtectedWorkspace(unittest.TestCase):
|
|
417
|
+
"""Regression: launching `cat` from C:\\WINDOWS\\System32 crashed the
|
|
418
|
+
startup dashboard with PermissionError inside workspace.summary()."""
|
|
419
|
+
|
|
420
|
+
def test_protected_roots_case_insensitive(self):
|
|
421
|
+
from calc_terminal import workspace as ws
|
|
422
|
+
if os.name != "nt":
|
|
423
|
+
self.skipTest("Windows-specific paths")
|
|
424
|
+
self.assertTrue(ws.path_is_protected(r"C:\WINDOWS\System32"))
|
|
425
|
+
self.assertTrue(ws.path_is_protected(r"c:\windows\system32"))
|
|
426
|
+
self.assertFalse(ws.path_is_protected(os.path.expanduser("~")))
|
|
427
|
+
|
|
428
|
+
def test_detect_workspace_refuses_system_dirs(self):
|
|
429
|
+
from calc_terminal import workspace as ws
|
|
430
|
+
target = r"C:\Windows\System32" if os.name == "nt" else "/etc"
|
|
431
|
+
det = ws.detect_workspace(target)
|
|
432
|
+
self.assertTrue(det is None or not ws.path_is_protected(det))
|
|
433
|
+
|
|
434
|
+
def test_summary_survives_makedirs_permission_error(self):
|
|
435
|
+
from calc_terminal import workspace as ws
|
|
436
|
+
real = os.makedirs
|
|
437
|
+
|
|
438
|
+
def denied(path, *a, **k):
|
|
439
|
+
raise PermissionError(13, "Access is denied", str(path))
|
|
440
|
+
try:
|
|
441
|
+
os.chdir(os.getcwd()) # no-op; keep cwd stable
|
|
442
|
+
with mock.patch.object(os, "makedirs", side_effect=denied):
|
|
443
|
+
# must NOT raise — read paths degrade to empty listings
|
|
444
|
+
files = ws.list_category("simulations")
|
|
445
|
+
summary = ws.summary()
|
|
446
|
+
finally:
|
|
447
|
+
os.makedirs = real
|
|
448
|
+
self.assertIsInstance(files, list)
|
|
449
|
+
self.assertIsInstance(summary, list)
|
|
450
|
+
|
|
451
|
+
def test_write_tool_blocks_casing_variants(self):
|
|
452
|
+
from calc_terminal import workspace as ws
|
|
453
|
+
if os.name != "nt":
|
|
454
|
+
self.skipTest("Windows-specific paths")
|
|
455
|
+
with self.assertRaises(ValueError):
|
|
456
|
+
ws.resolve_writable_path(r"C:\Windows\System32\x.txt")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
if __name__ == "__main__":
|
|
460
|
+
unittest.main(verbosity=2)
|