Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ let package = Package(
exclude: [
"Yemma4.entitlements",
]
),
.testTarget(
name: "Yemma4Tests",
dependencies: ["Yemma4"],
path: "Tests/Yemma4Tests"
)
]
)
80 changes: 80 additions & 0 deletions Tests/Yemma4Tests/ConversationStoreTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Foundation
import XCTest
import ExyteChat
@testable import Yemma4

@MainActor
final class ConversationStoreTests: XCTestCase {
func testAsyncRestoreDecodesIso8601DatesFromPersistedConversation() async throws {
let fileManager = FileManager.default
let storageRoot = fileManager.temporaryDirectory.appendingPathComponent(
"ConversationStoreTests-\(UUID().uuidString)",
isDirectory: true
)
let defaultsName = "ConversationStoreTests-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: defaultsName)
XCTAssertNotNil(defaults)

guard let defaults else {
return
}

defer {
defaults.removePersistentDomain(forName: defaultsName)
try? fileManager.removeItem(at: storageRoot)
}

let store = ConversationStore(
fileManager: fileManager,
defaults: defaults,
storageRootOverride: storageRoot
)

let createdAt = Date(timeIntervalSince1970: 1_700_000_000)
let message = ChatMessage(
id: "message-1",
user: .user,
status: .sent,
createdAt: createdAt,
text: "Hello",
attachments: []
)
let draftAttachment = Attachment(
id: "attachment-1",
url: storageRoot.appendingPathComponent("draft.png"),
type: .image
)

let conversationID = store.saveConversation(
id: nil,
messages: [message],
draftText: "Draft text",
draftAttachments: [draftAttachment]
)

let reloadedStore = ConversationStore(
fileManager: fileManager,
defaults: defaults,
storageRootOverride: storageRoot
)

await reloadedStore.loadIndexIfNeeded()

XCTAssertEqual(reloadedStore.conversations.count, 1)
XCTAssertEqual(reloadedStore.conversations.first?.id, conversationID)
XCTAssertEqual(reloadedStore.conversations.first?.messageCount, 1)
XCTAssertEqual(reloadedStore.conversations.first?.hasDraft, true)
XCTAssertEqual(reloadedStore.currentConversationID, conversationID)

let snapshot = await reloadedStore.loadConversationAsync(id: conversationID)

XCTAssertNotNil(snapshot)
XCTAssertEqual(snapshot?.id, conversationID)
XCTAssertEqual(snapshot?.title, "Hello")
XCTAssertEqual(snapshot?.draftText, "Draft text")
XCTAssertEqual(snapshot?.draftAttachments.count, 1)
XCTAssertEqual(snapshot?.messages.count, 1)
XCTAssertEqual(snapshot?.messages.first?.createdAt, createdAt)
XCTAssertEqual(snapshot?.messages.first?.text, "Hello")
}
}
16 changes: 16 additions & 0 deletions Tests/Yemma4Tests/StreamingRendererTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import XCTest
@testable import Yemma4

final class StreamingRendererTests: XCTestCase {
func testSanitizeRemovesControlMarkersAndThinkingBlocks() {
let raw = "<start_of_turn>model\n<|channel>thinking<channel|>Hello<end_of_turn>user"

XCTAssertEqual(StreamingRenderer.sanitize(raw), "Hello")
}

func testStreamingVisibleTextAndStopDetection() {
XCTAssertEqual(StreamingRenderer.streamingVisibleText("Hello wor"), "Hello")
XCTAssertTrue(StreamingRenderer.shouldStopStreaming(tailOf: "prefix <|end_of_turn|>"))
XCTAssertFalse(StreamingRenderer.shouldStopStreaming(tailOf: "prefix Hello"))
}
}
2 changes: 1 addition & 1 deletion Yemma4/Appearance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ struct UtilityBackground: View {
}

private struct ProgressiveHeaderHeightKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static let defaultValue: CGFloat = 0

static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
Expand Down
3 changes: 3 additions & 0 deletions Yemma4/Services/ConversationStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ final class ConversationStore {
private func loadIndexAsync() async {
let decoded = await Task.detached(priority: .utility) { [indexURL] in
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
guard let data = try? Data(contentsOf: indexURL) else {
return [ConversationMetadata]()
}
Expand Down Expand Up @@ -638,6 +639,7 @@ final class ConversationStore {
decoder.dateDecodingStrategy = .iso8601
return decoder
}()

}

private enum ConversationSnapshotLoader {
Expand All @@ -647,6 +649,7 @@ private enum ConversationSnapshotLoader {
}

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
guard let conversation = try? decoder.decode(PersistedConversation.self, from: data) else {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion Yemma4/Views/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2037,7 +2037,7 @@ private struct StartupLoadingAnimationView: View {
}

private struct ConversationBottomOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static let defaultValue: CGFloat = 0

static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
Expand Down
17 changes: 17 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Testing Notes

The highest-value pure-Swift coverage added for this task lives under `Tests/Yemma4Tests/`:

- `ConversationStoreTests.swift` exercises the async restore path against an ISO-8601 persisted conversation.
- `StreamingRendererTests.swift` covers the sanitizer and stop-stream detection helpers.

Run the package-backed XCTest target with:

```bash
xcodebuild test \
-workspace .swiftpm/xcode/package.xcworkspace \
-scheme Yemma4 \
-destination 'platform=iOS Simulator,name=Yemma Preview 17 Pro Max'
```

The app target can still be validated separately with the project build path when you want a simulator compile of the shipped shell.