tunacode-cli 0.0.14__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.14/src/tunacode_cli.egg-info → tunacode_cli-0.0.16}/PKG-INFO +1 -1
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/pyproject.toml +1 -1
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/commands.py +101 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/constants.py +1 -1
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16/src/tunacode_cli.egg-info}/PKG-INFO +1 -1
- {tunacode_cli-0.0.14 → 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.14 → tunacode_cli-0.0.16}/LICENSE +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/README.md +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/setup.cfg +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/setup.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/main.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/repl.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/textual_app.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/cli/textual_bridge.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/configuration/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/configuration/defaults.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/configuration/models.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/configuration/settings.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/context.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/agents/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/agents/main.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/agent_setup.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/base.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/config_setup.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/coordinator.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/environment_setup.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/setup/git_safety_setup.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/state.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/core/tool_handler.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/exceptions.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/prompts/system.txt +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/py.typed +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/services/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/services/mcp.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/setup.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/base.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/bash.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/grep.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/read_file.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/run_command.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/update_file.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/tools/write_file.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/types.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/completers.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/console.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/constants.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/decorators.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/input.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/keybindings.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/lexers.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/output.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/panels.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/prompt_manager.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/tool_ui.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/ui/validators.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/__init__.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/bm25.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/diff_utils.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/file_utils.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/ripgrep.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/system.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/text_utils.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode/utils/user_configuration.py +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/dependency_links.txt +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/entry_points.txt +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/requires.txt +0 -0
- {tunacode_cli-0.0.14 → tunacode_cli-0.0.16}/src/tunacode_cli.egg-info/top_level.txt +0 -0
|
@@ -493,6 +493,106 @@ class CompactCommand(SimpleCommand):
|
|
|
493
493
|
context.state_manager.session.messages = context.state_manager.session.messages[-2:]
|
|
494
494
|
|
|
495
495
|
|
|
496
|
+
class UpdateCommand(SimpleCommand):
|
|
497
|
+
"""Update TunaCode to the latest version."""
|
|
498
|
+
|
|
499
|
+
def __init__(self):
|
|
500
|
+
super().__init__(
|
|
501
|
+
CommandSpec(
|
|
502
|
+
name="update",
|
|
503
|
+
aliases=["/update"],
|
|
504
|
+
description="Update TunaCode to the latest version",
|
|
505
|
+
category=CommandCategory.SYSTEM,
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
510
|
+
import subprocess
|
|
511
|
+
import sys
|
|
512
|
+
import shutil
|
|
513
|
+
|
|
514
|
+
await ui.info("Checking for TunaCode updates...")
|
|
515
|
+
|
|
516
|
+
# Detect installation method
|
|
517
|
+
installation_method = None
|
|
518
|
+
|
|
519
|
+
# Check if installed via pipx
|
|
520
|
+
if shutil.which("pipx"):
|
|
521
|
+
try:
|
|
522
|
+
result = subprocess.run(
|
|
523
|
+
["pipx", "list"],
|
|
524
|
+
capture_output=True,
|
|
525
|
+
text=True,
|
|
526
|
+
timeout=10
|
|
527
|
+
)
|
|
528
|
+
if "tunacode" in result.stdout.lower():
|
|
529
|
+
installation_method = "pipx"
|
|
530
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
|
|
531
|
+
pass
|
|
532
|
+
|
|
533
|
+
# Check if installed via pip
|
|
534
|
+
if not installation_method:
|
|
535
|
+
try:
|
|
536
|
+
result = subprocess.run(
|
|
537
|
+
[sys.executable, "-m", "pip", "show", "tunacode-cli"],
|
|
538
|
+
capture_output=True,
|
|
539
|
+
text=True,
|
|
540
|
+
timeout=10
|
|
541
|
+
)
|
|
542
|
+
if result.returncode == 0:
|
|
543
|
+
installation_method = "pip"
|
|
544
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
|
|
545
|
+
pass
|
|
546
|
+
|
|
547
|
+
if not installation_method:
|
|
548
|
+
await ui.error("Could not detect TunaCode installation method")
|
|
549
|
+
await ui.muted("Manual update options:")
|
|
550
|
+
await ui.muted(" pipx: pipx upgrade tunacode")
|
|
551
|
+
await ui.muted(" pip: pip install --upgrade tunacode-cli")
|
|
552
|
+
return
|
|
553
|
+
|
|
554
|
+
# Perform update based on detected method
|
|
555
|
+
try:
|
|
556
|
+
if installation_method == "pipx":
|
|
557
|
+
await ui.info("Updating via pipx...")
|
|
558
|
+
result = subprocess.run(
|
|
559
|
+
["pipx", "upgrade", "tunacode"],
|
|
560
|
+
capture_output=True,
|
|
561
|
+
text=True,
|
|
562
|
+
timeout=60
|
|
563
|
+
)
|
|
564
|
+
else: # pip
|
|
565
|
+
await ui.info("Updating via pip...")
|
|
566
|
+
result = subprocess.run(
|
|
567
|
+
[sys.executable, "-m", "pip", "install", "--upgrade", "tunacode-cli"],
|
|
568
|
+
capture_output=True,
|
|
569
|
+
text=True,
|
|
570
|
+
timeout=60
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
if result.returncode == 0:
|
|
574
|
+
await ui.success("TunaCode updated successfully!")
|
|
575
|
+
await ui.muted("Restart TunaCode to use the new version")
|
|
576
|
+
|
|
577
|
+
# Show update output if available
|
|
578
|
+
if result.stdout.strip():
|
|
579
|
+
output_lines = result.stdout.strip().split('\n')
|
|
580
|
+
for line in output_lines[-5:]: # Show last 5 lines
|
|
581
|
+
if line.strip():
|
|
582
|
+
await ui.muted(f" {line}")
|
|
583
|
+
else:
|
|
584
|
+
await ui.error("Update failed")
|
|
585
|
+
if result.stderr:
|
|
586
|
+
await ui.muted(f"Error: {result.stderr.strip()}")
|
|
587
|
+
|
|
588
|
+
except subprocess.TimeoutExpired:
|
|
589
|
+
await ui.error("Update timed out")
|
|
590
|
+
except subprocess.CalledProcessError as e:
|
|
591
|
+
await ui.error(f"Update failed: {e}")
|
|
592
|
+
except FileNotFoundError:
|
|
593
|
+
await ui.error(f"Could not find {installation_method} executable")
|
|
594
|
+
|
|
595
|
+
|
|
496
596
|
class ModelCommand(SimpleCommand):
|
|
497
597
|
"""Manage model selection."""
|
|
498
598
|
|
|
@@ -628,6 +728,7 @@ class CommandRegistry:
|
|
|
628
728
|
FixCommand,
|
|
629
729
|
ParseToolsCommand,
|
|
630
730
|
RefreshConfigCommand,
|
|
731
|
+
UpdateCommand,
|
|
631
732
|
HelpCommand,
|
|
632
733
|
BranchCommand,
|
|
633
734
|
# TunaCodeCommand, # TODO: Temporarily disabled
|
|
@@ -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
|