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