Notes files now export to notes/ + notes.json alongside conversations.json, matching folders.json's manifest pattern: matched by conversation ID, never overwrites notes a machine already has locally, same empty-state orphan- cleanup safety guard as the existing conversation/folder sync code. Also adds Cmd+D to the "Discard" button on the crash-recovery restore prompt. Fixes two Swift 6 actor-isolation build warnings surfaced along the way: ConversationNotesService and the String filename-sanitizing extension are pure, state-free helpers called from nonisolated contexts (DatabaseService, GitSyncService) but defaulted to @MainActor — marked nonisolated.
89 lines
3.9 KiB
Swift
89 lines
3.9 KiB
Swift
//
|
|
// ConversationNotesServiceTests.swift
|
|
// oAITests
|
|
//
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
// Copyright (C) 2026 Rune Olsen
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import Confab
|
|
|
|
@Suite("ConversationNotesService")
|
|
struct ConversationNotesServiceTests {
|
|
|
|
@Test("makeFilename sanitizes the conversation name and appends a short ID suffix")
|
|
func makeFilenameSanitizesAndSuffixes() {
|
|
let id = UUID(uuidString: "A3F2B1C0-0000-0000-0000-000000000000")!
|
|
let filename = ConversationNotesService.shared.makeFilename(conversationName: "My Recipe Chat", conversationId: id)
|
|
#expect(filename == "My Recipe Chat-a3f2.md")
|
|
}
|
|
|
|
@Test("makeFilename sanitizes invalid filename characters in the conversation name")
|
|
func makeFilenameSanitizesInvalidChars() {
|
|
let id = UUID(uuidString: "A3F2B1C0-0000-0000-0000-000000000000")!
|
|
let filename = ConversationNotesService.shared.makeFilename(conversationName: "Q&A: What/Why?", conversationId: id)
|
|
#expect(filename == "Q&A- What-Why--a3f2.md")
|
|
}
|
|
|
|
@Test("makeFilename falls back to Untitled for an empty conversation name")
|
|
func makeFilenameFallsBackForEmptyName() {
|
|
let id = UUID(uuidString: "A3F2B1C0-0000-0000-0000-000000000000")!
|
|
let filename = ConversationNotesService.shared.makeFilename(conversationName: "", conversationId: id)
|
|
#expect(filename == "Untitled-a3f2.md")
|
|
}
|
|
|
|
@Test("write then readBody round-trips the body with the ID header stripped")
|
|
func writeReadRoundTrip() {
|
|
let id = UUID()
|
|
let filename = "test-notes-\(UUID().uuidString).md"
|
|
defer { ConversationNotesService.shared.delete(filename: filename) }
|
|
|
|
ConversationNotesService.shared.write(body: "User prefers dark roast coffee.", filename: filename, conversationId: id)
|
|
let body = ConversationNotesService.shared.readBody(filename: filename)
|
|
|
|
#expect(body == "User prefers dark roast coffee.")
|
|
}
|
|
|
|
@Test("readBody on a nonexistent file returns nil, not an error")
|
|
func readBodyMissingFileReturnsNil() {
|
|
let body = ConversationNotesService.shared.readBody(filename: "definitely-does-not-exist-\(UUID().uuidString).md")
|
|
#expect(body == nil)
|
|
}
|
|
|
|
@Test("readRaw returns the exact on-disk content, ID header included")
|
|
func readRawIncludesHeader() {
|
|
let id = UUID()
|
|
let filename = "test-notes-\(UUID().uuidString).md"
|
|
defer { ConversationNotesService.shared.delete(filename: filename) }
|
|
|
|
ConversationNotesService.shared.write(body: "User prefers dark roast coffee.", filename: filename, conversationId: id)
|
|
let raw = ConversationNotesService.shared.readRaw(filename: filename)
|
|
|
|
#expect(raw == "**ID**: `\(id.uuidString)`\n\nUser prefers dark roast coffee.")
|
|
}
|
|
|
|
@Test("writeRaw then readRaw round-trips content byte-for-byte, with no header wrapping added")
|
|
func writeRawReadRawRoundTrip() {
|
|
let filename = "test-notes-\(UUID().uuidString).md"
|
|
defer { ConversationNotesService.shared.delete(filename: filename) }
|
|
let content = "**ID**: `12345678-1234-1234-1234-123456789012`\n\nImported from another machine."
|
|
|
|
ConversationNotesService.shared.writeRaw(content: content, filename: filename)
|
|
|
|
#expect(ConversationNotesService.shared.readRaw(filename: filename) == content)
|
|
}
|
|
|
|
@Test("stripIDHeader removes the embedded ID line and following blank line")
|
|
func stripIDHeaderRemovesHeader() {
|
|
let content = "**ID**: `12345678-1234-1234-1234-123456789012`\n\nActual notes content."
|
|
#expect(ConversationNotesService.stripIDHeader(from: content) == "Actual notes content.")
|
|
}
|
|
|
|
@Test("stripIDHeader leaves content without the header untouched")
|
|
func stripIDHeaderLeavesUnheaderedContentUntouched() {
|
|
let content = "Just some content, no header."
|
|
#expect(ConversationNotesService.stripIDHeader(from: content) == content)
|
|
}
|
|
}
|