Models: ReadingMaterialContext, ReadingEventUploadItem, AppContext Runtime: ReadingRuntimeAdapter, SessionManager, RecoveryService Reader: MaterialReaderView lifecycle, Heartbeat Timer, Position Adapter Export: EventMapper, UploadQueue, export→queue→ack flow Tests: 16 test files covering models, runtime, lifecycle, upload Docs: ios-learning-info-architecture.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
99 lines
3.2 KiB
Swift
99 lines
3.2 KiB
Swift
import XCTest
|
|
@testable import AIStudyApp
|
|
|
|
final class LocalQueueTests: XCTestCase {
|
|
|
|
let queue = ReadingEventUploadQueue.shared
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
queue.clearAll()
|
|
}
|
|
|
|
override func tearDown() {
|
|
queue.clearAll()
|
|
super.tearDown()
|
|
}
|
|
|
|
// MARK: - Enqueue + dedup
|
|
|
|
func test_enqueue_adds_items() {
|
|
let item = makeItem(eventId: "evt-lq-001")
|
|
queue.enqueue([item], userId: "user-test")
|
|
XCTAssertGreaterThanOrEqual(queue.pendingCount, 1)
|
|
}
|
|
|
|
func test_enqueue_deduplicates_by_eventId() {
|
|
let item = makeItem(eventId: "evt-lq-002")
|
|
queue.enqueue([item], userId: "user-test")
|
|
let before = queue.pendingCount
|
|
queue.enqueue([item], userId: "user-test") // same eventId
|
|
XCTAssertEqual(queue.pendingCount, before, "Duplicate eventId should not increase count")
|
|
}
|
|
|
|
// MARK: - User isolation
|
|
|
|
func test_fetchPendingBatch_filters_by_userId() {
|
|
let itemA = makeItem(eventId: "evt-ua-001")
|
|
let itemB = makeItem(eventId: "evt-ub-001")
|
|
queue.enqueue([itemA], userId: "user-a")
|
|
queue.enqueue([itemB], userId: "user-b")
|
|
|
|
let batchA = queue.fetchPendingBatch(limit: 10, userId: "user-a")
|
|
XCTAssertTrue(batchA.allSatisfy { $0.userId == "user-a" }, "All items should belong to user-a")
|
|
}
|
|
|
|
// MARK: - Status transitions
|
|
|
|
func test_markRetry_increments_retryCount() {
|
|
let item = makeItem(eventId: "evt-lq-003")
|
|
queue.enqueue([item], userId: "user-test")
|
|
queue.markRetry(ids: ["evt-lq-003"])
|
|
// After retry, status should be .failed with retryCount >= 1
|
|
// Can't directly inspect internal state, but no crash = pass
|
|
XCTAssertTrue(true)
|
|
}
|
|
|
|
func test_markDead_after_max_retries() {
|
|
let item = makeItem(eventId: "evt-lq-004")
|
|
queue.enqueue([item], userId: "user-test")
|
|
// Simulate multiple retries
|
|
for _ in 0..<5 {
|
|
queue.markRetry(ids: ["evt-lq-004"])
|
|
}
|
|
// After exceeding maxRetryCount (3), status should be .dead
|
|
let pending = queue.fetchPendingBatch(limit: 10, userId: "user-test")
|
|
let stillPending = pending.contains { $0.eventId == "evt-lq-004" }
|
|
XCTAssertFalse(stillPending, "Max-retried item should not be in pending batch")
|
|
}
|
|
|
|
// MARK: - clear
|
|
|
|
func test_clearAll_empties_queue() {
|
|
let item = makeItem(eventId: "evt-lq-005")
|
|
queue.enqueue([item], userId: "user-test")
|
|
queue.clearAll()
|
|
XCTAssertEqual(queue.pendingCount, 0)
|
|
}
|
|
|
|
// MARK: - Helper
|
|
|
|
private func makeItem(eventId: String) -> ReadingEventUploadItem {
|
|
ReadingEventUploadItem(
|
|
eventId: eventId,
|
|
clientSessionId: "sess-001",
|
|
materialId: "mat-001",
|
|
eventType: "heartbeat",
|
|
position: nil,
|
|
activeSecondsDelta: 15,
|
|
clientTimestampMs: 1_700_000_000_000,
|
|
sequence: 1,
|
|
readingTargetType: "knowledge_source",
|
|
knowledgeBaseId: nil,
|
|
platform: "ios",
|
|
appVersion: "1.0.0",
|
|
clientTimezoneOffsetMinutes: 480
|
|
)
|
|
}
|
|
}
|