import Foundation import SwiftData import Testing @testable import Notes @MainActor struct NoteMapperTests { /// Returns the container too — `mainContext` does not keep its container /// alive, and letting it deallocate mid-test crashes the process. private func makeStore() throws -> (container: ModelContainer, context: ModelContext) { let config = ModelConfiguration(isStoredInMemoryOnly: true, cloudKitDatabase: .none) let container = try ModelContainer(for: NoteEntity.self, configurations: config) return (container, container.mainContext) } private func makeNote(text: String = "Barista's name is Sam") -> Note { var context = CaptureContext.empty context.latitude = 37.8044 context.longitude = -122.2712 context.placeName = "Blue Bottle Coffee" context.timeZoneID = "America/Los_Angeles" return Note.create(text: text, tags: ["coffee"], context: context) } @Test func upsertInsertsThenUpdates() throws { let (container, context) = try makeStore() defer { withExtendedLifetime(container) {} } let note = makeNote() NoteMapper.upsert(note, relativePath: note.relativePath, into: context) let inserted = NoteMapper.fetch(id: note.meta.id.stringValue, in: context) #expect(inserted?.text == "Barista's name is Sam") #expect(inserted?.placeName == "Blue Bottle Coffee") var edited = note edited.payload.text = "Barista's name is Sam — likes cortados" edited.payload.tags = ["coffee", "people"] NoteMapper.upsert(edited, relativePath: note.relativePath, into: context) let all = try context.fetch(FetchDescriptor()) #expect(all.count == 1) #expect(all.first?.text == "Barista's name is Sam — likes cortados") #expect(all.first?.tags == ["coffee", "people"]) } @Test func upsertWritesProjectAndNoteRestoresIt() throws { let (container, context) = try makeStore() defer { withExtendedLifetime(container) {} } var note = makeNote() note.payload.project = "Kitchen Remodel" NoteMapper.upsert(note, relativePath: note.relativePath, into: context) let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context)) #expect(entity.project == "Kitchen Remodel") let restored = try #require(NoteMapper.note(from: entity)) #expect(restored.payload.project == "Kitchen Remodel") #expect(restored.payload == note.payload) } @Test func entityRoundTripsBackToNote() throws { let (container, context) = try makeStore() defer { withExtendedLifetime(container) {} } let note = makeNote() NoteMapper.upsert(note, relativePath: note.relativePath, into: context) let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context)) let restored = try #require(NoteMapper.note(from: entity)) #expect(restored.payload == note.payload) #expect(restored.meta.id == note.meta.id) } // MARK: - Audio columns @Test func pendingAudioColumnsRoundTrip() throws { let (container, context) = try makeStore() defer { withExtendedLifetime(container) {} } var ctx = CaptureContext.empty ctx.device = "Watch" let audio = Note.AudioInfo( duration: 30, transcriberDeviceID: "install-42", transcriptionStatus: .pending, transcriptionLocaleID: nil, transcribedAt: nil ) let note = Note.create(text: "", source: .transcribed, context: ctx, audio: audio) NoteMapper.upsert(note, relativePath: note.relativePath, into: context) let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context)) #expect(entity.hasAudio) #expect(entity.audioDuration == 30) #expect(entity.transcriberDeviceID == "install-42") #expect(entity.transcriptionStatusRaw == "pending") #expect(entity.source == .transcribed) // A pending audio note carries no `transcribedAt`, so it reconstructs // losslessly (that field is the only one the cache intentionally drops). let restored = try #require(NoteMapper.note(from: entity)) #expect(restored.payload == note.payload) #expect(restored.payload.audio == audio) } @Test func doneTranscriptionCachesColumnsButNotTranscribedAt() throws { let (container, context) = try makeStore() defer { withExtendedLifetime(container) {} } let audio = Note.AudioInfo( duration: 5, transcriberDeviceID: "install-7", transcriptionStatus: .done, transcriptionLocaleID: "en_US", transcribedAt: Date() ) let note = Note.create(text: "Hello there", source: .transcribed, context: .empty, audio: audio) NoteMapper.upsert(note, relativePath: note.relativePath, into: context) let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context)) #expect(entity.transcriptionStatusRaw == "done") #expect(entity.transcriptionLocaleID == "en_US") // The file is authoritative for the timestamp — the cache doesn't store it. #expect(entity.audio?.transcribedAt == nil) #expect(entity.audio?.transcriptionStatus == .done) } @Test func textNoteHasNoAudioColumns() throws { let (container, context) = try makeStore() defer { withExtendedLifetime(container) {} } let note = makeNote() NoteMapper.upsert(note, relativePath: note.relativePath, into: context) let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context)) #expect(!entity.hasAudio) #expect(entity.audio == nil) #expect(entity.audioDuration == nil) #expect(entity.transcriptionStatusRaw == nil) } }