ios-projects/AIStudyApp/AIStudyAppReaderTests/SessionManagerTests.swift

190 lines
7.1 KiB
Swift

import XCTest
@testable import AIStudyApp
@MainActor
private final class SpyReadingRuntimeAdapter: ReadingRuntimeAdapter {
var fingerprint = "fingerprint-1"
private var nextSequence: UInt64 = 1
func startSession(material: ReadingMaterialRefV2, timestampMs: Int64) throws -> String {
"spy-session"
}
func pauseSession(_ sessionId: String) throws {}
func resumeSession(_ sessionId: String) throws {}
func closeSession(_ sessionId: String, timestampMs: Int64) throws {}
func pushOpened(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 {
makeEvent(sessionId: sessionId, materialId: materialId, eventType: .materialOpened)
}
func pushClosed(sessionId: String, materialId: String, delta: UInt32, timestampMs: Int64) throws -> ReadingEventV2 {
makeEvent(sessionId: sessionId, materialId: materialId, eventType: .materialClosed)
}
func pushPositionChanged(sessionId: String, materialId: String, position: ReadingPosition, timestampMs: Int64) throws -> ReadingEventV2 {
makeEvent(sessionId: sessionId, materialId: materialId, eventType: .positionChanged, position: position)
}
func pushCheckpointChanged(sessionId: String, materialId: String, checkpoint: ReadingCheckpoint, timestampMs: Int64) throws -> ReadingEventV2 {
makeEvent(sessionId: sessionId, materialId: materialId, eventType: .positionChanged, checkpoint: checkpoint)
}
func pushHeartbeat(sessionId: String, materialId: String, delta: UInt32, position: ReadingPosition?, timestampMs: Int64) throws -> ReadingEventV2 {
makeEvent(sessionId: sessionId, materialId: materialId, eventType: .heartbeat, position: position)
}
func pushHeartbeatCheckpoint(sessionId: String, materialId: String, delta: UInt32, checkpoint: ReadingCheckpoint?, timestampMs: Int64) throws -> ReadingEventV2 {
makeEvent(sessionId: sessionId, materialId: materialId, eventType: .heartbeat, checkpoint: checkpoint)
}
func pushMarkedAsRead(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 {
makeEvent(sessionId: sessionId, materialId: materialId, eventType: .markedAsRead)
}
func exportEvents(limit: UInt32, timestampMs: Int64) -> [ReadingEventV2] { [] }
func ackEvents(_ eventIds: [String]) -> UInt32 { UInt32(eventIds.count) }
func markFailed(_ eventIds: [String]) -> UInt32 { UInt32(eventIds.count) }
func reloadStaleEvents() -> UInt32 { 0 }
func cleanupStaleSessions(nowMs: Int64, maxAgeMs: Int64) -> UInt32 { 0 }
func runtimeComputeFileFingerprint(filePath: String) throws -> String {
fingerprint
}
func runtimeBuildRepresentationFingerprint(sourceFingerprint: String, rendererKind: RendererKind, parserVersion: String, schemaVersion: UInt32) -> String {
"\(sourceFingerprint)|\(rendererKind)|\(parserVersion)|\(schemaVersion)"
}
private func makeEvent(
sessionId: String,
materialId: String,
eventType: ReadingEventTypeV2,
position: ReadingPosition? = nil,
checkpoint: ReadingCheckpoint? = nil
) -> ReadingEventV2 {
defer { nextSequence += 1 }
return ReadingEventV2(
eventId: "evt-\(nextSequence)",
clientSessionId: sessionId,
materialId: materialId,
eventType: eventType,
position: position,
checkpoint: checkpoint,
activeSecondsDelta: 0,
timestampMs: 1_700_000_000_000,
sequence: nextSequence
)
}
}
@MainActor
final class SessionManagerTests: XCTestCase {
var manager: ReadingRuntimeSessionManager!
var noopAdapter: NoopReadingRuntimeAdapter!
fileprivate var spyAdapter: SpyReadingRuntimeAdapter!
override func setUp() {
super.setUp()
manager = ReadingRuntimeSessionManager.shared
noopAdapter = NoopReadingRuntimeAdapter()
spyAdapter = SpyReadingRuntimeAdapter()
manager.adapter = noopAdapter
manager.closeMaterial()
}
override func tearDown() {
manager.closeMaterial()
super.tearDown()
}
func test_initial_state_is_idle() {
XCTAssertTrue(manager.state == .idle || manager.state == .closed)
}
func test_markAsRead_guards_against_inactive() {
manager.closeMaterial()
manager.markAsRead()
XCTAssertTrue(true)
}
func test_isCompatibleRestoreCheckpoint_matches_active_representation() throws {
manager.adapter = spyAdapter
let context = ReadingMaterialContext(
readingTargetType: .knowledgeSource,
materialId: "restore-match",
materialType: .markdown,
filePath: "",
fileName: "doc.md",
fileType: "markdown"
)
try manager.openMaterial(context)
let checkpoint = ReadingCheckpoint(
schemaVersion: 2,
materialId: "restore-match",
representationFingerprint: "restore-match:block",
rendererKind: .block,
capturedAtMs: 1,
deviceId: "device-1",
deviceSequence: 1,
position: .block(blockId: "b1", blockProgress: 0.2, overallProgress: 0.2)
)
XCTAssertTrue(manager.isCompatibleRestoreCheckpoint(checkpoint))
}
func test_isCompatibleRestoreCheckpoint_rejects_mismatched_fingerprint() throws {
manager.adapter = spyAdapter
let context = ReadingMaterialContext(
readingTargetType: .knowledgeSource,
materialId: "restore-mismatch",
materialType: .markdown,
filePath: "",
fileName: "doc.md",
fileType: "markdown"
)
try manager.openMaterial(context)
let checkpoint = ReadingCheckpoint(
schemaVersion: 2,
materialId: "restore-mismatch",
representationFingerprint: "stale-fingerprint",
rendererKind: .block,
capturedAtMs: 1,
deviceId: "device-1",
deviceSequence: 1,
position: .block(blockId: "b1", blockProgress: 0.2, overallProgress: 0.2)
)
XCTAssertFalse(manager.isCompatibleRestoreCheckpoint(checkpoint))
}
func test_isCompatibleRestoreCheckpoint_rejects_mismatched_renderer_kind() throws {
manager.adapter = spyAdapter
let context = ReadingMaterialContext(
readingTargetType: .knowledgeSource,
materialId: "restore-renderer",
materialType: .markdown,
filePath: "",
fileName: "doc.md",
fileType: "markdown"
)
try manager.openMaterial(context)
let checkpoint = ReadingCheckpoint(
schemaVersion: 2,
materialId: "restore-renderer",
representationFingerprint: "restore-renderer:block",
rendererKind: .pdf,
capturedAtMs: 1,
deviceId: "device-1",
deviceSequence: 1,
position: .pdf(pageNumber: 1, pageProgress: 0, overallProgress: 0)
)
XCTAssertFalse(manager.isCompatibleRestoreCheckpoint(checkpoint))
}
}