fix: history load notifies and never overwrites unreadable file; dictionary keeps ids across external edits, undo safety

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 16:04:01 +02:00
co-authored by Claude Sonnet 5.5
parent 5542f9cf70
commit 31c34a1d4a
8 changed files with 150 additions and 6 deletions
@@ -49,4 +49,23 @@ struct DictionaryFileTests {
#expect(file.entries.isEmpty)
#expect(file.serialized() == "# c\n")
}
@Test func preservingIDsKeepsIdsOfUnchangedLines() {
let old = DictionaryFile(parsing: "a\nb => c\nd\n")
let new = DictionaryFile(parsing: "z\na\nb => c\nd2\n").preservingIDs(from: old)
let oldIDs = old.entries.map(\.id), newIDs = new.entries.map(\.id)
#expect(newIDs[1] == oldIDs[0])
#expect(newIDs[2] == oldIDs[1])
#expect(!oldIDs.contains(newIDs[0]))
#expect(!oldIDs.contains(newIDs[3]))
#expect(new.serialized() == "z\na\nb => c\nd2\n")
}
@Test func preservingIDsMapsDuplicatesOneToOneInOrder() {
let old = DictionaryFile(parsing: "a\na\n")
let new = DictionaryFile(parsing: "a\na\na\n").preservingIDs(from: old)
let oldIDs = old.entries.map(\.id), newIDs = new.entries.map(\.id)
#expect(Array(newIDs[0...1]) == oldIDs)
#expect(!oldIDs.contains(newIDs[2]))
}
}
@@ -43,6 +43,23 @@ struct DictionaryStoreTests {
#expect(changes == 1)
}
@Test func externalChangeFiresBothCallbacksAndKeepsIDs() throws {
let url = tempFile()
let store = DictionaryStore(fileURL: url)
try store.load()
try store.apply { $0.append(.term("Vercel")) }
let id = store.entries[0].id
var changes = 0, external = 0
store.onChange = { changes += 1 }
store.onExternalChange = { external += 1 }
try "Nieuw\nVercel\n".write(to: url, atomically: true, encoding: .utf8)
store.reloadIfChanged()
#expect(changes == 1 && external == 1)
#expect(store.entries[1].id == id)
try store.apply { $0.append(.term("Eigen")) }
#expect(changes == 2 && external == 1)
}
@Test func ourOwnWriteDoesNotTriggerAReload() throws {
let store = DictionaryStore(fileURL: tempFile())
try store.load()
+29
View File
@@ -57,6 +57,35 @@ struct HistoryStoreTests {
#expect(store.entries.isEmpty)
}
@Test func loadNotifiesOnChange() {
let url = tempFile()
let writer = HistoryStore(fileURL: url)
writer.add(entry("a", at: 0)); writer.add(entry("b", at: 1))
writer.flush()
let store = HistoryStore(fileURL: url)
var calls = 0
store.onChange = { calls += 1 }
#expect(store.load() == .loaded)
#expect(calls == 1)
#expect(store.entries.count == 2)
}
@Test func unreadableFileIsReportedAndNeverOverwritten() throws {
let url = tempFile()
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) // history.json is a directory
let store = HistoryStore(fileURL: url)
guard case .unreadable = store.load() else {
Issue.record("expected unreadable"); return
}
store.add(entry("a", at: 0))
store.flush()
var isDirectory: ObjCBool = false
#expect(FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory))
#expect(isDirectory.boolValue)
#expect(store.lastError != nil)
}
@Test func onChangeFiresForEdits() {
let store = HistoryStore(fileURL: tempFile())
var calls = 0
+39
View File
@@ -67,6 +67,20 @@ struct HistoryViewModelTests {
return store
}
@Test func daysAreFilledAfterTheStoreLoads() {
let url = FileManager.default.temporaryDirectory.appending(path: "sst-vm-\(UUID().uuidString)/h.json")
let writer = HistoryStore(fileURL: url)
writer.add(HistoryEntry(date: .now, duration: 1, rawText: "x", finalText: "x"))
writer.flush()
let store = HistoryStore(fileURL: url)
let model = HistoryViewModel(store: store)
store.onChange = { model.refresh() }
#expect(model.days.isEmpty)
store.load()
#expect(!model.days.isEmpty)
#expect(model.totalCount == 1)
}
@Test func listsAllEntriesNewestFirstAndFiltersByQuery() {
let model = HistoryViewModel(store: store(["eerste", "tweede zin", "derde"]))
#expect(model.totalCount == 3)
@@ -152,6 +166,31 @@ struct DictionaryViewModelTests {
#expect(store.entries == [term("Supabase")])
}
@Test func externalChangeClearsUndo() throws {
let (model, store) = try makeModel("Supabase\n")
model.add(.term("Vercel"))
#expect(model.canUndo)
model.externalChangeDetected()
#expect(!model.canUndo)
model.undo()
#expect(store.entries == [term("Supabase"), term("Vercel")])
}
@Test func failedUndoKeepsTheSnapshot() throws {
let (model, store) = try makeModel("Supabase\n")
model.add(.term("Vercel"))
let folder = store.fileURL.deletingLastPathComponent()
try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: folder.path)
model.undo()
try FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: folder.path)
#expect(model.errorMessage != nil)
#expect(model.canUndo)
model.errorMessage = nil
model.undo()
#expect(store.entries == [term("Supabase")])
#expect(!model.canUndo)
}
@Test func searchMatchesBothSidesIgnoringCaseAndDiacritics() throws {
let (model, _) = try makeModel("cloud code => Claude Code\nCafé\nVercel\n")
model.query = "CLAUDE"