From 72544aa82e4eb6c7db74495119ea39c5aa232cf4 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Fri, 12 Dec 2025 18:11:40 -0800 Subject: [PATCH 1/7] feat: Add ReplaceFileContent function with line count verification - Adds new ReplaceFileContent function for AI-safe file replacement - Requires oldContentLineCount parameter as verification (CAPTCHA for agents) - Prevents accidental overwrites by validating line count matches - Returns helpful error if verification fails - Saves old content to edit history for undo support --- .../StrReplaceEditorHelperFunctions.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs index be8bab2e..f6be8445 100644 --- a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs +++ b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs @@ -305,6 +305,43 @@ public string CreateFile( return $"Created file {path} with {fileText.Length} characters."; } + [ReadOnly(false)] + [Description("Replaces the entire content of an existing file. Requires knowing the current line count as verification that you've read the file.")] + public string ReplaceFileContent( + [Description("Absolute or relative path to file.")] string path, + [Description("New content to replace the entire file.")] string newContent, + [Description("Current line count of the file (for verification).")] int oldContentLineCount) + { + if (!File.Exists(path)) + { + return $"File {path} does not exist. Use CreateFile to create a new file."; + } + + // Read current content and count lines + var currentContent = File.ReadAllText(path); + var currentLines = currentContent.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + var actualLineCount = currentLines.Length; + + // Verify line count matches + if (actualLineCount != oldContentLineCount) + { + return $"Line count mismatch: you specified {oldContentLineCount} but file has {actualLineCount} lines. Please read the file with ViewFile and verify the line count."; + } + + // Save current content for undo + if (!EditHistory.ContainsKey(path)) + { + EditHistory[path] = currentContent; + } + + // Replace entire content + File.WriteAllText(path, newContent); + var newLines = newContent.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + + return $"Replaced content in {path}: {actualLineCount} lines → {newLines.Length} lines ({newContent.Length} characters)."; + } + + // [ReadOnly(false)] // [Description("Replaces the lines in the file at `path` from `startLine` to `endLine` with the new string `newStr`. If `endLine` is -1, all remaining lines are replaced.")] // public string LinesReplace( From af9e42bab664c7dfb76c3ba5cdabb747aecb451d Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Sun, 14 Dec 2025 13:27:38 -0800 Subject: [PATCH 2/7] feat: Add ReplaceMultipleInFile function and fix string array deserialization bug - Add ReplaceMultipleInFile function for atomic multi-pattern replacement in single file - Fix bug in FunctionFactory.cs where string arrays included quotes (GetRawText vs GetString) - Apply fix to Array, List, and Tuple deserialization - Clean, concise output: 'File X updated: replaced N occurrences.' - Validates all patterns before making any changes (atomic operation) - Supports undo via EditHistory --- src/cycod/FunctionCalling/FunctionFactory.cs | 9 ++- .../StrReplaceEditorHelperFunctions.cs | 69 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/cycod/FunctionCalling/FunctionFactory.cs b/src/cycod/FunctionCalling/FunctionFactory.cs index 18bdeaa6..877c6f03 100644 --- a/src/cycod/FunctionCalling/FunctionFactory.cs +++ b/src/cycod/FunctionCalling/FunctionFactory.cs @@ -318,7 +318,8 @@ private static object CreateGenericCollectionFromJsonArray(string parameterValue var collection = Array.CreateInstance(elementType, array.Length); for (int i = 0; i < array.Length; i++) { - var parsed = ParseParameterValue(array[i].GetRawText(), elementType); + var elementValue = array[i].ValueKind == JsonValueKind.String ? array[i].GetString()! : array[i].GetRawText(); + var parsed = ParseParameterValue(elementValue, elementType); if (parsed != null) collection.SetValue(parsed, i); } return collection; @@ -329,7 +330,8 @@ private static object CreateGenericCollectionFromJsonArray(string parameterValue var list = collection as IList; foreach (var item in array) { - var parsed = ParseParameterValue(item.GetRawText(), elementType); + var elementValue = item.ValueKind == JsonValueKind.String ? item.GetString()! : item.GetRawText(); + var parsed = ParseParameterValue(elementValue, elementType); if (parsed != null) list!.Add(parsed); } return collection!; @@ -349,7 +351,8 @@ private static object CreateTuppleTypeFromJsonArray(string parameterValue, Type foreach (var item in array) { - var parsed = ParseParameterValue(item.GetRawText(), elementType); + var elementValue = item.ValueKind == JsonValueKind.String ? item.GetString()! : item.GetRawText(); + var parsed = ParseParameterValue(elementValue, elementType); if (parsed != null) list!.Add(parsed); } diff --git a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs index f6be8445..a32b5602 100644 --- a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs +++ b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs @@ -415,6 +415,75 @@ public string ReplaceOneInFile( return $"File {path} updated: replaced 1 occurrence of specified text."; } + [ReadOnly(false)] + [Description("Replaces multiple text patterns in a single file atomically. All replacements must be unique occurrences.")] + public string ReplaceMultipleInFile( + [Description("Absolute or relative path to file.")] string path, + [Description("Array of old strings to be replaced. Each must match exactly one occurrence.")] string[] oldStrings, + [Description("Array of new strings to replace with. Must be same length as oldStrings.")] string[] newStrings) + { + if (!File.Exists(path)) + { + return $"File {path} does not exist."; + } + + if (oldStrings.Length != newStrings.Length) + { + return $"Error: oldStrings array length ({oldStrings.Length}) must match newStrings array length ({newStrings.Length})."; + } + + if (oldStrings.Length == 0) + { + return "Error: No replacements specified."; + } + + var originalText = FileHelpers.ReadAllText(path); + var currentText = originalText; + + // Validate all patterns exist and are unique before making any changes + for (int i = 0; i < oldStrings.Length; i++) + { + var oldStr = oldStrings[i]; + var newStr = newStrings[i]; + + var testReplacement = StringHelpers.ReplaceOnce(currentText, oldStr, newStr, out var countFound); + if (countFound != 1) + { + var message = countFound == 0 + ? $"Replacement {i + 1}: No occurrences of specified text found." + : $"Replacement {i + 1}: Multiple matches found for specified text; must be unique."; + return $"{message}\nNo changes made to {path}."; + } + } + + // All validations passed, now perform the replacements + for (int i = 0; i < oldStrings.Length; i++) + { + var oldStr = oldStrings[i]; + var newStr = newStrings[i]; + + var replacedText = StringHelpers.ReplaceOnce(currentText, oldStr, newStr, out var countFound); + if (replacedText != null && countFound == 1) + { + currentText = replacedText; + } + else + { + // This shouldn't happen since we validated, but handle gracefully + return $"Unexpected error during replacement {i + 1}. File may be in inconsistent state."; + } + } + + // Save original content for undo before writing + if (!EditHistory.ContainsKey(path)) + { + EditHistory[path] = originalText; + } + + File.WriteAllText(path, currentText); + + return $"File {path} updated: replaced {oldStrings.Length} occurrences."; + } [ReadOnly(false)] [Description("Inserts the specified string `newStr` into the file at `path` after the specified line number (`insertLine`). Use 0 to insert at the beginning of the file.")] public string Insert( From 82b7767dd0c53b1a8de90112429af3c371888aec Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Fri, 19 Dec 2025 10:25:13 -0800 Subject: [PATCH 3/7] Add comprehensive tests for ReplaceFileContent and ReplaceMultipleInFile functions --- .../file-replacement-functions.yaml | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 tests/cycod-yaml/file-replacement-functions.yaml diff --git a/tests/cycod-yaml/file-replacement-functions.yaml b/tests/cycod-yaml/file-replacement-functions.yaml new file mode 100644 index 00000000..1fe5401a --- /dev/null +++ b/tests/cycod-yaml/file-replacement-functions.yaml @@ -0,0 +1,154 @@ +tests: + # ReplaceFileContent tests + - name: ReplaceFileContent with correct line count succeeds + steps: + - name: Run test + run: | + echo "line 1 + line 2 + line 3" > test-replace-content.txt + cycod --input "use ReplaceFileContent to replace test-replace-content.txt with 'new content', it has 3 lines" --auto-approve * + expect-regex: | + assistant-function: ReplaceFileContent + path.*test-replace-content\.txt + newContent.*new content + oldContentLineCount.*3 + Replaced content.*3 lines.*1 lines + timeout: 30000 + - name: Cleanup + run: rm -f test-replace-content.txt + optional: needsAI + + - name: ReplaceFileContent with wrong line count fails + steps: + - name: Run test + run: | + echo "line 1 + line 2 + line 3" > test-replace-fail.txt + cycod --input "use ReplaceFileContent to replace test-replace-fail.txt with 'new', say it has 5 lines (which is wrong)" --auto-approve * + expect-regex: | + assistant-function: ReplaceFileContent + oldContentLineCount.*5 + Line count mismatch.*you specified 5 but file has 3 lines + timeout: 30000 + - name: Cleanup + run: rm -f test-replace-fail.txt + optional: needsAI + + - name: ReplaceFileContent on non-existent file fails + run: | + cycod --input "use ReplaceFileContent to replace nonexistent-file.txt with 'content', say it has 1 line" --auto-approve * + expect-regex: | + assistant-function: ReplaceFileContent + File.*does not exist.*Use CreateFile + timeout: 30000 + optional: needsAI + + - name: ReplaceFileContent can be undone + steps: + - name: Run test + run: | + echo "original content" > test-undo-replace.txt + cycod --input "First use ReplaceFileContent to replace test-undo-replace.txt with 'new content', it has 1 line. Then use UndoEdit to revert it." --auto-approve * + expect-regex: | + assistant-function: ReplaceFileContent + Replaced content + assistant-function: UndoEdit + Reverted last edit + timeout: 45000 + - name: Cleanup + run: rm -f test-undo-replace.txt + optional: needsAI + + # ReplaceMultipleInFile tests + - name: ReplaceMultipleInFile replaces multiple patterns atomically + steps: + - name: Run test + run: | + echo "foo bar baz" > test-multi-replace.txt + cycod --input "use ReplaceMultipleInFile on test-multi-replace.txt to replace 'foo' with 'FOO' and 'bar' with 'BAR'" --auto-approve * + expect-regex: | + assistant-function: ReplaceMultipleInFile + path.*test-multi-replace\.txt + oldStrings.*foo.*bar + newStrings.*FOO.*BAR + replaced 2 occurrences + timeout: 30000 + - name: Cleanup + run: rm -f test-multi-replace.txt + optional: needsAI + + - name: ReplaceMultipleInFile fails if pattern not found + steps: + - name: Run test + run: | + echo "foo bar" > test-multi-fail.txt + cycod --input "use ReplaceMultipleInFile on test-multi-fail.txt to replace 'foo' with 'FOO' and 'baz' with 'BAZ' (baz doesn't exist)" --auto-approve * + expect-regex: | + assistant-function: ReplaceMultipleInFile + No occurrences.*found|not found + No changes made + timeout: 30000 + - name: Cleanup + run: rm -f test-multi-fail.txt + optional: needsAI + + - name: ReplaceMultipleInFile fails if array lengths don't match + steps: + - name: Run test + run: | + echo "foo bar baz" > test-array-length.txt + cycod --input "use ReplaceMultipleInFile on test-array-length.txt with oldStrings=['foo','bar'] but newStrings=['FOO'] (mismatched lengths)" --auto-approve * + expect-regex: | + assistant-function: ReplaceMultipleInFile + array length.*must match + timeout: 30000 + - name: Cleanup + run: rm -f test-array-length.txt + optional: needsAI + + - name: ReplaceMultipleInFile handles string arrays correctly + steps: + - name: Run test + run: | + echo "alpha beta gamma delta" > test-string-array.txt + cycod --input "use ReplaceMultipleInFile on test-string-array.txt to replace ['alpha', 'beta', 'gamma'] with ['ALPHA', 'BETA', 'GAMMA']" --auto-approve * + expect-regex: | + assistant-function: ReplaceMultipleInFile + replaced 3 occurrences + timeout: 30000 + - name: Cleanup + run: rm -f test-string-array.txt + optional: needsAI + + - name: ReplaceMultipleInFile can be undone + steps: + - name: Run test + run: | + echo "foo bar baz" > test-multi-undo.txt + cycod --input "First use ReplaceMultipleInFile on test-multi-undo.txt to replace 'foo' with 'FOO' and 'bar' with 'BAR'. Then use UndoEdit to revert." --auto-approve * + expect-regex: | + assistant-function: ReplaceMultipleInFile + replaced 2 occurrences + assistant-function: UndoEdit + Reverted last edit + timeout: 45000 + - name: Cleanup + run: rm -f test-multi-undo.txt + optional: needsAI + + - name: ReplaceMultipleInFile validates uniqueness of patterns + steps: + - name: Run test + run: | + echo "test test test" > test-unique.txt + cycod --input "use ReplaceMultipleInFile on test-unique.txt to replace 'test' with 'TEST' (but 'test' appears multiple times, so should fail)" --auto-approve * + expect-regex: | + assistant-function: ReplaceMultipleInFile + Multiple matches found.*must be unique + No changes made + timeout: 30000 + - name: Cleanup + run: rm -f test-unique.txt + optional: needsAI From 9f466d8e8f0dd7a6fab49c5b5a1d004e07c2b83a Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Fri, 19 Dec 2025 10:31:58 -0800 Subject: [PATCH 4/7] Fix shell glob expansion in test commands by quoting asterisk --- .../file-replacement-functions.yaml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/cycod-yaml/file-replacement-functions.yaml b/tests/cycod-yaml/file-replacement-functions.yaml index 1fe5401a..1e96a01f 100644 --- a/tests/cycod-yaml/file-replacement-functions.yaml +++ b/tests/cycod-yaml/file-replacement-functions.yaml @@ -7,7 +7,7 @@ tests: echo "line 1 line 2 line 3" > test-replace-content.txt - cycod --input "use ReplaceFileContent to replace test-replace-content.txt with 'new content', it has 3 lines" --auto-approve * + cycod --input "use ReplaceFileContent to replace test-replace-content.txt with 'new content', it has 3 lines" --auto-approve '*' expect-regex: | assistant-function: ReplaceFileContent path.*test-replace-content\.txt @@ -26,7 +26,7 @@ tests: echo "line 1 line 2 line 3" > test-replace-fail.txt - cycod --input "use ReplaceFileContent to replace test-replace-fail.txt with 'new', say it has 5 lines (which is wrong)" --auto-approve * + cycod --input "use ReplaceFileContent to replace test-replace-fail.txt with 'new', say it has 5 lines (which is wrong)" --auto-approve '*' expect-regex: | assistant-function: ReplaceFileContent oldContentLineCount.*5 @@ -38,7 +38,7 @@ tests: - name: ReplaceFileContent on non-existent file fails run: | - cycod --input "use ReplaceFileContent to replace nonexistent-file.txt with 'content', say it has 1 line" --auto-approve * + cycod --input "use ReplaceFileContent to replace nonexistent-file.txt with 'content', say it has 1 line" --auto-approve '*' expect-regex: | assistant-function: ReplaceFileContent File.*does not exist.*Use CreateFile @@ -50,7 +50,7 @@ tests: - name: Run test run: | echo "original content" > test-undo-replace.txt - cycod --input "First use ReplaceFileContent to replace test-undo-replace.txt with 'new content', it has 1 line. Then use UndoEdit to revert it." --auto-approve * + cycod --input "First use ReplaceFileContent to replace test-undo-replace.txt with 'new content', it has 1 line. Then use UndoEdit to revert it." --auto-approve '*' expect-regex: | assistant-function: ReplaceFileContent Replaced content @@ -67,7 +67,7 @@ tests: - name: Run test run: | echo "foo bar baz" > test-multi-replace.txt - cycod --input "use ReplaceMultipleInFile on test-multi-replace.txt to replace 'foo' with 'FOO' and 'bar' with 'BAR'" --auto-approve * + cycod --input "use ReplaceMultipleInFile on test-multi-replace.txt to replace 'foo' with 'FOO' and 'bar' with 'BAR'" --auto-approve '*' expect-regex: | assistant-function: ReplaceMultipleInFile path.*test-multi-replace\.txt @@ -84,7 +84,7 @@ tests: - name: Run test run: | echo "foo bar" > test-multi-fail.txt - cycod --input "use ReplaceMultipleInFile on test-multi-fail.txt to replace 'foo' with 'FOO' and 'baz' with 'BAZ' (baz doesn't exist)" --auto-approve * + cycod --input "use ReplaceMultipleInFile on test-multi-fail.txt to replace 'foo' with 'FOO' and 'baz' with 'BAZ' (baz doesn't exist)" --auto-approve '*' expect-regex: | assistant-function: ReplaceMultipleInFile No occurrences.*found|not found @@ -99,7 +99,7 @@ tests: - name: Run test run: | echo "foo bar baz" > test-array-length.txt - cycod --input "use ReplaceMultipleInFile on test-array-length.txt with oldStrings=['foo','bar'] but newStrings=['FOO'] (mismatched lengths)" --auto-approve * + cycod --input "use ReplaceMultipleInFile on test-array-length.txt with oldStrings=['foo','bar'] but newStrings=['FOO'] (mismatched lengths)" --auto-approve '*' expect-regex: | assistant-function: ReplaceMultipleInFile array length.*must match @@ -113,7 +113,7 @@ tests: - name: Run test run: | echo "alpha beta gamma delta" > test-string-array.txt - cycod --input "use ReplaceMultipleInFile on test-string-array.txt to replace ['alpha', 'beta', 'gamma'] with ['ALPHA', 'BETA', 'GAMMA']" --auto-approve * + cycod --input "use ReplaceMultipleInFile on test-string-array.txt to replace ['alpha', 'beta', 'gamma'] with ['ALPHA', 'BETA', 'GAMMA']" --auto-approve '*' expect-regex: | assistant-function: ReplaceMultipleInFile replaced 3 occurrences @@ -127,7 +127,7 @@ tests: - name: Run test run: | echo "foo bar baz" > test-multi-undo.txt - cycod --input "First use ReplaceMultipleInFile on test-multi-undo.txt to replace 'foo' with 'FOO' and 'bar' with 'BAR'. Then use UndoEdit to revert." --auto-approve * + cycod --input "First use ReplaceMultipleInFile on test-multi-undo.txt to replace 'foo' with 'FOO' and 'bar' with 'BAR'. Then use UndoEdit to revert." --auto-approve '*' expect-regex: | assistant-function: ReplaceMultipleInFile replaced 2 occurrences @@ -143,7 +143,7 @@ tests: - name: Run test run: | echo "test test test" > test-unique.txt - cycod --input "use ReplaceMultipleInFile on test-unique.txt to replace 'test' with 'TEST' (but 'test' appears multiple times, so should fail)" --auto-approve * + cycod --input "use ReplaceMultipleInFile on test-unique.txt to replace 'test' with 'TEST' (but 'test' appears multiple times, so should fail)" --auto-approve '*' expect-regex: | assistant-function: ReplaceMultipleInFile Multiple matches found.*must be unique From 370e2cb61fb2eb969047a1942d4366a7c9da0e68 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Fri, 19 Dec 2025 10:45:13 -0800 Subject: [PATCH 5/7] Fix test format: use 'bash:' instead of 'run:' for multi-line shell scripts --- .../file-replacement-functions.yaml | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/cycod-yaml/file-replacement-functions.yaml b/tests/cycod-yaml/file-replacement-functions.yaml index 1e96a01f..1bc63971 100644 --- a/tests/cycod-yaml/file-replacement-functions.yaml +++ b/tests/cycod-yaml/file-replacement-functions.yaml @@ -3,7 +3,7 @@ tests: - name: ReplaceFileContent with correct line count succeeds steps: - name: Run test - run: | + bash: | echo "line 1 line 2 line 3" > test-replace-content.txt @@ -16,13 +16,13 @@ tests: Replaced content.*3 lines.*1 lines timeout: 30000 - name: Cleanup - run: rm -f test-replace-content.txt + bash: rm -f test-replace-content.txt optional: needsAI - name: ReplaceFileContent with wrong line count fails steps: - name: Run test - run: | + bash: | echo "line 1 line 2 line 3" > test-replace-fail.txt @@ -33,11 +33,11 @@ tests: Line count mismatch.*you specified 5 but file has 3 lines timeout: 30000 - name: Cleanup - run: rm -f test-replace-fail.txt + bash: rm -f test-replace-fail.txt optional: needsAI - name: ReplaceFileContent on non-existent file fails - run: | + bash: | cycod --input "use ReplaceFileContent to replace nonexistent-file.txt with 'content', say it has 1 line" --auto-approve '*' expect-regex: | assistant-function: ReplaceFileContent @@ -48,7 +48,7 @@ tests: - name: ReplaceFileContent can be undone steps: - name: Run test - run: | + bash: | echo "original content" > test-undo-replace.txt cycod --input "First use ReplaceFileContent to replace test-undo-replace.txt with 'new content', it has 1 line. Then use UndoEdit to revert it." --auto-approve '*' expect-regex: | @@ -58,14 +58,14 @@ tests: Reverted last edit timeout: 45000 - name: Cleanup - run: rm -f test-undo-replace.txt + bash: rm -f test-undo-replace.txt optional: needsAI # ReplaceMultipleInFile tests - name: ReplaceMultipleInFile replaces multiple patterns atomically steps: - name: Run test - run: | + bash: | echo "foo bar baz" > test-multi-replace.txt cycod --input "use ReplaceMultipleInFile on test-multi-replace.txt to replace 'foo' with 'FOO' and 'bar' with 'BAR'" --auto-approve '*' expect-regex: | @@ -76,13 +76,13 @@ tests: replaced 2 occurrences timeout: 30000 - name: Cleanup - run: rm -f test-multi-replace.txt + bash: rm -f test-multi-replace.txt optional: needsAI - name: ReplaceMultipleInFile fails if pattern not found steps: - name: Run test - run: | + bash: | echo "foo bar" > test-multi-fail.txt cycod --input "use ReplaceMultipleInFile on test-multi-fail.txt to replace 'foo' with 'FOO' and 'baz' with 'BAZ' (baz doesn't exist)" --auto-approve '*' expect-regex: | @@ -91,13 +91,13 @@ tests: No changes made timeout: 30000 - name: Cleanup - run: rm -f test-multi-fail.txt + bash: rm -f test-multi-fail.txt optional: needsAI - name: ReplaceMultipleInFile fails if array lengths don't match steps: - name: Run test - run: | + bash: | echo "foo bar baz" > test-array-length.txt cycod --input "use ReplaceMultipleInFile on test-array-length.txt with oldStrings=['foo','bar'] but newStrings=['FOO'] (mismatched lengths)" --auto-approve '*' expect-regex: | @@ -105,13 +105,13 @@ tests: array length.*must match timeout: 30000 - name: Cleanup - run: rm -f test-array-length.txt + bash: rm -f test-array-length.txt optional: needsAI - name: ReplaceMultipleInFile handles string arrays correctly steps: - name: Run test - run: | + bash: | echo "alpha beta gamma delta" > test-string-array.txt cycod --input "use ReplaceMultipleInFile on test-string-array.txt to replace ['alpha', 'beta', 'gamma'] with ['ALPHA', 'BETA', 'GAMMA']" --auto-approve '*' expect-regex: | @@ -119,13 +119,13 @@ tests: replaced 3 occurrences timeout: 30000 - name: Cleanup - run: rm -f test-string-array.txt + bash: rm -f test-string-array.txt optional: needsAI - name: ReplaceMultipleInFile can be undone steps: - name: Run test - run: | + bash: | echo "foo bar baz" > test-multi-undo.txt cycod --input "First use ReplaceMultipleInFile on test-multi-undo.txt to replace 'foo' with 'FOO' and 'bar' with 'BAR'. Then use UndoEdit to revert." --auto-approve '*' expect-regex: | @@ -135,13 +135,13 @@ tests: Reverted last edit timeout: 45000 - name: Cleanup - run: rm -f test-multi-undo.txt + bash: rm -f test-multi-undo.txt optional: needsAI - name: ReplaceMultipleInFile validates uniqueness of patterns steps: - name: Run test - run: | + bash: | echo "test test test" > test-unique.txt cycod --input "use ReplaceMultipleInFile on test-unique.txt to replace 'test' with 'TEST' (but 'test' appears multiple times, so should fail)" --auto-approve '*' expect-regex: | @@ -150,5 +150,5 @@ tests: No changes made timeout: 30000 - name: Cleanup - run: rm -f test-unique.txt + bash: rm -f test-unique.txt optional: needsAI From 91a8d08daafa6efcf9cbc296fd1e4428407146b4 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Fri, 19 Dec 2025 10:47:09 -0800 Subject: [PATCH 6/7] Fix echo commands to create proper 3-line files using -e flag --- tests/cycod-yaml/file-replacement-functions.yaml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/cycod-yaml/file-replacement-functions.yaml b/tests/cycod-yaml/file-replacement-functions.yaml index 1bc63971..cf921177 100644 --- a/tests/cycod-yaml/file-replacement-functions.yaml +++ b/tests/cycod-yaml/file-replacement-functions.yaml @@ -4,9 +4,7 @@ tests: steps: - name: Run test bash: | - echo "line 1 - line 2 - line 3" > test-replace-content.txt + echo -e 'line 1\nline 2\nline 3' > test-replace-content.txt cycod --input "use ReplaceFileContent to replace test-replace-content.txt with 'new content', it has 3 lines" --auto-approve '*' expect-regex: | assistant-function: ReplaceFileContent @@ -23,9 +21,7 @@ tests: steps: - name: Run test bash: | - echo "line 1 - line 2 - line 3" > test-replace-fail.txt + echo -e 'line 1\nline 2\nline 3' > test-replace-fail.txt cycod --input "use ReplaceFileContent to replace test-replace-fail.txt with 'new', say it has 5 lines (which is wrong)" --auto-approve '*' expect-regex: | assistant-function: ReplaceFileContent From 58d29728b823736cea01cdc56eaa8551763adbbb Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Fri, 19 Dec 2025 10:48:11 -0800 Subject: [PATCH 7/7] Fix line count: use printf instead of echo to avoid trailing newline --- tests/cycod-yaml/file-replacement-functions.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cycod-yaml/file-replacement-functions.yaml b/tests/cycod-yaml/file-replacement-functions.yaml index cf921177..463b21e9 100644 --- a/tests/cycod-yaml/file-replacement-functions.yaml +++ b/tests/cycod-yaml/file-replacement-functions.yaml @@ -4,7 +4,7 @@ tests: steps: - name: Run test bash: | - echo -e 'line 1\nline 2\nline 3' > test-replace-content.txt + printf 'line 1\nline 2\nline 3' > test-replace-content.txt cycod --input "use ReplaceFileContent to replace test-replace-content.txt with 'new content', it has 3 lines" --auto-approve '*' expect-regex: | assistant-function: ReplaceFileContent @@ -21,7 +21,7 @@ tests: steps: - name: Run test bash: | - echo -e 'line 1\nline 2\nline 3' > test-replace-fail.txt + printf 'line 1\nline 2\nline 3' > test-replace-fail.txt cycod --input "use ReplaceFileContent to replace test-replace-fail.txt with 'new', say it has 5 lines (which is wrong)" --auto-approve '*' expect-regex: | assistant-function: ReplaceFileContent