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,557 @@
|
|
|
1
|
+
"""Test suite for the v0.7 overhaul modules."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import time
|
|
9
|
+
import threading
|
|
10
|
+
import unittest
|
|
11
|
+
|
|
12
|
+
# Add parent directory so calc_terminal is importable as a package
|
|
13
|
+
_parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
14
|
+
if _parent not in sys.path:
|
|
15
|
+
sys.path.insert(0, _parent)
|
|
16
|
+
|
|
17
|
+
from calc_terminal.tool_call_normalizer import (
|
|
18
|
+
ToolCall,
|
|
19
|
+
normalize_tool_calls,
|
|
20
|
+
extract_final_answer,
|
|
21
|
+
is_tool_call_response,
|
|
22
|
+
_find_balanced_json,
|
|
23
|
+
)
|
|
24
|
+
from calc_terminal.memory_v2 import MemoryManager, Memory, _sanitize_content, VALID_CATEGORIES
|
|
25
|
+
from calc_terminal.event_stream import EventStream, Event, MODEL_RESPONSE, TOOL_REQUEST
|
|
26
|
+
from calc_terminal.agent_runtime import AgentRuntime
|
|
27
|
+
from calc_terminal.collaboration import FileLockManager, CollaborationContext
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestToolCallNormalizer(unittest.TestCase):
|
|
31
|
+
|
|
32
|
+
def test_empty_input(self):
|
|
33
|
+
self.assertEqual(normalize_tool_calls(""), [])
|
|
34
|
+
self.assertEqual(normalize_tool_calls(None), [])
|
|
35
|
+
|
|
36
|
+
def test_json_protocol_tool_args(self):
|
|
37
|
+
text = '{"action": "tool", "tool": "calc", "args": {"expr": "2+2"}}'
|
|
38
|
+
calls = normalize_tool_calls(text, known_tools=["calc"])
|
|
39
|
+
self.assertEqual(len(calls), 1)
|
|
40
|
+
self.assertEqual(calls[0].name, "calc")
|
|
41
|
+
self.assertEqual(calls[0].arguments["expr"], "2+2")
|
|
42
|
+
self.assertEqual(calls[0].source_format, "json_protocol")
|
|
43
|
+
self.assertAlmostEqual(calls[0].confidence, 0.95)
|
|
44
|
+
|
|
45
|
+
def test_json_protocol_tool_parameters(self):
|
|
46
|
+
text = '{"action": "tool", "tool": "search", "parameters": {"query": "hello"}}'
|
|
47
|
+
calls = normalize_tool_calls(text, known_tools=["search"])
|
|
48
|
+
self.assertEqual(len(calls), 1)
|
|
49
|
+
self.assertEqual(calls[0].name, "search")
|
|
50
|
+
self.assertEqual(calls[0].arguments["query"], "hello")
|
|
51
|
+
|
|
52
|
+
def test_json_name_arguments_format(self):
|
|
53
|
+
text = '{"name": "write_file", "arguments": {"path": "/tmp/x.py", "content": "print()"}}'
|
|
54
|
+
calls = normalize_tool_calls(text, known_tools=["write_file"])
|
|
55
|
+
self.assertEqual(len(calls), 1)
|
|
56
|
+
self.assertEqual(calls[0].name, "write_file")
|
|
57
|
+
self.assertEqual(calls[0].arguments["path"], "/tmp/x.py")
|
|
58
|
+
|
|
59
|
+
def test_unknown_tool_filtered(self):
|
|
60
|
+
text = '{"action": "tool", "tool": "nonexistent", "args": {}}'
|
|
61
|
+
calls = normalize_tool_calls(text, known_tools=["calc"])
|
|
62
|
+
self.assertEqual(len(calls), 0)
|
|
63
|
+
|
|
64
|
+
def test_xml_minimax_toolcall(self):
|
|
65
|
+
text = ('<minimax:toolcall>\n'
|
|
66
|
+
' <tool_name>calc</tool_name>\n'
|
|
67
|
+
' <parameters>{"expr": "3*7"}</parameters>\n'
|
|
68
|
+
'</minimax:toolcall>')
|
|
69
|
+
calls = normalize_tool_calls(text, known_tools=["calc"])
|
|
70
|
+
self.assertEqual(len(calls), 1)
|
|
71
|
+
self.assertEqual(calls[0].name, "calc")
|
|
72
|
+
self.assertEqual(calls[0].arguments["expr"], "3*7")
|
|
73
|
+
self.assertIn("xml", calls[0].source_format)
|
|
74
|
+
|
|
75
|
+
def test_xml_tool_call_tag(self):
|
|
76
|
+
text = '<tool_call>\n<name>search</name>\n<arguments>{"q": "test"}</arguments>\n</tool_call>'
|
|
77
|
+
calls = normalize_tool_calls(text, known_tools=["search", "search_workspace"])
|
|
78
|
+
self.assertEqual(len(calls), 1)
|
|
79
|
+
self.assertEqual(calls[0].name, "search_workspace")
|
|
80
|
+
|
|
81
|
+
def test_xml_invoke(self):
|
|
82
|
+
text = '<invoke name="calc"><parameters>{"expr": "1+1"}</parameters></invoke>'
|
|
83
|
+
calls = normalize_tool_calls(text, known_tools=["calc"])
|
|
84
|
+
self.assertEqual(len(calls), 1)
|
|
85
|
+
self.assertEqual(calls[0].name, "calc")
|
|
86
|
+
|
|
87
|
+
def test_function_call_syntax(self):
|
|
88
|
+
text = 'calc(expr="2+3")'
|
|
89
|
+
calls = normalize_tool_calls(text, known_tools=["calc"])
|
|
90
|
+
self.assertTrue(len(calls) >= 1)
|
|
91
|
+
self.assertEqual(calls[0].name, "calc")
|
|
92
|
+
|
|
93
|
+
def test_heuristic_fallback(self):
|
|
94
|
+
text = 'Please use calc with {"expr": "5*5"}'
|
|
95
|
+
calls = normalize_tool_calls(text, known_tools=["calc"])
|
|
96
|
+
self.assertTrue(len(calls) >= 1)
|
|
97
|
+
self.assertEqual(calls[0].name, "calc")
|
|
98
|
+
|
|
99
|
+
def test_deduplication(self):
|
|
100
|
+
text = ('{"action": "tool", "tool": "calc", "args": {"expr": "1"}}\n'
|
|
101
|
+
'{"action": "tool", "tool": "calc", "args": {"expr": "1"}}')
|
|
102
|
+
calls = normalize_tool_calls(text, known_tools=["calc"])
|
|
103
|
+
self.assertEqual(len(calls), 1)
|
|
104
|
+
|
|
105
|
+
def test_sorted_by_confidence(self):
|
|
106
|
+
text = ('{"action": "tool", "tool": "calc", "args": {"expr": "1"}}\n'
|
|
107
|
+
'calc(expr="1")')
|
|
108
|
+
calls = normalize_tool_calls(text, known_tools=["calc"])
|
|
109
|
+
if len(calls) >= 2:
|
|
110
|
+
self.assertGreaterEqual(calls[0].confidence, calls[1].confidence)
|
|
111
|
+
|
|
112
|
+
def test_extract_final_answer_pattern(self):
|
|
113
|
+
self.assertEqual(extract_final_answer("The answer is 42."), "42")
|
|
114
|
+
self.assertEqual(extract_final_answer("RESULT: 99"), "99")
|
|
115
|
+
self.assertIsNone(extract_final_answer(""))
|
|
116
|
+
|
|
117
|
+
def test_extract_final_answer_long_text(self):
|
|
118
|
+
long_text = "x" * 600
|
|
119
|
+
self.assertIsNone(extract_final_answer(long_text))
|
|
120
|
+
|
|
121
|
+
def test_is_tool_call_response_true(self):
|
|
122
|
+
self.assertTrue(is_tool_call_response('{"tool": "calc"}'))
|
|
123
|
+
self.assertTrue(is_tool_call_response('<minimax:toolcall>'))
|
|
124
|
+
self.assertTrue(is_tool_call_response('<tool_call>'))
|
|
125
|
+
self.assertTrue(is_tool_call_response('<invoke name="x">'))
|
|
126
|
+
|
|
127
|
+
def test_is_tool_call_response_false(self):
|
|
128
|
+
self.assertFalse(is_tool_call_response(""))
|
|
129
|
+
self.assertFalse(is_tool_call_response(None))
|
|
130
|
+
self.assertFalse(is_tool_call_response("Hello world, no tools here."))
|
|
131
|
+
|
|
132
|
+
def test_find_balanced_json(self):
|
|
133
|
+
result = _find_balanced_json('prefix {"a": 1} suffix', 7)
|
|
134
|
+
self.assertEqual(result, '{"a": 1}')
|
|
135
|
+
|
|
136
|
+
def test_find_balanced_json_no_brace(self):
|
|
137
|
+
result = _find_balanced_json("no braces", 0)
|
|
138
|
+
self.assertIsNone(result)
|
|
139
|
+
|
|
140
|
+
def test_tool_call_dataclass_defaults(self):
|
|
141
|
+
tc = ToolCall()
|
|
142
|
+
self.assertTrue(tc.id)
|
|
143
|
+
self.assertEqual(tc.name, "")
|
|
144
|
+
self.assertEqual(tc.arguments, {})
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestMemoryV2(unittest.TestCase):
|
|
148
|
+
|
|
149
|
+
def setUp(self):
|
|
150
|
+
self.tmp_dir = tempfile.mkdtemp(prefix="cct_test_mem_")
|
|
151
|
+
self.mm = MemoryManager(memory_dir=self.tmp_dir)
|
|
152
|
+
|
|
153
|
+
def tearDown(self):
|
|
154
|
+
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
|
155
|
+
|
|
156
|
+
def test_sanitize_api_key(self):
|
|
157
|
+
raw = "api_key = sk-abcdefghijklmnop1234"
|
|
158
|
+
result = _sanitize_content(raw)
|
|
159
|
+
self.assertNotIn("sk-abcdefghijklmnop1234", result)
|
|
160
|
+
self.assertIn("[REDACTED]", result)
|
|
161
|
+
|
|
162
|
+
def test_sanitize_bearer_token(self):
|
|
163
|
+
raw = "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.abcdef"
|
|
164
|
+
result = _sanitize_content(raw)
|
|
165
|
+
self.assertNotIn("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.abcdef", result)
|
|
166
|
+
|
|
167
|
+
def test_sanitize_clean_text(self):
|
|
168
|
+
raw = "Just a normal sentence with no secrets."
|
|
169
|
+
self.assertEqual(_sanitize_content(raw), raw)
|
|
170
|
+
|
|
171
|
+
def test_valid_categories(self):
|
|
172
|
+
self.assertIn("user_preferences", VALID_CATEGORIES)
|
|
173
|
+
self.assertIn("project_facts", VALID_CATEGORIES)
|
|
174
|
+
self.assertIn("custom", VALID_CATEGORIES)
|
|
175
|
+
self.assertEqual(len(VALID_CATEGORIES), 8)
|
|
176
|
+
|
|
177
|
+
def test_session_add_and_retrieve(self):
|
|
178
|
+
self.mm.add_session_turn("user", "Hello", mode="chat")
|
|
179
|
+
self.mm.add_session_turn("assistant", "Hi there", mode="chat")
|
|
180
|
+
ctx = self.mm.get_session_context(max_turns=10)
|
|
181
|
+
self.assertEqual(len(ctx), 2)
|
|
182
|
+
self.assertEqual(ctx[0]["role"], "user")
|
|
183
|
+
self.assertEqual(ctx[1]["role"], "assistant")
|
|
184
|
+
|
|
185
|
+
def test_session_max_turns(self):
|
|
186
|
+
for i in range(5):
|
|
187
|
+
self.mm.add_session_turn("user", f"msg {i}")
|
|
188
|
+
ctx = self.mm.get_session_context(max_turns=3)
|
|
189
|
+
self.assertEqual(len(ctx), 3)
|
|
190
|
+
self.assertEqual(ctx[0]["text"], "msg 2")
|
|
191
|
+
|
|
192
|
+
def test_session_clear(self):
|
|
193
|
+
self.mm.add_session_turn("user", "test")
|
|
194
|
+
self.mm.clear_session()
|
|
195
|
+
self.assertEqual(self.mm.get_session_context(), [])
|
|
196
|
+
|
|
197
|
+
def test_store_and_retrieve_memory(self):
|
|
198
|
+
mem = self.mm.store_memory("conversation_facts", "User prefers Python")
|
|
199
|
+
self.assertTrue(mem.id)
|
|
200
|
+
self.assertEqual(mem.category, "conversation_facts")
|
|
201
|
+
self.assertEqual(mem.content, "User prefers Python")
|
|
202
|
+
retrieved = self.mm.retrieve_memories(limit=10)
|
|
203
|
+
self.assertEqual(len(retrieved), 1)
|
|
204
|
+
self.assertEqual(retrieved[0].id, mem.id)
|
|
205
|
+
|
|
206
|
+
def test_store_memory_sanitize(self):
|
|
207
|
+
mem = self.mm.store_memory("custom", "api_key = secret123456789012345")
|
|
208
|
+
self.assertNotIn("secret123456789012345", mem.content)
|
|
209
|
+
self.assertIn("[REDACTED]", mem.content)
|
|
210
|
+
|
|
211
|
+
def test_invalid_category_raises(self):
|
|
212
|
+
with self.assertRaises(ValueError):
|
|
213
|
+
self.mm.store_memory("invalid_category", "test")
|
|
214
|
+
|
|
215
|
+
def test_delete_memory(self):
|
|
216
|
+
mem = self.mm.store_memory("custom", "to delete")
|
|
217
|
+
self.assertTrue(self.mm.delete_memory(mem.id))
|
|
218
|
+
self.assertEqual(self.mm.delete_memory(mem.id), False)
|
|
219
|
+
|
|
220
|
+
def test_search_memories(self):
|
|
221
|
+
self.mm.store_memory("conversation_facts", "User likes blue color")
|
|
222
|
+
self.mm.store_memory("conversation_facts", "User likes red color")
|
|
223
|
+
self.mm.store_memory("custom", "Unrelated fact")
|
|
224
|
+
results = self.mm.search_memories("blue")
|
|
225
|
+
self.assertEqual(len(results), 1)
|
|
226
|
+
self.assertIn("blue", results[0].content)
|
|
227
|
+
|
|
228
|
+
def test_get_memories_by_category(self):
|
|
229
|
+
self.mm.store_memory("custom", "c1")
|
|
230
|
+
self.mm.store_memory("custom", "c2")
|
|
231
|
+
self.mm.store_memory("conversation_facts", "f1")
|
|
232
|
+
customs = self.mm.get_memories_by_category("custom")
|
|
233
|
+
self.assertEqual(len(customs), 2)
|
|
234
|
+
|
|
235
|
+
def test_update_memory(self):
|
|
236
|
+
mem = self.mm.store_memory("custom", "original")
|
|
237
|
+
updated = self.mm.update_memory(mem.id, content="updated", importance=0.9)
|
|
238
|
+
self.assertIsNotNone(updated)
|
|
239
|
+
self.assertEqual(updated.content, "updated")
|
|
240
|
+
self.assertAlmostEqual(updated.importance, 0.9)
|
|
241
|
+
|
|
242
|
+
def test_update_invalid_category_raises(self):
|
|
243
|
+
mem = self.mm.store_memory("custom", "test")
|
|
244
|
+
with self.assertRaises(ValueError):
|
|
245
|
+
self.mm.update_memory(mem.id, category="bogus")
|
|
246
|
+
|
|
247
|
+
def test_deduplicate(self):
|
|
248
|
+
self.mm.store_memory("custom", "the quick brown fox jumps")
|
|
249
|
+
self.mm.store_memory("custom", "the quick brown fox jumps")
|
|
250
|
+
removed = self.mm.deduplicate()
|
|
251
|
+
self.assertEqual(removed, 1)
|
|
252
|
+
remaining = self.mm.get_all_memories()
|
|
253
|
+
self.assertEqual(len(remaining), 1)
|
|
254
|
+
|
|
255
|
+
def test_project_memory_store_and_get(self):
|
|
256
|
+
self.mm.store_project_fact("/test/project", "Uses pytest", category="testing")
|
|
257
|
+
facts = self.mm.get_project_context("/test/project")
|
|
258
|
+
self.assertEqual(len(facts), 1)
|
|
259
|
+
self.assertEqual(facts[0]["fact"], "Uses pytest")
|
|
260
|
+
self.assertEqual(facts[0]["category"], "testing")
|
|
261
|
+
|
|
262
|
+
def test_project_memory_clear(self):
|
|
263
|
+
self.mm.store_project_fact("/test/project", "fact1")
|
|
264
|
+
self.mm.store_project_fact("/test/project", "fact2")
|
|
265
|
+
self.mm.clear_project_memory("/test/project")
|
|
266
|
+
facts = self.mm.get_project_context("/test/project")
|
|
267
|
+
self.assertEqual(len(facts), 0)
|
|
268
|
+
|
|
269
|
+
def test_context_block(self):
|
|
270
|
+
self.mm.add_session_turn("user", "Hello")
|
|
271
|
+
self.mm.store_memory("conversation_facts", "User likes testing")
|
|
272
|
+
block = self.mm.context_block()
|
|
273
|
+
self.assertIn("Recent Session", block)
|
|
274
|
+
self.assertIn("Long-term Memories", block)
|
|
275
|
+
|
|
276
|
+
def test_context_block_with_project(self):
|
|
277
|
+
self.mm.store_project_fact("/proj", "Uses Flask")
|
|
278
|
+
block = self.mm.context_block(project_path="/proj")
|
|
279
|
+
self.assertIn("Project Facts", block)
|
|
280
|
+
|
|
281
|
+
def test_memory_dataclass_to_dict(self):
|
|
282
|
+
mem = Memory(category="custom", content="test data")
|
|
283
|
+
d = mem.to_dict()
|
|
284
|
+
self.assertEqual(d["category"], "custom")
|
|
285
|
+
self.assertEqual(d["content"], "test data")
|
|
286
|
+
|
|
287
|
+
def test_memory_from_dict(self):
|
|
288
|
+
d = {"category": "custom", "content": "from dict", "importance": 0.8}
|
|
289
|
+
mem = Memory.from_dict(d)
|
|
290
|
+
self.assertEqual(mem.content, "from dict")
|
|
291
|
+
self.assertAlmostEqual(mem.importance, 0.8)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TestEventStream(unittest.TestCase):
|
|
295
|
+
|
|
296
|
+
def setUp(self):
|
|
297
|
+
self.es = EventStream()
|
|
298
|
+
|
|
299
|
+
def test_emit_and_get_recent(self):
|
|
300
|
+
self.es.emit("test_event", source="unit", msg="hello")
|
|
301
|
+
events = self.es.get_recent("test_event")
|
|
302
|
+
self.assertEqual(len(events), 1)
|
|
303
|
+
self.assertEqual(events[0].event_type, "test_event")
|
|
304
|
+
self.assertEqual(events[0].data["msg"], "hello")
|
|
305
|
+
self.assertEqual(events[0].source, "unit")
|
|
306
|
+
|
|
307
|
+
def test_emit_returns_event(self):
|
|
308
|
+
ev = self.es.emit("x")
|
|
309
|
+
self.assertIsInstance(ev, Event)
|
|
310
|
+
self.assertGreater(ev.timestamp, 0)
|
|
311
|
+
|
|
312
|
+
def test_subscribe_receives_events(self):
|
|
313
|
+
received = []
|
|
314
|
+
self.es.subscribe("test_event", lambda e: received.append(e))
|
|
315
|
+
self.es.emit("test_event")
|
|
316
|
+
self.es.emit("other_event")
|
|
317
|
+
self.assertEqual(len(received), 1)
|
|
318
|
+
|
|
319
|
+
def test_unsubscribe(self):
|
|
320
|
+
received = []
|
|
321
|
+
cb = lambda e: received.append(e)
|
|
322
|
+
self.es.subscribe("test_event", cb)
|
|
323
|
+
self.es.emit("test_event")
|
|
324
|
+
self.es.unsubscribe("test_event", cb)
|
|
325
|
+
self.es.emit("test_event")
|
|
326
|
+
self.assertEqual(len(received), 1)
|
|
327
|
+
|
|
328
|
+
def test_get_recent_no_filter(self):
|
|
329
|
+
self.es.emit("a")
|
|
330
|
+
self.es.emit("b")
|
|
331
|
+
self.es.emit("c")
|
|
332
|
+
all_ev = self.es.get_recent()
|
|
333
|
+
self.assertEqual(len(all_ev), 3)
|
|
334
|
+
|
|
335
|
+
def test_get_recent_limit(self):
|
|
336
|
+
for i in range(10):
|
|
337
|
+
self.es.emit("e")
|
|
338
|
+
recent = self.es.get_recent(limit=3)
|
|
339
|
+
self.assertEqual(len(recent), 3)
|
|
340
|
+
|
|
341
|
+
def test_get_all_filtered(self):
|
|
342
|
+
self.es.emit("a")
|
|
343
|
+
self.es.emit("b")
|
|
344
|
+
self.es.emit("a")
|
|
345
|
+
a_events = self.es.get_all("a")
|
|
346
|
+
self.assertEqual(len(a_events), 2)
|
|
347
|
+
|
|
348
|
+
def test_clear(self):
|
|
349
|
+
self.es.emit("a")
|
|
350
|
+
self.es.emit("b")
|
|
351
|
+
self.es.clear()
|
|
352
|
+
self.assertEqual(self.es.get_recent(), [])
|
|
353
|
+
|
|
354
|
+
def test_subscriber_count(self):
|
|
355
|
+
self.es.subscribe("a", lambda e: None)
|
|
356
|
+
self.es.subscribe("a", lambda e: None)
|
|
357
|
+
self.es.subscribe("b", lambda e: None)
|
|
358
|
+
self.assertEqual(self.es.subscriber_count("a"), 2)
|
|
359
|
+
self.assertEqual(self.es.subscriber_count(), 3)
|
|
360
|
+
|
|
361
|
+
def test_subscriber_error_does_not_crash(self):
|
|
362
|
+
def bad_callback(e):
|
|
363
|
+
raise RuntimeError("boom")
|
|
364
|
+
self.es.subscribe("test", bad_callback)
|
|
365
|
+
event = self.es.emit("test")
|
|
366
|
+
self.assertIsNotNone(event)
|
|
367
|
+
|
|
368
|
+
def test_multiple_data_kwargs(self):
|
|
369
|
+
self.es.emit("calc", x=1, y=2, z=3)
|
|
370
|
+
ev = self.es.get_recent("calc")[0]
|
|
371
|
+
self.assertEqual(ev.data["x"], 1)
|
|
372
|
+
self.assertEqual(ev.data["z"], 3)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class TestAgentRuntime(unittest.TestCase):
|
|
376
|
+
|
|
377
|
+
def test_init(self):
|
|
378
|
+
tools = {"calc": {"run": lambda a: "42"}}
|
|
379
|
+
ar = AgentRuntime(tools, max_steps=5)
|
|
380
|
+
self.assertEqual(ar.tools, tools)
|
|
381
|
+
self.assertEqual(ar.max_steps, 5)
|
|
382
|
+
|
|
383
|
+
def test_init_defaults(self):
|
|
384
|
+
ar = AgentRuntime({})
|
|
385
|
+
self.assertEqual(ar.max_steps, 18)
|
|
386
|
+
|
|
387
|
+
def test_build_prompt(self):
|
|
388
|
+
ar = AgentRuntime({})
|
|
389
|
+
convo = [
|
|
390
|
+
{"role": "user", "text": "What is 2+2?"},
|
|
391
|
+
{"role": "assistant", "text": "Let me calculate."},
|
|
392
|
+
]
|
|
393
|
+
prompt = ar._build_prompt(convo)
|
|
394
|
+
self.assertIn("USER: What is 2+2?", prompt)
|
|
395
|
+
self.assertIn("ASSISTANT:", prompt)
|
|
396
|
+
|
|
397
|
+
def test_extract_json_simple(self):
|
|
398
|
+
ar = AgentRuntime({})
|
|
399
|
+
result = ar._extract_json('Here is JSON: {"action": "final", "text": "done"}')
|
|
400
|
+
self.assertEqual(result["action"], "final")
|
|
401
|
+
self.assertEqual(result["text"], "done")
|
|
402
|
+
|
|
403
|
+
def test_extract_json_none(self):
|
|
404
|
+
ar = AgentRuntime({})
|
|
405
|
+
self.assertIsNone(ar._extract_json(None))
|
|
406
|
+
self.assertIsNone(ar._extract_json("no json here"))
|
|
407
|
+
|
|
408
|
+
def test_extract_json_with_code_block(self):
|
|
409
|
+
ar = AgentRuntime({})
|
|
410
|
+
text = '```json\n{"action": "final", "text": "42"}\n```'
|
|
411
|
+
result = ar._extract_json(text)
|
|
412
|
+
self.assertEqual(result["action"], "final")
|
|
413
|
+
|
|
414
|
+
def test_normalize_action_tool(self):
|
|
415
|
+
tools = {"calc": {"run": lambda a: "ok"}}
|
|
416
|
+
ar = AgentRuntime(tools)
|
|
417
|
+
action = ar._normalize_action({"action": "tool", "tool": "calc", "args": {"expr": "1"}})
|
|
418
|
+
self.assertEqual(action["action"], "tool")
|
|
419
|
+
self.assertEqual(action["tool"], "calc")
|
|
420
|
+
|
|
421
|
+
def test_normalize_action_final(self):
|
|
422
|
+
ar = AgentRuntime({})
|
|
423
|
+
action = ar._normalize_action({"action": "final", "text": "answer"})
|
|
424
|
+
self.assertEqual(action["action"], "final")
|
|
425
|
+
self.assertEqual(action["text"], "answer")
|
|
426
|
+
|
|
427
|
+
def test_normalize_action_implicit_tool(self):
|
|
428
|
+
tools = {"calc": {"run": lambda a: "ok"}}
|
|
429
|
+
ar = AgentRuntime(tools)
|
|
430
|
+
action = ar._normalize_action({"calc": {"expr": "1"}})
|
|
431
|
+
self.assertIsNotNone(action)
|
|
432
|
+
self.assertEqual(action["tool"], "calc")
|
|
433
|
+
|
|
434
|
+
def test_normalize_action_text_key(self):
|
|
435
|
+
ar = AgentRuntime({})
|
|
436
|
+
action = ar._normalize_action({"text": "the answer"})
|
|
437
|
+
self.assertEqual(action["action"], "final")
|
|
438
|
+
|
|
439
|
+
def test_clean_final_text_empty(self):
|
|
440
|
+
ar = AgentRuntime({})
|
|
441
|
+
self.assertEqual(ar.clean_final_text(""), "")
|
|
442
|
+
self.assertEqual(ar.clean_final_text(None), "")
|
|
443
|
+
|
|
444
|
+
def test_clean_final_text_plain(self):
|
|
445
|
+
ar = AgentRuntime({})
|
|
446
|
+
result = ar.clean_final_text("Just a normal answer")
|
|
447
|
+
self.assertEqual(result, "Just a normal answer")
|
|
448
|
+
|
|
449
|
+
def test_extract_protocol_text(self):
|
|
450
|
+
ar = AgentRuntime({})
|
|
451
|
+
result = ar._extract_protocol_text({"action": "final", "text": "hello"})
|
|
452
|
+
self.assertEqual(result, "hello")
|
|
453
|
+
result2 = ar._extract_protocol_text({"text": "fallback"})
|
|
454
|
+
self.assertEqual(result2, "fallback")
|
|
455
|
+
result3 = ar._extract_protocol_text("not a dict")
|
|
456
|
+
self.assertIsNone(result3)
|
|
457
|
+
|
|
458
|
+
def test_find_balanced_json_method(self):
|
|
459
|
+
ar = AgentRuntime({})
|
|
460
|
+
result = ar._find_balanced_json('{"a": 1}', 0)
|
|
461
|
+
self.assertEqual(result, '{"a": 1}')
|
|
462
|
+
self.assertIsNone(ar._find_balanced_json("x", 0))
|
|
463
|
+
|
|
464
|
+
def test_balanced_json_spans(self):
|
|
465
|
+
ar = AgentRuntime({})
|
|
466
|
+
spans = ar._balanced_json_spans('text {"a":1} more {"b":2} end')
|
|
467
|
+
self.assertEqual(len(spans), 2)
|
|
468
|
+
|
|
469
|
+
def test_is_protocol_blob(self):
|
|
470
|
+
ar = AgentRuntime({})
|
|
471
|
+
self.assertTrue(ar._is_protocol_blob({"action": "tool"}))
|
|
472
|
+
self.assertFalse(ar._is_protocol_blob({"text": "hi"}))
|
|
473
|
+
self.assertFalse(ar._is_protocol_blob("string"))
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class TestCollaboration(unittest.TestCase):
|
|
477
|
+
|
|
478
|
+
def test_file_lock_acquire_release(self):
|
|
479
|
+
flm = FileLockManager()
|
|
480
|
+
self.assertTrue(flm.lock("/test.py", "agent1"))
|
|
481
|
+
self.assertTrue(flm.is_locked("/test.py"))
|
|
482
|
+
self.assertEqual(flm.get_lock_owner("/test.py"), "agent1")
|
|
483
|
+
flm.unlock("/test.py", "agent1")
|
|
484
|
+
self.assertFalse(flm.is_locked("/test.py"))
|
|
485
|
+
|
|
486
|
+
def test_file_lock_contention(self):
|
|
487
|
+
flm = FileLockManager()
|
|
488
|
+
self.assertTrue(flm.lock("/test.py", "agent1"))
|
|
489
|
+
self.assertFalse(flm.lock("/test.py", "agent2"))
|
|
490
|
+
self.assertEqual(flm.get_lock_owner("/test.py"), "agent1")
|
|
491
|
+
|
|
492
|
+
def test_file_lock_wrong_owner_cannot_unlock(self):
|
|
493
|
+
flm = FileLockManager()
|
|
494
|
+
flm.lock("/test.py", "agent1")
|
|
495
|
+
flm.unlock("/test.py", "agent2")
|
|
496
|
+
self.assertTrue(flm.is_locked("/test.py"))
|
|
497
|
+
|
|
498
|
+
def test_file_lock_release_all(self):
|
|
499
|
+
flm = FileLockManager()
|
|
500
|
+
flm.lock("/a.py", "agent1")
|
|
501
|
+
flm.lock("/b.py", "agent1")
|
|
502
|
+
flm.lock("/c.py", "agent2")
|
|
503
|
+
released = flm.release_all("agent1")
|
|
504
|
+
self.assertEqual(len(released), 2)
|
|
505
|
+
self.assertIn("/a.py", released)
|
|
506
|
+
self.assertIn("/b.py", released)
|
|
507
|
+
self.assertFalse(flm.is_locked("/a.py"))
|
|
508
|
+
self.assertTrue(flm.is_locked("/c.py"))
|
|
509
|
+
|
|
510
|
+
def test_file_lock_unlock_unowned(self):
|
|
511
|
+
flm = FileLockManager()
|
|
512
|
+
flm.unlock("/nonexistent.py", "agent1")
|
|
513
|
+
self.assertFalse(flm.is_locked("/nonexistent.py"))
|
|
514
|
+
|
|
515
|
+
def test_collaboration_context_defaults(self):
|
|
516
|
+
ctx = CollaborationContext()
|
|
517
|
+
self.assertEqual(ctx.task, "")
|
|
518
|
+
self.assertEqual(ctx.decisions, [])
|
|
519
|
+
self.assertEqual(ctx.issues, [])
|
|
520
|
+
|
|
521
|
+
def test_collaboration_context_to_prompt_block(self):
|
|
522
|
+
ctx = CollaborationContext(
|
|
523
|
+
task="Plot the curve",
|
|
524
|
+
decisions=["Use matplotlib"],
|
|
525
|
+
issues=["Need data"],
|
|
526
|
+
relevant_files=["plot.py"],
|
|
527
|
+
)
|
|
528
|
+
block = ctx.to_prompt_block()
|
|
529
|
+
self.assertIn("TASK: Plot the curve", block)
|
|
530
|
+
self.assertIn("DECISIONS:", block)
|
|
531
|
+
self.assertIn("Use matplotlib", block)
|
|
532
|
+
self.assertIn("OPEN ISSUES:", block)
|
|
533
|
+
self.assertIn("Need data", block)
|
|
534
|
+
self.assertIn("plot.py", block)
|
|
535
|
+
|
|
536
|
+
def test_collaboration_context_exclude_roles(self):
|
|
537
|
+
ctx = CollaborationContext(
|
|
538
|
+
task="test",
|
|
539
|
+
agent_findings={"planner": "plan here", "coder": "code here"},
|
|
540
|
+
)
|
|
541
|
+
block = ctx.to_prompt_block(exclude_roles=["planner"])
|
|
542
|
+
self.assertNotIn("PLAN HERE", block.upper())
|
|
543
|
+
self.assertIn("CODER FINDING", block)
|
|
544
|
+
self.assertIn("code here", block)
|
|
545
|
+
|
|
546
|
+
def test_collaboration_context_with_findings(self):
|
|
547
|
+
ctx = CollaborationContext(
|
|
548
|
+
task="test",
|
|
549
|
+
agent_findings={"researcher": "found info about X"},
|
|
550
|
+
)
|
|
551
|
+
block = ctx.to_prompt_block()
|
|
552
|
+
self.assertIn("RESEARCHER FINDING:", block)
|
|
553
|
+
self.assertIn("found info about X", block)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
if __name__ == "__main__":
|
|
557
|
+
unittest.main(verbosity=2)
|