import XCTest @testable import Voice @MainActor final class ChatTests: XCTestCase { func testOfflineCRUDSearchPinsArchiveMoveAndPreferences() async throws { let api = OfflineChatAPI() let store = ChatStore(api: api) await store.load() XCTAssertTrue(store.conversations.isEmpty) await store.saveWorkspace(nil, name: "Writing", instructions: "Be concise") let space = try XCTUnwrap(store.workspaces.first) await store.create(workspace: space.id) let original = try XCTUnwrap(store.selected) store.draft = "A durable offline message" await store.send() XCTAssertEqual(store.messages.count, 2) XCTAssertEqual(store.draft, "") await store.patch(original, changes: ["title": "Weekend", "pinned": true]) XCTAssertEqual(store.selected?.title, "Weekend") XCTAssertTrue(store.selected?.pinned == true) await store.load(query: "durable") XCTAssertEqual(store.conversations.count, 1) await store.savePreferences(palette: "ocean", appearance: "dark") XCTAssertEqual(store.preferences.palette, "ocean") await store.deleteWorkspace(space) XCTAssertNil(store.conversations.first?.workspaceId) let renamed = try XCTUnwrap(store.selected) await store.patch(renamed, changes: ["archived": true]) await store.load() XCTAssertTrue(store.conversations.isEmpty) await store.load(archived: true) XCTAssertEqual(store.conversations.count, 1) await store.delete(try XCTUnwrap(store.conversations.first)) XCTAssertNil(store.selected) XCTAssertTrue(store.messages.isEmpty) } func testOfflineResearchTruthfulExportAndTeardown() async throws { let store = ChatStore(api: OfflineChatAPI()) await store.create() store.draft = "Export this"; await store.send() await store.loadResearch() XCTAssertFalse(store.researchAvailable) await store.exportConversation(format: "markdown") XCTAssertTrue(store.exportText?.contains("Export this") == true) store.foreground(true) store.foreground(false) store.close() XCTAssertNil(store.exportText) XCTAssertFalse(store.research) } func testBranchAndRegeneratePreserveOriginal() async throws { let store = ChatStore(api: OfflineChatAPI()) await store.create() let original = try XCTUnwrap(store.selected) store.draft = "Original question"; await store.send() let user = try XCTUnwrap(store.messages.first) await store.branch(user, edited: "Edited question") XCTAssertNotEqual(store.selected?.id, original.id) XCTAssertEqual(store.messages.first?.content, "Edited question") await store.open(original) XCTAssertEqual(store.messages.first?.content, "Original question") let answer = try XCTUnwrap(store.messages.last) await store.branch(answer, regenerate: true) XCTAssertNotEqual(store.selected?.id, original.id) XCTAssertEqual(store.messages.first?.content, "Original question") } func testReplayFullReplacementAndDuplicateCursor() async throws { let store = ChatStore(api: OfflineChatAPI()) await store.create() let id = try XCTUnwrap(store.selected?.id) func event(_ seq: Int, _ text: String) -> ChatEvent { ChatEvent(id: seq, name: "message", data: Data("{\"message\":{\"id\":\"m\",\"conversation_id\":\"\(id)\",\"role\":\"assistant\",\"content\":\"\(text)\",\"status\":\"streaming\",\"run_id\":\"r\",\"created_at\":1}}".utf8)) } try store.apply(event(1, "A"), run: "r") try store.apply(event(2, "Answer"), run: "r") try store.apply(event(1, "Duplicate"), run: "r") XCTAssertEqual(store.messages.count, 1) XCTAssertEqual(store.messages.first?.content, "Answer") store.close() XCTAssertTrue(store.messages.isEmpty) XCTAssertNil(store.selected) XCTAssertEqual(store.draft, "") } func testSSECommentsMultilineAndFraming() throws { var parser = SSEParser() XCTAssertNil(parser.consume(": keepalive")) XCTAssertNil(parser.consume("id: 12")) XCTAssertNil(parser.consume("event: message")) XCTAssertNil(parser.consume("data: {")) XCTAssertNil(parser.consume("data: }")) let result = try XCTUnwrap(parser.consume("")) XCTAssertEqual(result.id, 12) XCTAssertEqual(result.name, "message") XCTAssertEqual(String(data: result.data, encoding: .utf8), "{\n}") XCTAssertNil(parser.consume("")) } func testRevisionConflictRetainsDraft() async throws { let store = ChatStore(api: OfflineChatAPI()) await store.create(); let old = try XCTUnwrap(store.selected) await store.patch(old, changes: ["title": "First"]) store.draft = "Keep me" await store.patch(old, changes: ["title": "Stale"]) XCTAssertEqual(store.selected?.title, "First") XCTAssertEqual(store.draft, "Keep me") XCTAssertNotNil(store.error) } func testAdvancedOfflineUploadArtifactTaskMemoryAndPrivacy() async throws { let api = OfflineChatAPI() let features = ChatFeatureStore(api: api) await features.load() XCTAssertTrue(features.enabled.contains("attachments")) let uploaded = await features.upload(Data("Private notes".utf8), filename: "notes.txt", mime: "text/plain") let upload = try XCTUnwrap(uploaded) await features.preview(upload) XCTAssertEqual(features.previewText, "Private notes") await features.saveArtifact(nil, title: "Draft", kind: "markdown", content: "# Draft") let item = try XCTUnwrap(features.artifacts.first) await features.saveArtifact(item, title: "Revised", kind: "markdown", content: "# Revised") XCTAssertEqual(features.artifacts.first?.revision, 2) await features.saveTask(nil, name: "Reminder", schedule: "in 1 hour", prompt: "Check notes") let task = try XCTUnwrap(features.tasks.first) await features.taskAction(task, action: "pause") XCTAssertEqual(features.tasks.first?.enabled, false) let memory = try XCTUnwrap(features.memory.first) await features.saveMemory(memory, content: "New memory") XCTAssertEqual(features.memory.first?.content, "New memory") await features.saveMemory(memory, content: "Stale overwrite") XCTAssertNotNil(features.error) XCTAssertEqual(features.memory.first?.content, "New memory") features.close() XCTAssertTrue(features.attachments.isEmpty) XCTAssertTrue(features.artifacts.isEmpty) XCTAssertTrue(features.tasks.isEmpty) XCTAssertTrue(features.memory.isEmpty) XCTAssertNil(features.previewText) } func testSharedThemeContrastAcrossPalettes() { func luminance(_ rgb: HopeRGB) -> Double { func channel(_ x: Double) -> Double { x <= 0.04045 ? x / 12.92 : pow((x + 0.055) / 1.055, 2.4) } return 0.2126 * channel(rgb.red) + 0.7152 * channel(rgb.green) + 0.0722 * channel(rgb.blue) } for palette in HopePalette.allCases { for dark in [false, true] { let ground = luminance(HopeTheme.rgb(.ground, palette: palette, dark: dark)) for role in [HopeThemeRole.ink, .quiet, .accent] { let text = luminance(HopeTheme.rgb(role, palette: palette, dark: dark)) XCTAssertGreaterThanOrEqual((max(text, ground) + 0.05) / (min(text, ground) + 0.05), 4.5, "\(palette) \(role) \(dark)") } } } } }