analytics(integration): adapter + reconcile coordinator + deep links (KC-51 ph3)
Phase 3 core (verifiable): WorkoutAnalyticsAdapter (app data → DaySample), AnalyticsCoordinator (idempotent backfill of missing weekly/monthly reports + retention prune, injectable providers), AnalyticsDeepLink (wenza://analytics report routing). Fixed a reconcile bug caught by tests: a retention-boundary week was generated then immediately pruned, regenerating every run — aligned completedWeekStartKeys to the prune cutoff → idempotent. Tests: AnalyticsAdapterTests + AnalyticsCoordinatorTests. Verified standalone via swiftc (~90 analytics assertions total across engine/store/adapter/coordinator). Remaining (app wiring, notifications, UI) requires an Xcode build — logged in ISSUES.md as device-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
53
KisaniCalTests/AnalyticsCoordinatorTests.swift
Normal file
53
KisaniCalTests/AnalyticsCoordinatorTests.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
import XCTest
|
||||
@testable import KisaniCal
|
||||
|
||||
/// Deep-link routing + reconcile idempotency (no duplicate reports across runs).
|
||||
final class AnalyticsCoordinatorTests: XCTestCase {
|
||||
|
||||
private func cal() -> Calendar {
|
||||
var c = Calendar(identifier: .gregorian)
|
||||
c.timeZone = TimeZone(identifier: "UTC")!
|
||||
c.locale = Locale(identifier: "en_US_POSIX")
|
||||
return c
|
||||
}
|
||||
private let now = Date(timeIntervalSince1970: 1_783_500_000) // 2026-07-08
|
||||
|
||||
// MARK: - Deep links
|
||||
|
||||
func testDeepLinkRoundTrips() {
|
||||
XCTAssertEqual(AnalyticsDeepLink(url: AnalyticsDeepLink.overview.url), .overview)
|
||||
XCTAssertEqual(AnalyticsDeepLink(url: AnalyticsDeepLink.weekly(weekStartKey: "2026-06-29").url),
|
||||
.weekly(weekStartKey: "2026-06-29"))
|
||||
XCTAssertEqual(AnalyticsDeepLink(string: "wenza://analytics/monthly/2026-05"),
|
||||
.monthly(monthKey: "2026-05"))
|
||||
XCTAssertEqual(AnalyticsDeepLink.weekly(weekStartKey: "2026-06-29").url.absoluteString,
|
||||
"wenza://analytics/weekly/2026-06-29")
|
||||
XCTAssertNil(AnalyticsDeepLink(string: "http://x/weekly/2026-06-29"))
|
||||
}
|
||||
|
||||
// MARK: - Reconcile idempotency
|
||||
|
||||
func testReconcileGeneratesThenIsIdempotent() {
|
||||
let tmp = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("coord_\(UUID().uuidString)")
|
||||
defer { try? FileManager.default.removeItem(at: tmp) }
|
||||
let store = AnalyticsStore(root: tmp)
|
||||
func sample(_ v: Double) -> [DaySample] {
|
||||
[DaySample(dateKey: "x", didWorkout: true, scheduled: true, sessions: 1, sets: 10, volume: v)]
|
||||
}
|
||||
let coord = AnalyticsCoordinator(
|
||||
store: store, calendar: cal(), retentionMonths: 12, now: { self.now },
|
||||
weekSamples: { _ in (sample(1000), sample(800)) },
|
||||
monthSamples: { _ in (sample(4000), sample(3500)) })
|
||||
|
||||
let first = coord.reconcile()
|
||||
XCTAssertGreaterThanOrEqual(first.weekliesGenerated.count, 50)
|
||||
XCTAssertEqual(first.monthliesGenerated.count, 12)
|
||||
|
||||
let second = coord.reconcile()
|
||||
XCTAssertTrue(second.isEmpty, "second run must generate nothing (no duplicate reports)")
|
||||
|
||||
XCTAssertEqual(store.allWeeklyReports().count, first.weekliesGenerated.count)
|
||||
XCTAssertEqual(store.allMonthlyReports().count, 12)
|
||||
XCTAssertEqual(store.allWeeklyReports().first?.progression, .improving) // 1000 vs 800
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user