Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/unreleased/1410.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 1410
affected:
- src/CodeIndex/Cli/SuggestionStore.cs
- tests/CodeIndex.Tests/SuggestionStoreTests.cs
---

## English

- **Suggestion storage now flushes temp files before replacement (#1410)** — local suggestion writes serialize to a temp file, flush it to disk before renaming, and preserve zero-byte stores as `.bak` files instead of silently treating them as empty.

## 日本語

- **提案ストアが置換前に一時ファイルをディスクへ flush するようになりました (#1410)** — ローカル提案の書き込みは一時ファイルへ serialize して rename 前にディスクへ flush し、zero-byte のストアは空として黙って扱わず `.bak` として退避します。
21 changes: 19 additions & 2 deletions src/CodeIndex/Cli/SuggestionStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@ private List<SuggestionRecord> ReadUnlocked()
if (!File.Exists(ioPath))
return new List<SuggestionRecord>();

if (new FileInfo(ioPath).Length == 0)
{
PreserveCorruptFile();
return new List<SuggestionRecord>();
}

var json = File.ReadAllText(ioPath);
if (string.IsNullOrWhiteSpace(json))
return new List<SuggestionRecord>();
Expand Down Expand Up @@ -547,7 +553,10 @@ private List<SuggestionRecord> ReadFilteredUnlocked(

var snapshot = File.ReadAllBytes(ioPath);
if (snapshot.Length == 0)
{
PreserveCorruptFile();
return new List<SuggestionRecord>();
}

if (IsEmptyOrJsonWhitespace(snapshot))
return new List<SuggestionRecord>();
Expand Down Expand Up @@ -650,10 +659,18 @@ private void SaveUnlocked(List<SuggestionRecord> records)
NormalizeRecordDefaults(records);

var tempPath = _filePath + ".tmp";
var json = JsonSerializer.Serialize(records, s_jsonOptions);
try
{
File.WriteAllText(tempPath, json);
using (var stream = new FileStream(
tempPath,
FileMode.Create,
FileAccess.Write,
FileShare.None))
{
JsonSerializer.Serialize(stream, records, s_jsonOptions);
stream.Flush(flushToDisk: true);
}

File.Move(tempPath, _filePath, overwrite: true);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
Expand Down
28 changes: 28 additions & 0 deletions tests/CodeIndex.Tests/SuggestionStoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,34 @@ public void CorruptFile_IsPreservedAsBackup()
Assert.False(File.Exists(filePath), "Original corrupt file should be removed");
}

[Fact]
public void ZeroByteFile_IsPreservedAsBackup()
{
var filePath = Path.Combine(_tempDir, "suggestions-codeindex.json");
var backupPath = filePath + ".bak";
File.WriteAllBytes(filePath, Array.Empty<byte>());

var all = _store.LoadAll();

Assert.Empty(all);
Assert.True(File.Exists(backupPath), "Zero-byte file should be preserved as .bak");
Assert.False(File.Exists(filePath), "Original zero-byte file should be removed");
}

[Fact]
public void FilteredZeroByteFile_IsPreservedAsBackup()
{
var filePath = Path.Combine(_tempDir, "suggestions-codeindex.json");
var backupPath = filePath + ".bak";
File.WriteAllBytes(filePath, Array.Empty<byte>());

var all = _store.LoadByCategory("other");

Assert.Empty(all);
Assert.True(File.Exists(backupPath), "Zero-byte file should be preserved as .bak");
Assert.False(File.Exists(filePath), "Original zero-byte file should be removed");
}

[Fact]
public void AtomicWrite_SurvivesAddAfterCorruption()
{
Expand Down
Loading