tunacode-cli 0.0.15__tar.gz → 0.0.17__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.

Files changed (74) hide show
  1. {tunacode_cli-0.0.15/src/tunacode_cli.egg-info → tunacode_cli-0.0.17}/PKG-INFO +14 -1
  2. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/README.md +14 -1
  3. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/pyproject.toml +1 -1
  4. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/constants.py +1 -1
  5. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/console.py +3 -2
  6. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/output.py +5 -0
  7. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17/src/tunacode_cli.egg-info}/PKG-INFO +14 -1
  8. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode_cli.egg-info/SOURCES.txt +2 -1
  9. tunacode_cli-0.0.17/tests/test_escape_mechanism.py +184 -0
  10. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/LICENSE +0 -0
  11. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/setup.cfg +0 -0
  12. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/setup.py +0 -0
  13. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/__init__.py +0 -0
  14. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/cli/__init__.py +0 -0
  15. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/cli/commands.py +0 -0
  16. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/cli/main.py +0 -0
  17. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/cli/repl.py +0 -0
  18. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/cli/textual_app.py +0 -0
  19. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/cli/textual_bridge.py +0 -0
  20. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/configuration/__init__.py +0 -0
  21. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/configuration/defaults.py +0 -0
  22. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/configuration/models.py +0 -0
  23. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/configuration/settings.py +0 -0
  24. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/context.py +0 -0
  25. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/__init__.py +0 -0
  26. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/agents/__init__.py +0 -0
  27. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/agents/main.py +0 -0
  28. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/setup/__init__.py +0 -0
  29. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/setup/agent_setup.py +0 -0
  30. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/setup/base.py +0 -0
  31. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/setup/config_setup.py +0 -0
  32. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/setup/coordinator.py +0 -0
  33. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/setup/environment_setup.py +0 -0
  34. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/setup/git_safety_setup.py +0 -0
  35. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/state.py +0 -0
  36. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/core/tool_handler.py +0 -0
  37. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/exceptions.py +0 -0
  38. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/prompts/system.txt +0 -0
  39. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/py.typed +0 -0
  40. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/services/__init__.py +0 -0
  41. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/services/mcp.py +0 -0
  42. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/setup.py +0 -0
  43. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/tools/__init__.py +0 -0
  44. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/tools/base.py +0 -0
  45. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/tools/bash.py +0 -0
  46. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/tools/grep.py +0 -0
  47. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/tools/read_file.py +0 -0
  48. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/tools/run_command.py +0 -0
  49. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/tools/update_file.py +0 -0
  50. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/tools/write_file.py +0 -0
  51. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/types.py +0 -0
  52. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/__init__.py +0 -0
  53. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/completers.py +0 -0
  54. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/constants.py +0 -0
  55. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/decorators.py +0 -0
  56. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/input.py +0 -0
  57. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/keybindings.py +0 -0
  58. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/lexers.py +0 -0
  59. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/panels.py +0 -0
  60. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/prompt_manager.py +0 -0
  61. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/tool_ui.py +0 -0
  62. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/ui/validators.py +0 -0
  63. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/utils/__init__.py +0 -0
  64. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/utils/bm25.py +0 -0
  65. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/utils/diff_utils.py +0 -0
  66. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/utils/file_utils.py +0 -0
  67. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/utils/ripgrep.py +0 -0
  68. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/utils/system.py +0 -0
  69. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/utils/text_utils.py +0 -0
  70. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode/utils/user_configuration.py +0 -0
  71. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode_cli.egg-info/dependency_links.txt +0 -0
  72. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode_cli.egg-info/entry_points.txt +0 -0
  73. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode_cli.egg-info/requires.txt +0 -0
  74. {tunacode_cli-0.0.15 → tunacode_cli-0.0.17}/src/tunacode_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.15
3
+ Version: 0.0.17
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License-Expression: MIT
@@ -475,3 +475,16 @@ MIT License - see [LICENSE](LICENSE) file for details.
475
475
  ## Acknowledgments
476
476
 
477
477
  TunaCode is a fork of [sidekick-cli](https://github.com/geekforbrains/sidekick-cli). Special thanks to the sidekick-cli team for creating the foundation that made TunaCode possible.
478
+
479
+ ### Key Differences from sidekick-cli
480
+
481
+ While TunaCode builds on the foundation of sidekick-cli, we've made several architectural changes for our workflow:
482
+
483
+ - **JSON Tool Parsing Fallback**: Added fallback parsing for when API providers fail with structured tool calling
484
+ - **Parallel Search Tools**: New `bash` and `grep` tools with parallel execution for codebase navigation
485
+ - **ReAct Reasoning**: Implemented ReAct (Reasoning + Acting) patterns with configurable iteration limits
486
+ - **Dynamic Configuration**: Added `/refresh` command and modified configuration management
487
+ - **Safety Changes**: Removed automatic git commits and `/undo` command - requires explicit git usage
488
+ - **Error Recovery**: Multiple fallback mechanisms and orphaned tool call recovery
489
+ - **Tool System Rewrite**: Complete overhaul of internal tools with atomic operations and different confirmation UIs
490
+ - **Debug Commands**: Added `/parsetools`, `/thoughts`, `/iterations` for debugging
@@ -438,4 +438,17 @@ MIT License - see [LICENSE](LICENSE) file for details.
438
438
 
439
439
  ## Acknowledgments
440
440
 
441
- TunaCode is a fork of [sidekick-cli](https://github.com/geekforbrains/sidekick-cli). Special thanks to the sidekick-cli team for creating the foundation that made TunaCode possible.
441
+ TunaCode is a fork of [sidekick-cli](https://github.com/geekforbrains/sidekick-cli). Special thanks to the sidekick-cli team for creating the foundation that made TunaCode possible.
442
+
443
+ ### Key Differences from sidekick-cli
444
+
445
+ While TunaCode builds on the foundation of sidekick-cli, we've made several architectural changes for our workflow:
446
+
447
+ - **JSON Tool Parsing Fallback**: Added fallback parsing for when API providers fail with structured tool calling
448
+ - **Parallel Search Tools**: New `bash` and `grep` tools with parallel execution for codebase navigation
449
+ - **ReAct Reasoning**: Implemented ReAct (Reasoning + Acting) patterns with configurable iteration limits
450
+ - **Dynamic Configuration**: Added `/refresh` command and modified configuration management
451
+ - **Safety Changes**: Removed automatic git commits and `/undo` command - requires explicit git usage
452
+ - **Error Recovery**: Multiple fallback mechanisms and orphaned tool call recovery
453
+ - **Tool System Rewrite**: Complete overhaul of internal tools with atomic operations and different confirmation UIs
454
+ - **Debug Commands**: Added `/parsetools`, `/thoughts`, `/iterations` for debugging
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tunacode-cli"
7
- version = "0.0.15"
7
+ version = "0.0.17"
8
8
  description = "Your agentic CLI developer."
9
9
  keywords = ["cli", "agent", "development", "automation"]
10
10
  readme = "README.md"
@@ -7,7 +7,7 @@ Centralizes all magic strings, UI text, error messages, and application constant
7
7
 
8
8
  # Application info
9
9
  APP_NAME = "TunaCode"
10
- APP_VERSION = "0.0.15"
10
+ APP_VERSION = "0.0.17"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"
@@ -9,8 +9,8 @@ from rich.markdown import Markdown
9
9
  # Import and re-export all functions from specialized modules
10
10
  from .input import formatted_text, input, multiline_input
11
11
  from .keybindings import create_key_bindings
12
- from .output import (banner, clear, info, line, muted, print, spinner, success, sync_print,
13
- update_available, usage, version, warning)
12
+ from .output import (banner, clear, info, line, muted, print, show_update_message, spinner, success,
13
+ sync_print, update_available, usage, version, warning)
14
14
  from .panels import (agent, dump_messages, error, help, models, panel, sync_panel,
15
15
  sync_tool_confirm, tool_confirm)
16
16
  from .prompt_manager import PromptConfig, PromptManager
@@ -46,6 +46,7 @@ __all__ = [
46
46
  "line",
47
47
  "muted",
48
48
  "print",
49
+ "show_update_message",
49
50
  "spinner",
50
51
  "success",
51
52
  "sync_print",
@@ -89,6 +89,11 @@ async def update_available(latest_version: str) -> None:
89
89
  await muted(MSG_UPDATE_INSTRUCTION)
90
90
 
91
91
 
92
+ async def show_update_message(latest_version: str) -> None:
93
+ """Display update available message (alias for update_available)."""
94
+ await update_available(latest_version)
95
+
96
+
92
97
  async def spinner(show: bool = True, spinner_obj=None, state_manager: StateManager = None):
93
98
  """Manage a spinner display."""
94
99
  icon = SPINNER_TYPE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.15
3
+ Version: 0.0.17
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License-Expression: MIT
@@ -475,3 +475,16 @@ MIT License - see [LICENSE](LICENSE) file for details.
475
475
  ## Acknowledgments
476
476
 
477
477
  TunaCode is a fork of [sidekick-cli](https://github.com/geekforbrains/sidekick-cli). Special thanks to the sidekick-cli team for creating the foundation that made TunaCode possible.
478
+
479
+ ### Key Differences from sidekick-cli
480
+
481
+ While TunaCode builds on the foundation of sidekick-cli, we've made several architectural changes for our workflow:
482
+
483
+ - **JSON Tool Parsing Fallback**: Added fallback parsing for when API providers fail with structured tool calling
484
+ - **Parallel Search Tools**: New `bash` and `grep` tools with parallel execution for codebase navigation
485
+ - **ReAct Reasoning**: Implemented ReAct (Reasoning + Acting) patterns with configurable iteration limits
486
+ - **Dynamic Configuration**: Added `/refresh` command and modified configuration management
487
+ - **Safety Changes**: Removed automatic git commits and `/undo` command - requires explicit git usage
488
+ - **Error Recovery**: Multiple fallback mechanisms and orphaned tool call recovery
489
+ - **Tool System Rewrite**: Complete overhaul of internal tools with atomic operations and different confirmation UIs
490
+ - **Debug Commands**: Added `/parsetools`, `/thoughts`, `/iterations` for debugging
@@ -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