From 26b06fe0ff67332d4bfcc5b026fa2930e559b10b Mon Sep 17 00:00:00 2001 From: Dennis Date: Tue, 4 Aug 2026 18:18:08 +0800 Subject: [PATCH] fix(chat): replace exports atomically on Windows --- internal/shortcut/chat/file_replace.go | 13 +++++++ .../shortcut/chat/file_replace_windows.go | 13 +++++++ internal/shortcut/chat/resource_download.go | 2 +- .../chat/resource_download_coverage_test.go | 34 +++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 internal/shortcut/chat/file_replace.go create mode 100644 internal/shortcut/chat/file_replace_windows.go diff --git a/internal/shortcut/chat/file_replace.go b/internal/shortcut/chat/file_replace.go new file mode 100644 index 00000000..98560989 --- /dev/null +++ b/internal/shortcut/chat/file_replace.go @@ -0,0 +1,13 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +//go:build !windows + +package chat + +import "os" + +func replaceFileAtomically(source, target string) error { + return os.Rename(source, target) +} diff --git a/internal/shortcut/chat/file_replace_windows.go b/internal/shortcut/chat/file_replace_windows.go new file mode 100644 index 00000000..3bdcabdd --- /dev/null +++ b/internal/shortcut/chat/file_replace_windows.go @@ -0,0 +1,13 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +//go:build windows + +package chat + +import "golang.org/x/sys/windows" + +func replaceFileAtomically(source, target string) error { + return windows.Rename(source, target) +} diff --git a/internal/shortcut/chat/resource_download.go b/internal/shortcut/chat/resource_download.go index 39cb8ec0..ee132b3a 100644 --- a/internal/shortcut/chat/resource_download.go +++ b/internal/shortcut/chat/resource_download.go @@ -45,7 +45,7 @@ var ( resourceCopy = io.Copy resourceTempSync = (*os.File).Sync resourceTempClose = (*os.File).Close - resourceRename = os.Rename + resourceRename = replaceFileAtomically resourceLink = os.Link resourceDownload = downloadResourceAtomically ) diff --git a/internal/shortcut/chat/resource_download_coverage_test.go b/internal/shortcut/chat/resource_download_coverage_test.go index 332fbbc4..05cded28 100644 --- a/internal/shortcut/chat/resource_download_coverage_test.go +++ b/internal/shortcut/chat/resource_download_coverage_test.go @@ -783,3 +783,37 @@ func TestCrossPlatformCoverageMessageExportFailureBoundaries(t *testing.T) { }) } } + +func TestCrossPlatformCoverageMessageExportOverwriteReplacesExistingFile(t *testing.T) { + resetResourceDownloadHooks(t) + base := t.TempDir() + resourceGetwd = func() (string, error) { return base, nil } + resourceAbs = filepath.Abs + resourceEvalSymlinks = filepath.EvalSymlinks + resourceLstat = os.Lstat + resourceRel = filepath.Rel + resourceCreateTemp = os.CreateTemp + resourceTempSync = (*os.File).Sync + resourceTempClose = (*os.File).Close + resourceRename = replaceFileAtomically + resourceLink = os.Link + + target := filepath.Join(base, "out.json") + if err := os.WriteFile(target, []byte("old\n"), 0o600); err != nil { + t.Fatal(err) + } + relative, size, err := WriteMessageExportJSON( + "out.json", true, map[string]any{"value": "new"}, + ) + if err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(target) + if err != nil { + t.Fatal(err) + } + const want = "{\n \"value\": \"new\"\n}\n" + if relative != "out.json" || size != len(want) || string(data) != want { + t.Fatalf("relative=%q size=%d data=%q", relative, size, data) + } +}