Archived
152 lines
7.5 KiB
Swift
152 lines
7.5 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 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)")
|
|
}
|
|
} }
|
|
}
|
|
}
|