waveterm/pkg/util/fileutil/fileutil_test.go
Copilot 4956c92c55
Make Wave home config writes atomic and serialized to avoid watcher partial reads (#2945)
`WriteWaveHomeConfigFile()` previously used direct `os.WriteFile`, which
can expose truncation/partial-write states to the JSON file watcher.
This change switches config persistence to temp-file + rename semantics
and serializes writes through a single process-wide lock for config file
writes.

- **Atomic file write helper**
  - Added `AtomicWriteFile()` in `pkg/util/fileutil/fileutil.go`.
- Writes to `<filename>.tmp` in the same directory, then renames to the
target path.
  - Performs temp-file cleanup on error paths.
- Introduced a shared suffix constant (`TempFileSuffix`) used by
implementation/tests.

- **Config write path update**
- Updated `WriteWaveHomeConfigFile()` in `pkg/wconfig/settingsconfig.go`
to:
- Use a package-level mutex (`configWriteLock`) so only one config write
runs at a time (across all config files).
- Call `fileutil.AtomicWriteFile(...)` instead of direct
`os.WriteFile(...)`.

- **Focused coverage for atomic behavior**
  - Added `pkg/util/fileutil/fileutil_test.go` with tests for:
- Successful atomic write (target file contains expected payload and no
leftover `.tmp` file).
    - Rename-failure path cleanup (temp file is removed).

```go
func WriteWaveHomeConfigFile(fileName string, m waveobj.MetaMapType) error {
    configWriteLock.Lock()
    defer configWriteLock.Unlock()

    fullFileName := filepath.Join(wavebase.GetWaveConfigDir(), fileName)
    barr, err := jsonMarshalConfigInOrder(m)
    if err != nil {
        return err
    }
    return fileutil.AtomicWriteFile(fullFileName, barr, 0644)
}
```

<!-- START COPILOT CODING AGENT TIPS -->
---

🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
2026-02-27 15:43:37 -08:00

45 lines
1.2 KiB
Go

package fileutil
import (
"os"
"path/filepath"
"testing"
)
func TestAtomicWriteFile(t *testing.T) {
tmpDir := t.TempDir()
fileName := filepath.Join(tmpDir, "settings.json")
err := AtomicWriteFile(fileName, []byte(`{"key":"value"}`), 0644)
if err != nil {
t.Fatalf("AtomicWriteFile failed: %v", err)
}
data, err := os.ReadFile(fileName)
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
if string(data) != `{"key":"value"}` {
t.Fatalf("unexpected file contents: %q", string(data))
}
if _, err := os.Stat(fileName + TempFileSuffix); !os.IsNotExist(err) {
t.Fatalf("temporary file should not exist, stat err: %v", err)
}
}
func TestAtomicWriteFileRenameErrorCleansTempFile(t *testing.T) {
tmpDir := t.TempDir()
fileName := filepath.Join(tmpDir, "settings.json")
if err := os.Mkdir(fileName, 0755); err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
err := AtomicWriteFile(fileName, []byte(`{"key":"value"}`), 0644)
if err == nil {
t.Fatalf("AtomicWriteFile expected error")
}
if _, statErr := os.Stat(fileName + TempFileSuffix); !os.IsNotExist(statErr) {
t.Fatalf("temporary file should be removed on rename error, stat err: %v", statErr)
}
}