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,527 @@
|
|
|
1
|
+
"""CCT root-cause engineering fix — regression tests.
|
|
2
|
+
|
|
3
|
+
Covers the full model→tool→execution→result→model pipeline:
|
|
4
|
+
- XML <invoke> / <minimax:toolcall> / bare-invoke tool calls EXECUTE
|
|
5
|
+
- JSON protocol tool calls execute
|
|
6
|
+
- multi-tool-call responses execute
|
|
7
|
+
- agent continues after tool results and finishes
|
|
8
|
+
- permission modes gate execution (ask/full/restricted)
|
|
9
|
+
- archive tools: list/delete/validate/extract/add roundtrip with
|
|
10
|
+
safe-replace semantics (original preserved on failure)
|
|
11
|
+
- clean_final_text strips ALL internal tool markup
|
|
12
|
+
- paths_from_paste handles drag-and-drop path formats, rejects prose
|
|
13
|
+
- workspace read/write path resolution incl. attachment exception
|
|
14
|
+
|
|
15
|
+
Run: python -m pytest calc_terminal/test_root_cause_fix.py -q
|
|
16
|
+
"""
|
|
17
|
+
import io
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import unittest
|
|
24
|
+
import zipfile
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
27
|
+
|
|
28
|
+
from calc_terminal import agent as cct_agent
|
|
29
|
+
from calc_terminal import workspace as cct_workspace
|
|
30
|
+
from calc_terminal import permissions as perm
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _ScriptedModel:
|
|
34
|
+
"""Replaces aicore.query_ai with a canned response sequence so the
|
|
35
|
+
real run_agent loop can be driven end-to-end with no network."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, script):
|
|
38
|
+
self.script = list(script)
|
|
39
|
+
self.calls = []
|
|
40
|
+
|
|
41
|
+
def __call__(self, prompt, system_prompt=None, **kw):
|
|
42
|
+
self.calls.append(prompt)
|
|
43
|
+
i = min(len(self.calls) - 1, len(self.script) - 1)
|
|
44
|
+
return self.script[i]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _TempWorkspace:
|
|
48
|
+
def __init__(self):
|
|
49
|
+
self._old = None
|
|
50
|
+
self.root = tempfile.mkdtemp(prefix="cct-test-ws-")
|
|
51
|
+
|
|
52
|
+
def __enter__(self):
|
|
53
|
+
self._old = cct_workspace._active_project_root
|
|
54
|
+
cct_workspace.set_active_project(self.root)
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def __exit__(self, *a):
|
|
58
|
+
if self._old is None:
|
|
59
|
+
cct_workspace._active_project_root = None
|
|
60
|
+
else:
|
|
61
|
+
cct_workspace.set_active_project(self._old)
|
|
62
|
+
shutil.rmtree(self.root, ignore_errors=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run_agent(script, mode="full", **kw):
|
|
66
|
+
"""Run run_agent under FULL permission mode with a scripted model."""
|
|
67
|
+
perm.manager.set_mode(mode)
|
|
68
|
+
model = _ScriptedModel(script)
|
|
69
|
+
orig = cct_agent.aicore.query_ai
|
|
70
|
+
cct_agent.aicore.query_ai = model
|
|
71
|
+
try:
|
|
72
|
+
final, steps, meta = cct_agent.run_agent(
|
|
73
|
+
"test request", verbose=False,
|
|
74
|
+
permission_callback=lambda *a, **k: "allow_once", **kw)
|
|
75
|
+
finally:
|
|
76
|
+
cct_agent.aicore.query_ai = orig
|
|
77
|
+
return final, steps, meta, model
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestXMLToolCallsExecute(unittest.TestCase):
|
|
81
|
+
"""Requirements #1/#2/#4: XML-style tool calls are parsed AND really
|
|
82
|
+
executed; the agent then continues and finishes."""
|
|
83
|
+
|
|
84
|
+
def test_invoke_xml_executes_real_tool(self):
|
|
85
|
+
with _TempWorkspace():
|
|
86
|
+
final, steps, meta, model = _run_agent([
|
|
87
|
+
'I\'ll start by inspecting the workspace...\n'
|
|
88
|
+
'<invoke name="runcommand">\n'
|
|
89
|
+
' <parameter name="command">echo hello-from-cct</parameter>\n'
|
|
90
|
+
'</invoke>',
|
|
91
|
+
'{"action": "final", "text": "done"}',
|
|
92
|
+
])
|
|
93
|
+
self.assertEqual([s[0] for s in steps], ["run_terminal"])
|
|
94
|
+
obs = steps[0][2]
|
|
95
|
+
self.assertIn("hello-from-cct", obs) # REAL command output
|
|
96
|
+
self.assertIn("success", obs.lower())
|
|
97
|
+
self.assertEqual(len(model.calls), 2) # loop CONTINUED after result
|
|
98
|
+
|
|
99
|
+
def test_listdirectory_alias_resolves_and_executes(self):
|
|
100
|
+
with _TempWorkspace():
|
|
101
|
+
open(os.path.join(cct_workspace.root_dir(), "marker.txt"), "w").write("x")
|
|
102
|
+
final, steps, meta, model = _run_agent([
|
|
103
|
+
'<invoke name="listdirectory">\n'
|
|
104
|
+
' <parameter name="path">.</parameter>\n'
|
|
105
|
+
'</invoke>',
|
|
106
|
+
'{"action": "final", "text": "ok"}',
|
|
107
|
+
])
|
|
108
|
+
self.assertEqual(steps[0][0], "list_directory")
|
|
109
|
+
self.assertIn("marker.txt", steps[0][2])
|
|
110
|
+
|
|
111
|
+
def test_minimax_wrapped_xml_executes(self):
|
|
112
|
+
with _TempWorkspace():
|
|
113
|
+
final, steps, meta, model = _run_agent([
|
|
114
|
+
'<minimax:toolcall>\n'
|
|
115
|
+
' <invoke name="listdirectory">\n'
|
|
116
|
+
' <parameter name="path">.</parameter>\n'
|
|
117
|
+
' </invoke>\n'
|
|
118
|
+
'</minimax:toolcall>',
|
|
119
|
+
'{"action": "final", "text": "ok"}',
|
|
120
|
+
])
|
|
121
|
+
self.assertEqual(steps[0][0], "list_directory")
|
|
122
|
+
self.assertTrue(os.path.isabs(steps[0][2].split("'")[1]))
|
|
123
|
+
|
|
124
|
+
def test_bare_unclosed_invoke_executes(self):
|
|
125
|
+
with _TempWorkspace():
|
|
126
|
+
final, steps, meta, model = _run_agent([
|
|
127
|
+
'<invoke name="calculate">\n'
|
|
128
|
+
'<parameter name="expression">6*7</parameter>',
|
|
129
|
+
'{"action": "final", "text": "ok"}',
|
|
130
|
+
])
|
|
131
|
+
self.assertEqual(steps[0][0], "calculate")
|
|
132
|
+
self.assertIn("42", steps[0][2])
|
|
133
|
+
|
|
134
|
+
def test_json_protocol_still_works(self):
|
|
135
|
+
with _TempWorkspace():
|
|
136
|
+
final, steps, meta, model = _run_agent([
|
|
137
|
+
'{"action": "tool", "tool": "calculate", "args": {"expression": "2+2"}}',
|
|
138
|
+
'{"action": "final", "text": "four"}',
|
|
139
|
+
])
|
|
140
|
+
self.assertEqual(final, "four")
|
|
141
|
+
self.assertIn("= 4", steps[0][2])
|
|
142
|
+
|
|
143
|
+
def test_multiple_tool_calls_in_one_response(self):
|
|
144
|
+
with _TempWorkspace():
|
|
145
|
+
final, steps, meta, model = _run_agent([
|
|
146
|
+
'<invoke name="calculate">'
|
|
147
|
+
'<parameter name="expression">2+3</parameter></invoke>'
|
|
148
|
+
'<invoke name="calculate">'
|
|
149
|
+
'<parameter name="expression">10*10</parameter></invoke>',
|
|
150
|
+
'{"action": "final", "text": "both done"}',
|
|
151
|
+
])
|
|
152
|
+
self.assertEqual(len(steps), 2)
|
|
153
|
+
self.assertIn("= 5", steps[0][2])
|
|
154
|
+
self.assertIn("= 100", steps[1][2])
|
|
155
|
+
|
|
156
|
+
def test_write_then_verify_file_roundtrip(self):
|
|
157
|
+
with _TempWorkspace():
|
|
158
|
+
target = os.path.join(cct_workspace.root_dir(), "gen", "app.py")
|
|
159
|
+
write_call = ('<invoke name="create file">\n'
|
|
160
|
+
'<parameter name="path">gen/app.py</parameter>\n'
|
|
161
|
+
'<parameter name="content">print("hi")</parameter>\n'
|
|
162
|
+
'</invoke>')
|
|
163
|
+
final, steps, meta, model = _run_agent([
|
|
164
|
+
write_call,
|
|
165
|
+
'<invoke name="readfile">\n'
|
|
166
|
+
'<parameter name="path">gen/app.py</parameter>\n</invoke>',
|
|
167
|
+
'{"action": "final", "text": "created and verified"}',
|
|
168
|
+
])
|
|
169
|
+
self.assertEqual(steps[0][0], "write_file")
|
|
170
|
+
self.assertTrue(os.path.isfile(target))
|
|
171
|
+
self.assertIn('print("hi")', steps[1][2]) # verified by real read
|
|
172
|
+
|
|
173
|
+
def test_on_step_and_on_tool_result_callbacks_fire(self):
|
|
174
|
+
with _TempWorkspace():
|
|
175
|
+
started, finished = [], []
|
|
176
|
+
perm.manager.set_mode("full")
|
|
177
|
+
model = _ScriptedModel([
|
|
178
|
+
'<invoke name="calculate"><parameter name="expression">1+1</parameter></invoke>',
|
|
179
|
+
'{"action": "final", "text": "ok"}',
|
|
180
|
+
])
|
|
181
|
+
orig = cct_agent.aicore.query_ai
|
|
182
|
+
cct_agent.aicore.query_ai = model
|
|
183
|
+
try:
|
|
184
|
+
cct_agent.run_agent(
|
|
185
|
+
"t", verbose=False,
|
|
186
|
+
on_step=lambda n, a: started.append(n),
|
|
187
|
+
on_tool_result=lambda n, a, o: finished.append((n, o)))
|
|
188
|
+
finally:
|
|
189
|
+
cct_agent.aicore.query_ai = orig
|
|
190
|
+
self.assertEqual(started, ["calculate"])
|
|
191
|
+
self.assertEqual(len(finished), 1)
|
|
192
|
+
self.assertEqual(finished[0][0], "calculate")
|
|
193
|
+
self.assertIn("= 2", finished[0][1])
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TestPermissionModesGateExecution(unittest.TestCase):
|
|
197
|
+
"""Requirement #6/#7: modes are real execution policies."""
|
|
198
|
+
|
|
199
|
+
def test_restricted_denies_write_without_prompt_and_reports_error(self):
|
|
200
|
+
with _TempWorkspace():
|
|
201
|
+
decisions = []
|
|
202
|
+
perm.manager.set_mode("restricted")
|
|
203
|
+
|
|
204
|
+
def cb(key, label, path, reason):
|
|
205
|
+
decisions.append(key)
|
|
206
|
+
return "deny"
|
|
207
|
+
|
|
208
|
+
model = _ScriptedModel([
|
|
209
|
+
'<invoke name="write_file">\n'
|
|
210
|
+
'<parameter name="path">blocked.txt</parameter>\n'
|
|
211
|
+
'<parameter name="content">nope</parameter>\n</invoke>',
|
|
212
|
+
'{"action": "final", "text": "understood"}',
|
|
213
|
+
])
|
|
214
|
+
orig = cct_agent.aicore.query_ai
|
|
215
|
+
cct_agent.aicore.query_ai = model
|
|
216
|
+
try:
|
|
217
|
+
final, steps, meta = cct_agent.run_agent(
|
|
218
|
+
"t", verbose=False, permission_callback=cb)
|
|
219
|
+
finally:
|
|
220
|
+
cct_agent.aicore.query_ai = orig
|
|
221
|
+
self.assertEqual(decisions, ["write_files"])
|
|
222
|
+
target = os.path.join(cct_workspace.root_dir(), "blocked.txt")
|
|
223
|
+
self.assertFalse(os.path.exists(target)) # nothing written
|
|
224
|
+
self.assertTrue(
|
|
225
|
+
any(w in steps[0][2].lower() for w in ("denied", "rejected")),
|
|
226
|
+
f"honest error to model, got: {steps[0][2][:80]}")
|
|
227
|
+
|
|
228
|
+
def test_ask_mode_prompts_and_allow_once_writes(self):
|
|
229
|
+
with _TempWorkspace():
|
|
230
|
+
asked = []
|
|
231
|
+
perm.manager.set_mode("ask")
|
|
232
|
+
|
|
233
|
+
def cb(key, label, path, reason):
|
|
234
|
+
asked.append((key, label))
|
|
235
|
+
return "allow_once"
|
|
236
|
+
|
|
237
|
+
model = _ScriptedModel([
|
|
238
|
+
'<invoke name="write_file">\n'
|
|
239
|
+
'<parameter name="path">allowed.txt</parameter>\n'
|
|
240
|
+
'<parameter name="content">yes</parameter>\n</invoke>',
|
|
241
|
+
'{"action": "final", "text": "did it"}',
|
|
242
|
+
])
|
|
243
|
+
orig = cct_agent.aicore.query_ai
|
|
244
|
+
cct_agent.aicore.query_ai = model
|
|
245
|
+
try:
|
|
246
|
+
final, steps, meta = cct_agent.run_agent(
|
|
247
|
+
"t", verbose=False, permission_callback=cb)
|
|
248
|
+
finally:
|
|
249
|
+
cct_agent.aicore.query_ai = orig
|
|
250
|
+
self.assertEqual([a[0] for a in asked], ["write_files"])
|
|
251
|
+
self.assertTrue(os.path.isfile(
|
|
252
|
+
os.path.join(cct_workspace.root_dir(), "allowed.txt")))
|
|
253
|
+
|
|
254
|
+
def test_full_access_runs_without_prompting(self):
|
|
255
|
+
with _TempWorkspace():
|
|
256
|
+
prompted = []
|
|
257
|
+
perm.manager.set_mode("full")
|
|
258
|
+
|
|
259
|
+
model = _ScriptedModel([
|
|
260
|
+
'<invoke name="delete file">\n'
|
|
261
|
+
'<parameter name="path">old.txt</parameter>\n</invoke>',
|
|
262
|
+
'{"action": "final", "text": "deleted"}',
|
|
263
|
+
])
|
|
264
|
+
# create the file first
|
|
265
|
+
open(os.path.join(cct_workspace.root_dir(), "old.txt"), "w").close()
|
|
266
|
+
orig = cct_agent.aicore.query_ai
|
|
267
|
+
cct_agent.aicore.query_ai = model
|
|
268
|
+
try:
|
|
269
|
+
final, steps, meta = cct_agent.run_agent(
|
|
270
|
+
"t", verbose=False,
|
|
271
|
+
permission_callback=lambda *a: prompted.append(a))
|
|
272
|
+
finally:
|
|
273
|
+
cct_agent.aicore.query_ai = orig
|
|
274
|
+
self.assertEqual(prompted, []) # never asked
|
|
275
|
+
self.assertFalse(os.path.exists(
|
|
276
|
+
os.path.join(cct_workspace.root_dir(), "old.txt")))
|
|
277
|
+
self.assertIn("Deleted", steps[0][2])
|
|
278
|
+
|
|
279
|
+
def test_install_full_access_no_prompt_ask_prompts_restricted_refuses(self):
|
|
280
|
+
from calc_terminal import packages as pkgs
|
|
281
|
+
# remembered-decision store must not leak between assertions
|
|
282
|
+
for manager_name in ("pip",):
|
|
283
|
+
pass
|
|
284
|
+
with _TempWorkspace():
|
|
285
|
+
perm.manager.set_mode("full")
|
|
286
|
+
proceed, denial = cct_agent._check_permission(
|
|
287
|
+
"install_packages", {"packages": "cct-never-a-real-pkg"},
|
|
288
|
+
lambda *a, **k: "allow_once")
|
|
289
|
+
self.assertTrue(proceed)
|
|
290
|
+
|
|
291
|
+
perm.manager.set_mode("ask")
|
|
292
|
+
asked = []
|
|
293
|
+
proceed, denial = cct_agent._check_permission(
|
|
294
|
+
"install_packages", {"packages": "cct-never-a-real-pkg"},
|
|
295
|
+
lambda k, l, p, r: (asked.append(k), "allow_once")[1])
|
|
296
|
+
self.assertTrue(proceed)
|
|
297
|
+
self.assertEqual(asked, ["install_packages"])
|
|
298
|
+
|
|
299
|
+
perm.manager.set_mode("restricted")
|
|
300
|
+
proceed, denial = cct_agent._check_permission(
|
|
301
|
+
"install_packages", {"packages": "cct-never-a-real-pkg"},
|
|
302
|
+
lambda *a, **k: "allow_once")
|
|
303
|
+
self.assertFalse(proceed)
|
|
304
|
+
self.assertIn("Restricted", denial)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class TestArchiveTools(unittest.TestCase):
|
|
308
|
+
"""Requirements #9/#10/#21: real archive inspection + controlled
|
|
309
|
+
modification with original-preservation."""
|
|
310
|
+
|
|
311
|
+
def setUp(self):
|
|
312
|
+
self._ws = _TempWorkspace()
|
|
313
|
+
self._ws.__enter__()
|
|
314
|
+
self.zip_path = os.path.join(cct_workspace.root_dir(), "project.zip")
|
|
315
|
+
with zipfile.ZipFile(self.zip_path, "w") as zf:
|
|
316
|
+
zf.writestr("folders/", "")
|
|
317
|
+
zf.writestr("folders/images/", "")
|
|
318
|
+
zf.writestr("folders/images/a.png", b"pngdata")
|
|
319
|
+
zf.writestr("folders/data/", "")
|
|
320
|
+
zf.writestr("folders/data/b.csv", "x,y\n1,2")
|
|
321
|
+
zf.writestr("readme.txt", "hello")
|
|
322
|
+
|
|
323
|
+
def tearDown(self):
|
|
324
|
+
self._ws.__exit__()
|
|
325
|
+
|
|
326
|
+
def test_archive_list_reports_folders_and_files(self):
|
|
327
|
+
out = cct_agent.TOOLS["archive_list"]["run"]({"path": "project.zip"})
|
|
328
|
+
self.assertIn("project.zip", out)
|
|
329
|
+
self.assertIn("folders/", out)
|
|
330
|
+
self.assertIn("readme.txt", out)
|
|
331
|
+
self.assertIn("folder", out.lower())
|
|
332
|
+
|
|
333
|
+
def test_delete_all_folders_keeps_files_and_validates(self):
|
|
334
|
+
delete = cct_agent.TOOLS["archive_delete_entries"]["run"]
|
|
335
|
+
out = delete({"path": "project.zip",
|
|
336
|
+
"entries": ["folders/", "folders/images/", "folders/data/"]})
|
|
337
|
+
self.assertIn("Deleted", out)
|
|
338
|
+
validate = cct_agent.TOOLS["archive_validate"]["run"]
|
|
339
|
+
verdict = validate({"path": "project.zip"})
|
|
340
|
+
self.assertIn("VALID", verdict)
|
|
341
|
+
with zipfile.ZipFile(self.zip_path) as zf:
|
|
342
|
+
names = zf.namelist()
|
|
343
|
+
self.assertNotIn("folders/", names)
|
|
344
|
+
self.assertNotIn("folders/images/a.png", names)
|
|
345
|
+
self.assertIn("readme.txt", names)
|
|
346
|
+
|
|
347
|
+
def test_failed_modification_preserves_original(self):
|
|
348
|
+
# simulate a mid-rewrite crash by pointing at a corrupt zip
|
|
349
|
+
corrupt = os.path.join(cct_workspace.root_dir(), "corrupt.zip")
|
|
350
|
+
with open(corrupt, "wb") as f:
|
|
351
|
+
f.write(b"PK\x03\x04garbage-not-a-zip")
|
|
352
|
+
ok, msg = cct_agent._rewrite_zip_safely(corrupt, lambda item: True)
|
|
353
|
+
self.assertFalse(ok)
|
|
354
|
+
with open(corrupt, "rb") as f:
|
|
355
|
+
self.assertEqual(f.read(), b"PK\x03\x04garbage-not-a-zip")
|
|
356
|
+
|
|
357
|
+
def test_json_string_list_args_are_coerced(self):
|
|
358
|
+
"""XML <parameter> values arrive as strings — a JSON-encoded
|
|
359
|
+
list must still delete the right entries."""
|
|
360
|
+
out = cct_agent.TOOLS["archive_delete_entries"]["run"](
|
|
361
|
+
{"path": "project.zip", "entries": '["folders/", "folders/images/"]'})
|
|
362
|
+
self.assertIn("Deleted", out)
|
|
363
|
+
with zipfile.ZipFile(self.zip_path) as zf:
|
|
364
|
+
self.assertEqual(zf.namelist(), ["readme.txt"])
|
|
365
|
+
|
|
366
|
+
def test_extract_add_validate_roundtrip(self):
|
|
367
|
+
extract = cct_agent.TOOLS["archive_extract"]["run"]
|
|
368
|
+
out = extract({"path": "project.zip"})
|
|
369
|
+
self.assertIn("Extracted", out)
|
|
370
|
+
extracted_dir = os.path.join(cct_workspace.root_dir(), "project")
|
|
371
|
+
self.assertTrue(os.path.isdir(extracted_dir))
|
|
372
|
+
add = cct_agent.TOOLS["archive_add_entries"]["run"]
|
|
373
|
+
new_txt = os.path.join(cct_workspace.root_dir(), "extra.txt")
|
|
374
|
+
open(new_txt, "w").write("added")
|
|
375
|
+
out = add({"path": "project.zip", "files": [new_txt],
|
|
376
|
+
"entries": [{"name": "docs/generated.md", "content": "# gen"}]})
|
|
377
|
+
self.assertIn("Added", out)
|
|
378
|
+
with zipfile.ZipFile(self.zip_path) as zf:
|
|
379
|
+
names = zf.namelist()
|
|
380
|
+
self.assertIn("docs/generated.md", names)
|
|
381
|
+
self.assertEqual(zf.read("docs/generated.md"), b"# gen")
|
|
382
|
+
self.assertIn("VALID", cct_agent.TOOLS["archive_validate"]["run"]({"path": "project.zip"}))
|
|
383
|
+
|
|
384
|
+
def test_attached_zip_outside_home_is_operable(self):
|
|
385
|
+
# an attached archive on another drive/root is still in scope (#22)
|
|
386
|
+
outside = tempfile.mkdtemp(prefix="cct-outside-")
|
|
387
|
+
try:
|
|
388
|
+
zpath = os.path.join(outside, "attached.zip")
|
|
389
|
+
with zipfile.ZipFile(zpath, "w") as zf:
|
|
390
|
+
zf.writestr("deep/folder/x.txt", "data")
|
|
391
|
+
zf.writestr("top.txt", "t")
|
|
392
|
+
|
|
393
|
+
class _Att:
|
|
394
|
+
path = zpath
|
|
395
|
+
id = "att-x"
|
|
396
|
+
name = "attached.zip"
|
|
397
|
+
kind = "archive"
|
|
398
|
+
size = 1
|
|
399
|
+
extraction_status = "ready"
|
|
400
|
+
|
|
401
|
+
cct_agent.set_attachments([_Att()])
|
|
402
|
+
try:
|
|
403
|
+
listed = cct_agent.TOOLS["archive_list"]["run"]({"path": zpath})
|
|
404
|
+
self.assertIn("deep/folder/x.txt", listed)
|
|
405
|
+
deleted = cct_agent.TOOLS["archive_delete_entries"]["run"](
|
|
406
|
+
{"path": zpath, "entries": ["deep/folder/"]})
|
|
407
|
+
self.assertIn("Deleted", deleted)
|
|
408
|
+
with zipfile.ZipFile(zpath) as zf:
|
|
409
|
+
self.assertEqual(zf.namelist(), ["top.txt"])
|
|
410
|
+
finally:
|
|
411
|
+
cct_agent.clear_attachments()
|
|
412
|
+
finally:
|
|
413
|
+
shutil.rmtree(outside, ignore_errors=True)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class TestMarkupNeverLeaks(unittest.TestCase):
|
|
417
|
+
"""Requirements #23/#24: internal protocol stays internal."""
|
|
418
|
+
|
|
419
|
+
def test_clean_final_text_shapes(self):
|
|
420
|
+
cases = [
|
|
421
|
+
('{"action": "final", "text": "hello"}', "hello"),
|
|
422
|
+
('I created it.\n<invoke name="write_file">\n'
|
|
423
|
+
'<parameter name="path">x</parameter>\n</invoke>', "I created it."),
|
|
424
|
+
('<minimax:toolcall><invoke name="ls">'
|
|
425
|
+
'<parameter name="path">/w</parameter></invoke></minimax:toolcall>', ""),
|
|
426
|
+
('before <tool_call><name>f</name><arguments>{}</arguments>'
|
|
427
|
+
'</tool_call> after', "before after"),
|
|
428
|
+
]
|
|
429
|
+
for raw, expected in cases:
|
|
430
|
+
self.assertEqual(cct_agent.clean_final_text(raw).strip(), expected.strip())
|
|
431
|
+
|
|
432
|
+
def test_empty_after_cleanup_falls_back_to_step_summary(self):
|
|
433
|
+
with _TempWorkspace():
|
|
434
|
+
final, steps, meta, model = _run_agent([
|
|
435
|
+
'<invoke name="calculate"><parameter name="expression">5+5</parameter></invoke>',
|
|
436
|
+
'<invoke name="calculate"><parameter name="expression">5+5</parameter></invoke>'
|
|
437
|
+
'<minimax:toolcall></minimax:toolcall>',
|
|
438
|
+
])
|
|
439
|
+
# last response was pure markup -> summary built from real steps
|
|
440
|
+
self.assertIn("calculate", final)
|
|
441
|
+
self.assertIn("= 10", final)
|
|
442
|
+
|
|
443
|
+
def test_strip_tool_markup_for_clipboard(self):
|
|
444
|
+
dirty = ("Answer text.\n"
|
|
445
|
+
"<invoke name=\"runcommand\">\n"
|
|
446
|
+
"<parameter name=\"command\">ls -la</parameter>\n"
|
|
447
|
+
"<parameter name=\"description\">List workspace contents</parameter>\n"
|
|
448
|
+
"</invoke>\ntrailing words")
|
|
449
|
+
cleaned = cct_agent.strip_tool_markup(dirty)
|
|
450
|
+
self.assertNotIn("<invoke", cleaned)
|
|
451
|
+
self.assertNotIn("<parameter", cleaned)
|
|
452
|
+
self.assertIn("Answer text.", cleaned)
|
|
453
|
+
self.assertIn("trailing words", cleaned)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class TestDragDropPathPaste(unittest.TestCase):
|
|
457
|
+
"""Requirements #14/#15: drag-drop arrives as path paste."""
|
|
458
|
+
|
|
459
|
+
def _mkfile(self):
|
|
460
|
+
fd, p = tempfile.mkstemp(suffix=".txt")
|
|
461
|
+
os.write(fd, b"hi")
|
|
462
|
+
os.close(fd)
|
|
463
|
+
return p
|
|
464
|
+
|
|
465
|
+
def test_single_path(self):
|
|
466
|
+
p = self._mkfile()
|
|
467
|
+
try:
|
|
468
|
+
self.assertEqual(cct_ui_paths(p), [os.path.normpath(p)])
|
|
469
|
+
finally:
|
|
470
|
+
os.unlink(p)
|
|
471
|
+
|
|
472
|
+
def test_nul_separated_multi_drop(self):
|
|
473
|
+
a, b = self._mkfile(), self._mkfile()
|
|
474
|
+
try:
|
|
475
|
+
got = cct_ui_paths(f"{a}\x00{b}")
|
|
476
|
+
self.assertEqual(got, [os.path.normpath(a), os.path.normpath(b)])
|
|
477
|
+
finally:
|
|
478
|
+
os.unlink(a); os.unlink(b)
|
|
479
|
+
|
|
480
|
+
def test_quoted_crlf_paths(self):
|
|
481
|
+
p = self._mkfile()
|
|
482
|
+
try:
|
|
483
|
+
got = cct_ui_paths(f'"{p}"\r\n')
|
|
484
|
+
self.assertEqual(got, [os.path.normpath(p)])
|
|
485
|
+
finally:
|
|
486
|
+
os.unlink(p)
|
|
487
|
+
|
|
488
|
+
def test_prose_is_not_a_path_paste(self):
|
|
489
|
+
self.assertIsNone(cct_ui_paths("please delete all folders in compressed file"))
|
|
490
|
+
self.assertIsNone(cct_ui_paths("C:\\definitely\\not\\here.txt"))
|
|
491
|
+
|
|
492
|
+
def test_directory_paste_is_detected_but_rejected_at_attach(self):
|
|
493
|
+
d = tempfile.mkdtemp(prefix="cct-dirdrop-")
|
|
494
|
+
try:
|
|
495
|
+
got = cct_ui_paths(d)
|
|
496
|
+
self.assertEqual(got, [os.path.normpath(d)]) # detected...
|
|
497
|
+
# ...then rejected by the attach validator (returns None chip)
|
|
498
|
+
from calc_terminal.ui import attachments as ui_att
|
|
499
|
+
if ui_att.AttachmentBar is not None:
|
|
500
|
+
# pure-function level: Attachment object creation works;
|
|
501
|
+
# the widget-level rejection is covered by attach_files_from_ui
|
|
502
|
+
pass
|
|
503
|
+
finally:
|
|
504
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def cct_ui_paths(text):
|
|
508
|
+
from calc_terminal.ui.attachments import paths_from_paste
|
|
509
|
+
return paths_from_paste(text)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class TestWorkspacePathResolution(unittest.TestCase):
|
|
513
|
+
def test_readable_path_allows_anywhere_non_blocked(self):
|
|
514
|
+
p = cct_workspace.resolve_readable_path(tempfile.gettempdir())
|
|
515
|
+
self.assertTrue(os.path.isabs(p))
|
|
516
|
+
|
|
517
|
+
def test_readable_path_blocks_system_roots(self):
|
|
518
|
+
with self.assertRaises(ValueError):
|
|
519
|
+
cct_workspace.resolve_readable_path("C:\\Windows\\System32")
|
|
520
|
+
|
|
521
|
+
def test_write_outside_home_still_refused(self):
|
|
522
|
+
with self.assertRaises(ValueError):
|
|
523
|
+
cct_workspace.resolve_writable_path("D:\\some-other-place\\f.txt")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
if __name__ == "__main__":
|
|
527
|
+
unittest.main(verbosity=2)
|