ios-projects/AIStudyApp/AIStudyAppTests/ReadingEventUploadItemTests.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

137 lines
4.9 KiB
Swift

import XCTest
@testable import AIStudyApp
final class ReadingEventUploadItemTests: XCTestCase {
// MARK: - Codable roundtrip
func test_codable_roundtrip_without_position() throws {
let item = ReadingEventUploadItem(
eventId: "evt-001",
clientSessionId: "sess-001",
materialId: "mat-001",
eventType: "material_opened",
position: nil,
activeSecondsDelta: 0,
clientTimestampMs: 1_718_150_400_000,
sequence: 1,
readingTargetType: "knowledge_source",
knowledgeBaseId: "kb-001",
platform: "ios",
appVersion: "1.0.0",
clientTimezoneOffsetMinutes: 480
)
let data = try JSONEncoder().encode(item)
let decoded = try JSONDecoder().decode(ReadingEventUploadItem.self, from: data)
XCTAssertEqual(decoded.eventId, "evt-001")
XCTAssertEqual(decoded.materialId, "mat-001")
XCTAssertEqual(decoded.eventType, "material_opened")
XCTAssertEqual(decoded.readingTargetType, "knowledge_source")
XCTAssertEqual(decoded.knowledgeBaseId, "kb-001")
XCTAssertEqual(decoded.platform, "ios")
XCTAssertEqual(decoded.appVersion, "1.0.0")
XCTAssertEqual(decoded.clientTimezoneOffsetMinutes, 480)
}
func test_codable_roundtrip_nil_knowledge_base_id() throws {
let item = ReadingEventUploadItem(
eventId: "evt-002",
clientSessionId: "sess-002",
materialId: "mat-002",
eventType: "heartbeat",
position: nil,
activeSecondsDelta: 30,
clientTimestampMs: 1_718_150_500_000,
sequence: 2,
readingTargetType: "temporary_file",
knowledgeBaseId: nil,
platform: "ios",
appVersion: "1.0.0",
clientTimezoneOffsetMinutes: 480
)
let data = try JSONEncoder().encode(item)
let decoded = try JSONDecoder().decode(ReadingEventUploadItem.self, from: data)
XCTAssertNil(decoded.knowledgeBaseId)
}
func test_json_uses_camel_case_keys() throws {
let item = ReadingEventUploadItem(
eventId: "evt-003",
clientSessionId: "sess-003",
materialId: "mat-003",
eventType: "position_changed",
position: nil,
activeSecondsDelta: 15,
clientTimestampMs: 1_718_150_600_000,
sequence: 3,
readingTargetType: "knowledge_source",
knowledgeBaseId: nil,
platform: "ios",
appVersion: "1.0.0",
clientTimezoneOffsetMinutes: 480
)
let data = try JSONEncoder().encode(item)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
XCTAssertNotNil(json["eventId"])
XCTAssertNotNil(json["clientSessionId"])
XCTAssertNotNil(json["materialId"])
XCTAssertNotNil(json["eventType"])
XCTAssertNotNil(json["clientTimestampMs"])
XCTAssertNotNil(json["readingTargetType"])
XCTAssertNotNil(json["clientTimezoneOffsetMinutes"])
XCTAssertNil(json["event_id"]) // snake_case should not appear
}
// MARK: - Batch request/response
func test_batch_request_encodes_events_array() throws {
let events = [
ReadingEventUploadItem(
eventId: "evt-001",
clientSessionId: "sess-001",
materialId: "mat-001",
eventType: "material_opened",
position: nil,
activeSecondsDelta: 0,
clientTimestampMs: 1_718_150_400_000,
sequence: 1,
readingTargetType: "knowledge_source",
knowledgeBaseId: nil,
platform: "ios",
appVersion: "1.0.0",
clientTimezoneOffsetMinutes: 480
)
]
let batch = ReadingEventBatchRequest(events: events)
let data = try JSONEncoder().encode(batch)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
let eventsArray = json["events"] as! [[String: Any]]
XCTAssertEqual(eventsArray.count, 1)
XCTAssertEqual(eventsArray[0]["eventId"] as? String, "evt-001")
}
func test_batch_response_decodes() throws {
let json = """
{
"processed": 3,
"duplicate": 1,
"failed": 0,
"warnings": [
{"eventId": "evt-dup", "code": "DUPLICATE_EVENT", "message": "already processed"}
]
}
""".data(using: .utf8)!
let resp = try JSONDecoder().decode(ReadingEventBatchResponse.self, from: json)
XCTAssertEqual(resp.processed, 3)
XCTAssertEqual(resp.duplicate, 1)
XCTAssertEqual(resp.failed, 0)
XCTAssertEqual(resp.warnings?.count, 1)
XCTAssertEqual(resp.warnings?.first?.code, "DUPLICATE_EVENT")
}
}