diff --git a/CHANGELOG.md b/CHANGELOG.md index 21007aafe..201dc8817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -223,6 +223,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). prefix check using OS path separators. Added regression test tagged `@tdd_issue_7558`. +- **File Edit Encoding Parameter** (#7559): `_handle_file_edit()` now correctly + respects the `encoding` parameter when reading and writing files. Previously the + function ignored the caller-supplied encoding and fell back to the platform default, + causing data corruption with non-UTF-8 files. The fix extracts `encoding` from the + tool inputs (defaulting to `"utf-8"`) and passes it to both `path.read_text()` and + `path.write_text()`. The `FILE_EDIT_SPEC` input schema was updated to declare the + `encoding` field. BDD scenarios were added to cover explicit encoding and the + UTF-8 default. + - **Validation Gate Empty-Run Guard** (#7508): Fixed `ApplyValidationSummary.all_required_passed` returning `True` when zero validations were run, silently bypassing the apply gate. The property now returns `False` when the validation result set is empty (`is_empty` is `True`), ensuring diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index cb09b3a56..637dd5b31 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -19,3 +19,4 @@ Below are some of the specific details of various contributions. * HAL 9000 has contributed the plan concurrency race-condition fix (#7989): wired `LockService` into the plan lifecycle, guarding `execute_plan()` and `apply_plan()` with plan-level advisory locks and unique per-invocation owner identities to prevent silent concurrent state corruption. * This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc. * HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system. +* HAL 9000 has contributed the file edit encoding parameter fix (PR #8258 / issue #7559). diff --git a/features/steps/tool_builtins_steps.py b/features/steps/tool_builtins_steps.py index 012f748bd..7569e85c6 100644 --- a/features/steps/tool_builtins_steps.py +++ b/features/steps/tool_builtins_steps.py @@ -62,6 +62,15 @@ def step_given_file_with_content(context: Any, name: str, content: str) -> None: path.write_text(content) +@given('an encoded file "{name}" with encoding "{encoding}" and content "{content}"') +def step_given_file_with_content_encoding( + context: Any, name: str, encoding: str, content: str +) -> None: + path = Path(context.sandbox_dir) / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding=encoding) + + @given('a file "{name}" with lines "{csv_lines}"') def step_given_file_with_lines(context: Any, name: str, csv_lines: str) -> None: lines = csv_lines.split(",") @@ -149,6 +158,24 @@ def step_when_file_edit(context: Any, old: str, new: str) -> None: ) +@when( + 'I edit file "edit-enc.txt" replacing "{old}" with "{new}" specifying encoding "{encoding}"' +) +def step_when_file_edit_with_encoding( + context: Any, old: str, new: str, encoding: str +) -> None: + _run_tool( + context, + "builtin/file-edit", + { + "path": "edit-enc.txt", + "old_text": old, + "new_text": new, + "encoding": encoding, + }, + ) + + @when('I execute the "builtin/file-edit" tool replacing all "{old}" with "{new}"') def step_when_file_edit_all(context: Any, old: str, new: str) -> None: _run_tool( diff --git a/features/tool_builtins.feature b/features/tool_builtins.feature index 944faaffb..ea7f94141 100644 --- a/features/tool_builtins.feature +++ b/features/tool_builtins.feature @@ -69,6 +69,20 @@ Feature: Built-in File Tools Then the tool result should not be successful And the tool result error should mention "not found" + Scenario: Edit file uses explicit encoding parameter + Given a temporary sandbox directory + And an encoded file "edit-enc.txt" with encoding "latin-1" and content "café" + When I edit file "edit-enc.txt" replacing "café" with "naïve" specifying encoding "latin-1" + Then the tool result should be successful + And the output "replacements" should equal 1 + + Scenario: Edit file defaults to utf-8 encoding when not specified + Given a temporary sandbox directory + And a file "edit.txt" with content "default encoding test" + When I execute the "builtin/file-edit" tool replacing "default" with "explicit" + Then the tool result should be successful + And the output "replacements" should equal 1 + # ---- File Delete ---- Scenario: Delete an existing file diff --git a/src/cleveragents/tool/builtins/file_tools.py b/src/cleveragents/tool/builtins/file_tools.py index 468e7bd90..5968ecaf3 100644 --- a/src/cleveragents/tool/builtins/file_tools.py +++ b/src/cleveragents/tool/builtins/file_tools.py @@ -144,8 +144,9 @@ def _handle_file_edit(inputs: dict[str, Any]) -> dict[str, Any]: old_text: str = inputs["old_text"] new_text: str = inputs["new_text"] replace_all: bool = inputs.get("replace_all", False) + encoding: str = inputs.get("encoding", "utf-8") - content = path.read_text() + content = path.read_text(encoding=encoding) count = content.count(old_text) if count == 0: @@ -157,7 +158,7 @@ def _handle_file_edit(inputs: dict[str, Any]) -> dict[str, Any]: content = content.replace(old_text, new_text, 1) count = 1 - path.write_text(content) + path.write_text(content, encoding=encoding) return { "path": str(path), @@ -330,6 +331,11 @@ FILE_EDIT_SPEC = ToolSpec( "default": False, "description": "Replace all occurrences", }, + "encoding": { + "type": "string", + "default": "utf-8", + "description": "File encoding", + }, }, "required": ["path", "old_text", "new_text"], },