TestExecuteWriteFileRefusesMissingContent: valid-JSON args with no string "content" ({"path": ...} alone, or "content": null) must not decode to "" and silently truncate an existing file to 0 bytes behind a success-shaped result. An explicit "content": "" still writes (intentional empty file).
(t *testing.T)
| 106 | // and silently truncate an existing file to 0 bytes behind a success-shaped |
| 107 | // result. An explicit "content": "" still writes (intentional empty file). |
| 108 | func TestExecuteWriteFileRefusesMissingContent(t *testing.T) { |
| 109 | dir := t.TempDir() |
| 110 | path := filepath.Join(dir, "keep.go") |
| 111 | if err := os.WriteFile(path, []byte("package main\n"), 0o644); err != nil { |
| 112 | t.Fatal(err) |
| 113 | } |
| 114 | for name, args := range map[string]map[string]any{ |
| 115 | "content key absent": {"path": path}, |
| 116 | "content null": {"path": path, "content": nil}, |
| 117 | } { |
| 118 | msg := Execute(context.Background(), chmctx.ToolCall{Name: "write_file", Arguments: args}) |
| 119 | if !strings.HasPrefix(msg.Content, "(missing content argument") { |
| 120 | t.Fatalf("%s: want refusal, got %q", name, msg.Content) |
| 121 | } |
| 122 | got, _ := os.ReadFile(path) |
| 123 | if string(got) != "package main\n" { |
| 124 | t.Fatalf("%s: existing file was destroyed: %q", name, got) |
| 125 | } |
| 126 | } |
| 127 | // Explicit empty string is a deliberate empty write and must still pass. |
| 128 | msg := Execute(context.Background(), chmctx.ToolCall{ |
| 129 | Name: "write_file", Arguments: map[string]any{"path": path, "content": ""}, |
| 130 | }) |
| 131 | if !strings.Contains(msg.Content, "wrote 0 bytes") { |
| 132 | t.Fatalf("explicit empty content must write, got %q", msg.Content) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | // TestExecuteEditFileRefusesMissingNewString: same guard as write_file's |
| 137 | // content - a dropped new_string must not decode to "" and silently delete |