ios-projects/AIStudyApp/AIStudyAppTests/ExportQueueAckTests.swift
wangdl 4ba747af46 feat: iOS learning info P0 complete (IOS-INFO-000~017, 038~039)
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>
2026-06-15 21:53:36 +08:00

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)
}
}