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>
121 lines
4.1 KiB
Swift
121 lines
4.1 KiB
Swift
import XCTest
|
|
@testable import AIStudyApp
|
|
|
|
final class ExportQueueAckTests: XCTestCase {
|
|
|
|
// MARK: - Pipeline: export → enqueue
|
|
|
|
func test_exportAndEnqueue_pipeline_exists() {
|
|
let pipeline = ReadingEventUploadPipeline.shared
|
|
XCTAssertNotNil(pipeline)
|
|
}
|
|
|
|
// MARK: - Queue: enqueue + pending count
|
|
|
|
func test_queue_enqueue_increases_pending_count() {
|
|
let queue = ReadingEventUploadQueue.shared
|
|
let before = queue.pendingCount
|
|
|
|
let item = ReadingEventUploadItem(
|
|
eventId: "evt-e2a-001",
|
|
clientSessionId: "sess-001",
|
|
materialId: "mat-001",
|
|
eventType: "material_opened",
|
|
position: nil,
|
|
activeSecondsDelta: 0,
|
|
clientTimestampMs: 1_700_000_000_000,
|
|
sequence: 1,
|
|
readingTargetType: "knowledge_source",
|
|
knowledgeBaseId: nil,
|
|
platform: "ios",
|
|
appVersion: "1.0.0",
|
|
clientTimezoneOffsetMinutes: 480
|
|
)
|
|
queue.enqueue([item])
|
|
let after = queue.pendingCount
|
|
XCTAssertGreaterThanOrEqual(after, before, "Enqueue should not decrease pending count")
|
|
|
|
// Cleanup
|
|
queue.markPermanentFailed(ids: [item.eventId], errorCode: "TEST_CLEANUP")
|
|
}
|
|
|
|
// MARK: - Queue: mark uploaded
|
|
|
|
func test_queue_mark_uploaded_removes_from_pending() {
|
|
let queue = ReadingEventUploadQueue.shared
|
|
let item = ReadingEventUploadItem(
|
|
eventId: "evt-e2a-002",
|
|
clientSessionId: "sess-002",
|
|
materialId: "mat-002",
|
|
eventType: "heartbeat",
|
|
position: nil,
|
|
activeSecondsDelta: 15,
|
|
clientTimestampMs: 1_700_000_000_000,
|
|
sequence: 2,
|
|
readingTargetType: "temporary_file",
|
|
knowledgeBaseId: nil,
|
|
platform: "ios",
|
|
appVersion: "1.0.0",
|
|
clientTimezoneOffsetMinutes: 480
|
|
)
|
|
queue.enqueue([item])
|
|
queue.markUploading(ids: [item.eventId])
|
|
queue.markUploaded(ids: [item.eventId])
|
|
|
|
// After markUploaded, item should not be pending
|
|
let pending = queue.fetchPendingBatch(limit: 10)
|
|
let stillPending = pending.contains { $0.eventId == item.eventId }
|
|
XCTAssertFalse(stillPending, "Marked-uploaded item should not be in pending batch")
|
|
}
|
|
|
|
// MARK: - Queue: mark retry
|
|
|
|
func test_queue_mark_retry_keeps_item_pending() {
|
|
let queue = ReadingEventUploadQueue.shared
|
|
let item = ReadingEventUploadItem(
|
|
eventId: "evt-e2a-003",
|
|
clientSessionId: "sess-003",
|
|
materialId: "mat-003",
|
|
eventType: "position_changed",
|
|
position: nil,
|
|
activeSecondsDelta: 0,
|
|
clientTimestampMs: 1_700_000_000_000,
|
|
sequence: 3,
|
|
readingTargetType: "knowledge_source",
|
|
knowledgeBaseId: nil,
|
|
platform: "ios",
|
|
appVersion: "1.0.0",
|
|
clientTimezoneOffsetMinutes: 480
|
|
)
|
|
queue.enqueue([item])
|
|
queue.markRetry(ids: [item.eventId])
|
|
|
|
// Cleanup
|
|
queue.clearAll()
|
|
}
|
|
|
|
// MARK: - Ack idempotency
|
|
|
|
func test_ack_idempotent() {
|
|
let adapter = NoopReadingRuntimeAdapter()
|
|
// Ack should return the count even for non-existent events
|
|
let count1 = adapter.ackEvents(["evt-fake-1", "evt-fake-2"])
|
|
let count2 = adapter.ackEvents(["evt-fake-1", "evt-fake-2"])
|
|
XCTAssertEqual(count1, count2, "Repeated ack should return same count (idempotent)")
|
|
}
|
|
|
|
// MARK: - Adapter: export + ack + markFailed
|
|
|
|
func test_noop_adapter_export_returns_empty() {
|
|
let adapter = NoopReadingRuntimeAdapter()
|
|
let events = adapter.exportEvents(limit: 100, timestampMs: 1_700_000_000_000)
|
|
XCTAssertTrue(events.isEmpty, "Noop adapter should return empty export")
|
|
}
|
|
|
|
func test_noop_adapter_mark_failed_returns_count() {
|
|
let adapter = NoopReadingRuntimeAdapter()
|
|
let count = adapter.markFailed(["evt-f1", "evt-f2", "evt-f3"])
|
|
XCTAssertEqual(count, 3)
|
|
}
|
|
}
|