fix(tool): clarify write_file nested-JSON escape semantics and add tests
This commit is contained in:
parent
84e42d6904
commit
41904018e2
5 changed files with 66 additions and 7 deletions
|
|
@ -262,6 +262,22 @@ func TestDecodeToolCallArguments_StringJSON(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecodeToolCallArguments_StringJSON_NewlineEscape(t *testing.T) {
|
||||||
|
raw := json.RawMessage(`"{\"content\":\"line1\\nline2\"}"`)
|
||||||
|
args := DecodeToolCallArguments(raw, "write_file")
|
||||||
|
if args["content"] != "line1\nline2" {
|
||||||
|
t.Errorf("content = %q, want newline-expanded string", args["content"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeToolCallArguments_StringJSON_LiteralBackslashN(t *testing.T) {
|
||||||
|
raw := json.RawMessage(`"{\"content\":\"line1\\\\nline2\"}"`)
|
||||||
|
args := DecodeToolCallArguments(raw, "write_file")
|
||||||
|
if args["content"] != `line1\nline2` {
|
||||||
|
t.Errorf("content = %q, want literal backslash-n", args["content"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDecodeToolCallArguments_EmptyInput(t *testing.T) {
|
func TestDecodeToolCallArguments_EmptyInput(t *testing.T) {
|
||||||
args := DecodeToolCallArguments(nil, "test")
|
args := DecodeToolCallArguments(nil, "test")
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ func buildCLIToolsPrompt(tools []ToolDefinition) string {
|
||||||
)
|
)
|
||||||
sb.WriteString("\n```\n\n")
|
sb.WriteString("\n```\n\n")
|
||||||
sb.WriteString("CRITICAL: The 'arguments' field MUST be a JSON-encoded STRING.\n\n")
|
sb.WriteString("CRITICAL: The 'arguments' field MUST be a JSON-encoded STRING.\n\n")
|
||||||
|
sb.WriteString("Escaping rules for string arguments:\n")
|
||||||
|
sb.WriteString("- `\\n` means a real newline.\n")
|
||||||
|
sb.WriteString("- To pass a literal backslash+n (`\\n`), encode it as `\\\\n` inside arguments JSON.\n")
|
||||||
|
sb.WriteString("- Because `arguments` is itself a JSON string, this often appears as `\\\\\\\\n` in the outer payload.\n\n")
|
||||||
sb.WriteString("### Tool Definitions:\n\n")
|
sb.WriteString("### Tool Definitions:\n\n")
|
||||||
|
|
||||||
for _, tool := range tools {
|
for _, tool := range tools {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func (t *EditFileTool) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *EditFileTool) Description() string {
|
func (t *EditFileTool) Description() string {
|
||||||
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file."
|
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file. JSON escapes apply (for example, \\n is newline and \\\\n is literal backslash-n)."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *EditFileTool) Parameters() map[string]any {
|
func (t *EditFileTool) Parameters() map[string]any {
|
||||||
|
|
@ -42,11 +42,11 @@ func (t *EditFileTool) Parameters() map[string]any {
|
||||||
},
|
},
|
||||||
"old_text": map[string]any{
|
"old_text": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The exact text to find and replace",
|
"description": "The exact text to find and replace. JSON escapes apply: \\n is newline, \\\\n is literal backslash-n.",
|
||||||
},
|
},
|
||||||
"new_text": map[string]any{
|
"new_text": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The text to replace with",
|
"description": "The text to replace with. JSON escapes apply: \\n is newline, \\\\n is literal backslash-n.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"path", "old_text", "new_text"},
|
"required": []string{"path", "old_text", "new_text"},
|
||||||
|
|
@ -92,7 +92,7 @@ func (t *AppendFileTool) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AppendFileTool) Description() string {
|
func (t *AppendFileTool) Description() string {
|
||||||
return "Append content to the end of a file"
|
return "Append content to the end of a file. JSON escapes apply (for example, \\n is newline and \\\\n is literal backslash-n)."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AppendFileTool) Parameters() map[string]any {
|
func (t *AppendFileTool) Parameters() map[string]any {
|
||||||
|
|
@ -105,7 +105,7 @@ func (t *AppendFileTool) Parameters() map[string]any {
|
||||||
},
|
},
|
||||||
"content": map[string]any{
|
"content": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The content to append",
|
"description": "The content to append. JSON escapes apply: \\n is newline, \\\\n is literal backslash-n.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"path", "content"},
|
"required": []string{"path", "content"},
|
||||||
|
|
|
||||||
|
|
@ -870,7 +870,7 @@ func (t *WriteFileTool) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WriteFileTool) Description() string {
|
func (t *WriteFileTool) Description() string {
|
||||||
return "Write content to a file. If the file already exists, you must set overwrite=true to replace it."
|
return "Write content to a file. Content is written byte-for-byte after JSON argument decoding (for example, \\n becomes a newline, while \\\\n writes a literal backslash-n). If the file already exists, you must set overwrite=true to replace it."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WriteFileTool) Parameters() map[string]any {
|
func (t *WriteFileTool) Parameters() map[string]any {
|
||||||
|
|
@ -883,7 +883,7 @@ func (t *WriteFileTool) Parameters() map[string]any {
|
||||||
},
|
},
|
||||||
"content": map[string]any{
|
"content": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Content to write to the file",
|
"description": "Content to write to the file. JSON escapes apply: \\n is newline, \\\\n is literal backslash-n.",
|
||||||
},
|
},
|
||||||
"overwrite": map[string]any{
|
"overwrite": map[string]any{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,45 @@ func TestFilesystemTool_WriteFile_Success(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestFilesystemTool_WriteFile_LiteralBackslashN verifies write_file keeps
|
||||||
|
// literal backslash sequences unchanged when they are passed as plain text.
|
||||||
|
func TestFilesystemTool_WriteFile_LiteralBackslashN(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testFile := filepath.Join(tmpDir, "literal.txt")
|
||||||
|
|
||||||
|
tool := NewWriteFileTool("", false)
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
|
"path": testFile,
|
||||||
|
"content": `aaa\naaa`,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.False(t, result.IsError, "expected success, got: %s", result.ForLLM)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(testFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `aaa\naaa`, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilesystemTool_WriteFile_PreservesCRLF verifies write_file does not
|
||||||
|
// normalize line endings and writes CRLF bytes as provided.
|
||||||
|
func TestFilesystemTool_WriteFile_PreservesCRLF(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testFile := filepath.Join(tmpDir, "crlf.txt")
|
||||||
|
content := "line1\r\nline2\r\n"
|
||||||
|
|
||||||
|
tool := NewWriteFileTool("", false)
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
|
"path": testFile,
|
||||||
|
"content": content,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.False(t, result.IsError, "expected success, got: %s", result.ForLLM)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(testFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte(content), data)
|
||||||
|
}
|
||||||
|
|
||||||
// TestFilesystemTool_WriteFile_CreateDir verifies directory creation
|
// TestFilesystemTool_WriteFile_CreateDir verifies directory creation
|
||||||
func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
|
func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue