import Foundation import Testing import SstCore @testable import SstData @MainActor struct HistoryStoreTests { func tempFile() -> URL { FileManager.default.temporaryDirectory.appending(path: "sst-tests-\(UUID().uuidString)/history.json") } func entry(_ text: String, at seconds: TimeInterval) -> HistoryEntry { HistoryEntry(date: Date(timeIntervalSince1970: 1_700_000_000 + seconds), duration: 2, rawText: text, finalText: text) } @Test func newestEntryComesFirstAndSurvivesARoundTrip() { let url = tempFile() let store = HistoryStore(fileURL: url) store.add(entry("eerste", at: 0)) store.add(HistoryEntry( date: Date(timeIntervalSince1970: 1_700_000_100), duration: 3, rawText: "cloud code", finalText: "Claude Code", corrections: [AppliedCorrection(heard: "cloud code", wrote: "Claude Code", rule: "cloud code", start: 0, length: 11)])) store.flush() let reloaded = HistoryStore(fileURL: url) #expect(reloaded.load() == .loaded) #expect(reloaded.entries.map(\.finalText) == ["Claude Code", "eerste"]) #expect(reloaded.entries == store.entries) } @Test func missingFileIsEmpty() { let store = HistoryStore(fileURL: tempFile()) #expect(store.load() == .empty) #expect(store.entries.isEmpty) } @Test func removeAndRemoveAll() { let store = HistoryStore(fileURL: tempFile()) let a = entry("a", at: 0), b = entry("b", at: 1) store.add(a); store.add(b) store.remove(id: a.id) #expect(store.entries.map(\.finalText) == ["b"]) store.removeAll() #expect(store.entries.isEmpty) } @Test func corruptFileIsMovedAsideAndStoreStartsFresh() throws { let url = tempFile() try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) try "dit is geen json".write(to: url, atomically: true, encoding: .utf8) let store = HistoryStore(fileURL: url) guard case .recoveredCorrupt(let backup) = store.load() else { Issue.record("expected recoveredCorrupt"); return } #expect(FileManager.default.fileExists(atPath: backup.path)) #expect(!FileManager.default.fileExists(atPath: url.path)) #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 store.onChange = { calls += 1 } store.add(entry("a", at: 0)) store.removeAll() #expect(calls == 2) } @Test func entryFromDictationResultKeepsEverything() { let result = DictationResult( date: Date(timeIntervalSince1970: 1_700_000_000), duration: 4.5, rawText: "raw", finalText: "final", corrections: [AppliedCorrection(heard: "a", wrote: "b", rule: "a", start: 0, length: 1)]) let e = HistoryEntry(result: result) #expect(e.rawText == "raw" && e.finalText == "final" && e.duration == 4.5 && e.corrections.count == 1) } } struct HistorySearchTests { let entries = [ HistoryEntry(date: .now, duration: 1, rawText: "ik ga naar het café", finalText: "Ik ga naar het Café"), HistoryEntry(date: .now, duration: 1, rawText: "cloud code is handig", finalText: "Claude Code is handig"), ] @Test func emptyQueryReturnsEverything() { #expect(HistorySearch.filter(entries, query: " ").count == 2) } @Test func ignoresCaseAndDiacritics() { #expect(HistorySearch.filter(entries, query: "CAFE").count == 1) #expect(HistorySearch.filter(entries, query: "café").count == 1) } @Test func allWordsMustMatchInAnyOrder() { #expect(HistorySearch.filter(entries, query: "handig claude").count == 1) #expect(HistorySearch.filter(entries, query: "handig café").isEmpty) } @Test func searchesTheOriginalTextToo() { #expect(HistorySearch.filter(entries, query: "cloud").count == 1) } } struct HistoryGroupingTests { @Test func groupsByDayKeepingOrder() { var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(identifier: "UTC")! func at(_ day: Int, _ hour: Int) -> Date { calendar.date(from: DateComponents(year: 2026, month: 10, day: day, hour: hour))! } let entries = [at(2, 15), at(2, 9), at(1, 20)].map { HistoryEntry(date: $0, duration: 1, rawText: "x", finalText: "x") } let days = HistoryGrouping.byDay(entries, calendar: calendar) #expect(days.count == 2) #expect(days[0].entries.count == 2) #expect(days[1].entries.count == 1) #expect(days[0].day > days[1].day) } } @MainActor struct HistoryEditTests { @Test func updateTextReplacesFinalTextAndDropsCorrections() { let store = HistoryStore(fileURL: FileManager.default.temporaryDirectory.appending(path: "sst-tests-\(UUID().uuidString)/history.json")) let item = HistoryEntry( date: Date(), duration: 2, rawText: "cloud code", finalText: "Claude Code", corrections: [AppliedCorrection(heard: "cloud code", wrote: "Claude Code", rule: "cloud code", start: 0, length: 11)]) store.add(item) store.updateText(id: item.id, to: "Claude Code is fijn") #expect(store.entries[0].finalText == "Claude Code is fijn") #expect(store.entries[0].rawText == "cloud code") #expect(store.entries[0].corrections.isEmpty) } @Test func updateTextOfUnknownIdIsHarmless() { let store = HistoryStore(fileURL: FileManager.default.temporaryDirectory.appending(path: "sst-tests-\(UUID().uuidString)/history.json")) store.updateText(id: UUID(), to: "x") #expect(store.entries.isEmpty) } }