This repository has been archived on 2026-10-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
hope-voice-ios/Tests/ChatTests.swift
T

180 lines
9.1 KiB
Swift

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 testWorkspaceSaveReportsFailureAndPreservesExistingWorkspaces() async throws {
let store = ChatStore(api: OfflineChatAPI())
let rejected = await store.saveWorkspace(nil, name: " ", instructions: "Ignored")
XCTAssertFalse(rejected)
XCTAssertEqual(store.error, "Enter a workspace name.")
XCTAssertTrue(store.workspaces.isEmpty)
let saved = await store.saveWorkspace(nil, name: "Writing", instructions: "Be concise")
XCTAssertTrue(saved)
XCTAssertEqual(store.workspaces.map(\.name), ["Writing"])
}
func testPreviewClearsOldContentBeforeNewRequestAndOnTeardown() async throws {
let api = OfflineChatAPI()
let store = ChatStore(api: api)
await store.features.load()
let data = Data("Private preview".utf8)
_ = await store.features.upload(data, filename: "preview.txt", mime: "text/plain")
let attachment = try XCTUnwrap(store.features.attachments.first)
store.features.previewText = "stale"
await store.features.preview(attachment)
XCTAssertEqual(store.features.previewAttachment?.id, attachment.id)
XCTAssertNotEqual(store.features.previewText, "stale")
store.features.clearPreview()
XCTAssertNil(store.features.previewAttachment)
XCTAssertNil(store.features.previewText)
}
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)
XCTAssertTrue(store.exportFile?.filename.hasSuffix(".md") == true)
XCTAssertEqual(store.exportFile?.data, Data(store.exportText!.utf8))
store.foreground(true)
store.foreground(false)
store.close()
XCTAssertNil(store.exportText)
XCTAssertNil(store.exportFile)
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)")
}
} }
}
}