tunacode-cli 0.0.15__tar.gz → 0.0.16__tar.gz
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.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/PKG-INFO +1 -1
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/pyproject.toml +1 -1
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/constants.py +1 -1
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/PKG-INFO +1 -1
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/SOURCES.txt +2 -1
- tunacode_cli-0.0.16/tests/test_escape_mechanism.py +184 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/LICENSE +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/README.md +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/setup.cfg +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/setup.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/cli/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/cli/commands.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/cli/main.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/cli/repl.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/cli/textual_app.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/cli/textual_bridge.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/configuration/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/configuration/defaults.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/configuration/models.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/configuration/settings.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/context.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/agents/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/agents/main.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/setup/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/setup/agent_setup.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/setup/base.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/setup/config_setup.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/setup/coordinator.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/setup/environment_setup.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/setup/git_safety_setup.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/state.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/core/tool_handler.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/exceptions.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/prompts/system.txt +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/py.typed +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/services/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/services/mcp.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/setup.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/tools/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/tools/base.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/tools/bash.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/tools/grep.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/tools/read_file.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/tools/run_command.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/tools/update_file.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/tools/write_file.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/types.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/completers.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/console.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/constants.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/decorators.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/input.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/keybindings.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/lexers.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/output.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/panels.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/prompt_manager.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/tool_ui.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/ui/validators.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/utils/__init__.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/utils/bm25.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/utils/diff_utils.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/utils/file_utils.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/utils/ripgrep.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/utils/system.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/utils/text_utils.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode/utils/user_configuration.py +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/dependency_links.txt +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/entry_points.txt +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/requires.txt +0 -0
- {tunacode_cli-0.0.15 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/top_level.txt +0 -0
|
@@ -68,4 +68,5 @@ src/tunacode_cli.egg-info/SOURCES.txt
|
|
|
68
68
|
src/tunacode_cli.egg-info/dependency_links.txt
|
|
69
69
|
src/tunacode_cli.egg-info/entry_points.txt
|
|
70
70
|
src/tunacode_cli.egg-info/requires.txt
|
|
71
|
-
src/tunacode_cli.egg-info/top_level.txt
|
|
71
|
+
src/tunacode_cli.egg-info/top_level.txt
|
|
72
|
+
tests/test_escape_mechanism.py
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from tunacode.types import EscapeState
|
|
7
|
+
from tunacode.exceptions import EscapeInterrupt
|
|
8
|
+
from tunacode.core.state import StateManager
|
|
9
|
+
from tunacode.core.tool_handler import ToolHandler
|
|
10
|
+
from tunacode.tools.write_file import WriteFileTool, write_file
|
|
11
|
+
from tunacode.tools.update_file import UpdateFileTool, update_file
|
|
12
|
+
try:
|
|
13
|
+
from tunacode.ui.keybindings import create_key_bindings
|
|
14
|
+
except ImportError:
|
|
15
|
+
create_key_bindings = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(autouse=True)
|
|
19
|
+
def freeze_time(monkeypatch):
|
|
20
|
+
"""Freeze time.time() to a mutable value for predictable testing."""
|
|
21
|
+
t = [1000.0]
|
|
22
|
+
monkeypatch.setattr(time, 'time', lambda: t[0])
|
|
23
|
+
return t
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_escape_state_initial_window_inactive():
|
|
27
|
+
state = EscapeState()
|
|
28
|
+
assert not state.is_window_active()
|
|
29
|
+
assert state.last_escape_time is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_escape_state_window_active_and_reset(freeze_time):
|
|
33
|
+
state = EscapeState()
|
|
34
|
+
# First press sets last_escape_time
|
|
35
|
+
freeze_time[0] = 1000.0
|
|
36
|
+
state.last_escape_time = freeze_time[0]
|
|
37
|
+
# Within 1 second window it should be active
|
|
38
|
+
freeze_time[0] = 1000.5
|
|
39
|
+
assert state.is_window_active()
|
|
40
|
+
# After window expires it should reset state
|
|
41
|
+
freeze_time[0] = 2000.0
|
|
42
|
+
assert not state.is_window_active()
|
|
43
|
+
assert state.last_escape_time is None
|
|
44
|
+
assert not state.escape_pending
|
|
45
|
+
assert not state.message_shown
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_escape_state_reset():
|
|
49
|
+
state = EscapeState(last_escape_time=1.0, escape_pending=True, message_shown=True)
|
|
50
|
+
state.reset_escape_state()
|
|
51
|
+
assert state.last_escape_time is None
|
|
52
|
+
assert not state.escape_pending
|
|
53
|
+
assert not state.message_shown
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_tool_handler_check_escape_noop():
|
|
57
|
+
manager = StateManager()
|
|
58
|
+
handler = ToolHandler(manager)
|
|
59
|
+
handler.check_escape()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_tool_handler_check_escape_triggers(freeze_time):
|
|
63
|
+
manager = StateManager()
|
|
64
|
+
manager.escape_state.last_escape_time = freeze_time[0]
|
|
65
|
+
manager.escape_state.escape_pending = True
|
|
66
|
+
handler = ToolHandler(manager)
|
|
67
|
+
with pytest.raises(EscapeInterrupt):
|
|
68
|
+
handler.check_escape()
|
|
69
|
+
assert manager.escape_state.last_escape_time is None
|
|
70
|
+
assert not manager.escape_state.escape_pending
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _DummyBuffer:
|
|
74
|
+
def __init__(self):
|
|
75
|
+
self.text_inserted = ''
|
|
76
|
+
|
|
77
|
+
def validate_and_handle(self):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def insert_text(self, text):
|
|
81
|
+
self.text_inserted += text
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _DummyOutput:
|
|
85
|
+
def __init__(self):
|
|
86
|
+
self.messages = []
|
|
87
|
+
def write(self, txt):
|
|
88
|
+
self.messages.append(txt)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _DummyApp:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _make_event(manager):
|
|
96
|
+
app = _DummyApp()
|
|
97
|
+
app.state_manager = manager
|
|
98
|
+
app.output = _DummyOutput()
|
|
99
|
+
return type('Evt', (), {'app': app, 'current_buffer': _DummyBuffer()})()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_double_escape_binding(freeze_time):
|
|
103
|
+
if create_key_bindings is None:
|
|
104
|
+
pytest.skip("prompt_toolkit not installed, skipping keybinding test")
|
|
105
|
+
manager = StateManager()
|
|
106
|
+
# Simulate a running task so ESC ESC hint is enabled
|
|
107
|
+
manager.session.current_task = type('DummyTask', (), {'done': lambda self: False})()
|
|
108
|
+
kb = create_key_bindings()
|
|
109
|
+
esc_binding = next(b for b in kb.bindings if b.keys == ('escape',))
|
|
110
|
+
event = _make_event(manager)
|
|
111
|
+
# first ESC press
|
|
112
|
+
freeze_time[0] = 1000.0
|
|
113
|
+
esc_binding.handler(event)
|
|
114
|
+
assert manager.escape_state.escape_pending
|
|
115
|
+
assert event.app.output.messages == ['Press ESC again to stop\n']
|
|
116
|
+
# second ESC press within window
|
|
117
|
+
freeze_time[0] = 1000.5
|
|
118
|
+
with pytest.raises(EscapeInterrupt):
|
|
119
|
+
esc_binding.handler(event)
|
|
120
|
+
|
|
121
|
+
def test_escape_enter_binding(freeze_time):
|
|
122
|
+
if create_key_bindings is None:
|
|
123
|
+
pytest.skip("prompt_toolkit not installed, skipping keybinding test")
|
|
124
|
+
manager = StateManager()
|
|
125
|
+
kb = create_key_bindings()
|
|
126
|
+
# Keys.ControlM is used for Enter key
|
|
127
|
+
from prompt_toolkit.keys import Keys
|
|
128
|
+
ee_binding = next(b for b in kb.bindings if b.keys == (Keys.Escape, Keys.ControlM))
|
|
129
|
+
event = _make_event(manager)
|
|
130
|
+
ee_binding.handler(event)
|
|
131
|
+
buf = event.current_buffer
|
|
132
|
+
assert buf.text_inserted == "\n"
|
|
133
|
+
assert not manager.escape_state.escape_pending
|
|
134
|
+
|
|
135
|
+
def test_ctrl_c_not_overridden():
|
|
136
|
+
if create_key_bindings is None:
|
|
137
|
+
pytest.skip("prompt_toolkit not installed, skipping Ctrl+C compatibility test")
|
|
138
|
+
kb = create_key_bindings()
|
|
139
|
+
# Ensure no custom binding overrides Ctrl+C
|
|
140
|
+
assert not any(binding.keys == ('c-c',) for binding in kb.bindings)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.asyncio
|
|
144
|
+
async def test_write_file_tool_atomic(tmp_path):
|
|
145
|
+
filepath = tmp_path / 'out.txt'
|
|
146
|
+
content = 'Hello, Tuna!'
|
|
147
|
+
tool = WriteFileTool(None)
|
|
148
|
+
result = await tool._execute(str(filepath), content)
|
|
149
|
+
assert filepath.read_text(encoding='utf-8') == content
|
|
150
|
+
assert 'Successfully wrote to new file' in result
|
|
151
|
+
assert not os.path.exists(str(filepath) + '.tuna_tmp')
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@pytest.mark.asyncio
|
|
155
|
+
async def test_write_file_convenience(tmp_path):
|
|
156
|
+
filepath = tmp_path / 'out2.txt'
|
|
157
|
+
content = 'TunaCode!'
|
|
158
|
+
result = await write_file(str(filepath), content)
|
|
159
|
+
assert filepath.read_text(encoding='utf-8') == content
|
|
160
|
+
assert 'Successfully wrote to new file' in result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.asyncio
|
|
164
|
+
async def test_update_file_tool_atomic(tmp_path):
|
|
165
|
+
filepath = tmp_path / 'in.txt'
|
|
166
|
+
original = 'foo\nbar\nbaz\n'
|
|
167
|
+
filepath.write_text(original, encoding='utf-8')
|
|
168
|
+
tool = UpdateFileTool(None)
|
|
169
|
+
result = await tool._execute(str(filepath), 'bar', 'qux')
|
|
170
|
+
updated = filepath.read_text(encoding='utf-8')
|
|
171
|
+
assert 'bar' not in updated and 'qux' in updated
|
|
172
|
+
assert 'updated successfully' in result
|
|
173
|
+
assert not os.path.exists(str(filepath) + '.tuna_tmp')
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.mark.asyncio
|
|
177
|
+
async def test_update_file_convenience(tmp_path):
|
|
178
|
+
filepath = tmp_path / 'in2.txt'
|
|
179
|
+
original = 'alpha\nbeta\n'
|
|
180
|
+
filepath.write_text(original, encoding='utf-8')
|
|
181
|
+
result = await update_file(str(filepath), 'alpha', 'gamma')
|
|
182
|
+
updated = filepath.read_text(encoding='utf-8')
|
|
183
|
+
assert 'alpha' not in updated and 'gamma' in updated
|
|
184
|
+
assert 'updated successfully' in result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|