feat(swift-sdk): add ByteLystPlatform unified entry point + 5 new test files (4.1)

New source:
- ByteLystPlatform.swift — unified entry point wiring all services
  (config, client, telemetry, flags, killSwitch, crashReporter, keychain, auditLog, auth)
- BLKeychainAccessor — convenience wrapper binding BLKeychain to a bundleId
- start(userId:) / stop() lifecycle for telemetry + flags + killSwitch

New tests (5 files, ~25 test cases):
- ByteLystPlatformTests — init, start/stop, idempotency, keychain accessor
- BLPlatformConfigTests — default + custom init
- BLKillSwitchClientTests — default state, reset
- BLFeatureFlagClientTests — empty flags, unknown key, stop
- BLTelemetryClientTests — installId stability, session rotation, track/flush

Also: add .build/ and .swiftpm/ to .gitignore
This commit is contained in:
saravanakumardb1 2026-03-19 21:05:58 -07:00
parent 1fda345d38
commit 933390e89b
7 changed files with 347 additions and 0 deletions

2
.gitignore vendored
View File

@ -21,6 +21,8 @@ __LOCAL_LLMs/models/
__LOCAL_LLMs/.venv-*/
__LOCAL_LLMs/*.wav
packages/swift-platform-sdk/build/
packages/swift-platform-sdk/.build/
packages/swift-platform-sdk/.swiftpm/
packages/kotlin-platform-sdk/build/
packages/kotlin-platform-sdk/.gradle/
packages/kotlin-platform-sdk/gradle/

View File

@ -0,0 +1,121 @@
// ByteLystPlatform
// Unified entry point for the ByteLyst platform SDK.
// Creates and wires all platform services from a single config.
//
// Usage:
// let platform = ByteLystPlatform(config: .init(
// productId: "peakpulse",
// baseURL: "https://api.peakpulse.app",
// bundleId: "com.saravana.peakpulse"
// ))
//
// platform.start() // Start telemetry + flags + kill switch
// platform.telemetry.trackScreen("home") // Track events
// let isNew = platform.flags.isEnabled("new_feature")
// platform.stop() // Flush + stop timers
import Foundation
/// Unified entry point that wires all ByteLyst platform services together.
/// Create one instance at app launch and access services via properties.
public final class ByteLystPlatform {
/// Platform configuration.
public let config: BLPlatformConfig
/// HTTP client shared by all services.
public let client: BLPlatformClient
/// Telemetry event tracking.
public let telemetry: BLTelemetryClient
/// Feature flag polling.
public let flags: BLFeatureFlagClient
/// Kill switch checker.
public let killSwitch: BLKillSwitchClient
/// Crash reporter (MetricKit).
public let crashReporter: BLCrashReporter
/// Keychain access (via bundleId as service).
public let keychain: BLKeychainAccessor
/// Audit logger.
public let auditLog: BLAuditLogger
/// Auth client.
public let auth: BLAuthClient
/// Whether `start()` has been called.
public private(set) var isStarted = false
public init(config: BLPlatformConfig) {
self.config = config
self.client = BLPlatformClient(config: config)
self.telemetry = BLTelemetryClient(config: config, client: client)
self.flags = BLFeatureFlagClient(config: config, client: client)
self.killSwitch = BLKillSwitchClient(config: config)
self.crashReporter = BLCrashReporter(productId: config.productId)
self.keychain = BLKeychainAccessor(service: config.bundleId)
self.auditLog = BLAuditLogger(productId: config.productId)
self.auth = BLAuthClient(config: config, client: client)
}
/// Test-only initializer that accepts a custom URLSessionConfiguration.
public init(config: BLPlatformConfig, sessionConfiguration: URLSessionConfiguration) {
self.config = config
self.client = BLPlatformClient(config: config, sessionConfiguration: sessionConfiguration)
self.telemetry = BLTelemetryClient(config: config, client: client)
self.flags = BLFeatureFlagClient(config: config, client: client)
self.killSwitch = BLKillSwitchClient(config: config)
self.crashReporter = BLCrashReporter(productId: config.productId)
self.keychain = BLKeychainAccessor(service: config.bundleId)
self.auditLog = BLAuditLogger(productId: config.productId)
self.auth = BLAuthClient(config: config, client: client)
}
// MARK: - Lifecycle
/// Start all services: telemetry flush timer, feature flag polling, kill switch check.
public func start(userId: String? = nil) {
guard !isStarted else { return }
isStarted = true
telemetry.start()
flags.start(userId: userId)
Task { await killSwitch.check() }
}
/// Stop all services: flush telemetry, stop flag polling.
public func stop() {
guard isStarted else { return }
isStarted = false
telemetry.stop()
flags.stop()
}
}
// MARK: - Keychain Accessor
/// Convenience wrapper around BLKeychain that binds to a specific service (bundleId).
public struct BLKeychainAccessor {
private let service: String
public init(service: String) {
self.service = service
}
@discardableResult
public func save(key: String, value: String) -> Bool {
BLKeychain.save(service: service, key: key, value: value)
}
public func read(key: String) -> String? {
BLKeychain.read(service: service, key: key)
}
@discardableResult
public func delete(key: String) -> Bool {
BLKeychain.delete(service: service, key: key)
}
}

View File

@ -0,0 +1,31 @@
import XCTest
@testable import ByteLystPlatformSDK
final class BLFeatureFlagClientTests: XCTestCase {
private func makeClient() -> BLFeatureFlagClient {
let config = BLPlatformConfig(
productId: "testapp",
baseURL: "http://localhost:4003",
bundleId: "com.bytelyst.test"
)
let platformClient = BLPlatformClient(config: config)
return BLFeatureFlagClient(config: config, client: platformClient)
}
func testDefaultFlagsEmpty() {
let client = makeClient()
XCTAssertEqual(client.allFlags().count, 0)
}
func testIsEnabledReturnsFalseForUnknownKey() {
let client = makeClient()
XCTAssertFalse(client.isEnabled("nonexistent_flag"))
}
func testStopDoesNotCrash() {
let client = makeClient()
client.stop()
client.stop() // Double stop
}
}

View File

@ -0,0 +1,27 @@
import XCTest
@testable import ByteLystPlatformSDK
final class BLKillSwitchClientTests: XCTestCase {
private func makeConfig() -> BLPlatformConfig {
BLPlatformConfig(
productId: "testapp",
baseURL: "http://localhost:4003",
bundleId: "com.bytelyst.test"
)
}
func testDefaultState() {
let client = BLKillSwitchClient(config: makeConfig())
XCTAssertFalse(client.isDisabled)
XCTAssertEqual(client.maintenanceMessage, "")
}
func testReset() {
let client = BLKillSwitchClient(config: makeConfig())
// Manually test reset clears any hypothetical state
client.reset()
XCTAssertFalse(client.isDisabled)
XCTAssertEqual(client.maintenanceMessage, "")
}
}

View File

@ -0,0 +1,34 @@
import XCTest
@testable import ByteLystPlatformSDK
final class BLPlatformConfigTests: XCTestCase {
func testInitWithDefaults() {
let config = BLPlatformConfig(
productId: "testapp",
baseURL: "http://localhost:4003",
bundleId: "com.bytelyst.testapp"
)
XCTAssertEqual(config.productId, "testapp")
XCTAssertEqual(config.baseURL, "http://localhost:4003")
XCTAssertEqual(config.platform, "ios")
XCTAssertEqual(config.channel, "native")
XCTAssertEqual(config.bundleId, "com.bytelyst.testapp")
XCTAssertNil(config.appGroupId)
}
func testInitWithCustomValues() {
let config = BLPlatformConfig(
productId: "peakpulse",
baseURL: "https://api.peakpulse.app",
platform: "watchos",
channel: "companion",
bundleId: "com.saravana.peakpulse",
appGroupId: "group.com.saravana.peakpulse"
)
XCTAssertEqual(config.productId, "peakpulse")
XCTAssertEqual(config.platform, "watchos")
XCTAssertEqual(config.channel, "companion")
XCTAssertEqual(config.appGroupId, "group.com.saravana.peakpulse")
}
}

View File

@ -0,0 +1,65 @@
import XCTest
@testable import ByteLystPlatformSDK
final class BLTelemetryClientTests: XCTestCase {
private func makeClient() -> BLTelemetryClient {
let config = BLPlatformConfig(
productId: "testapp",
baseURL: "http://localhost:4003",
bundleId: "com.bytelyst.test"
)
let platformClient = BLPlatformClient(config: config)
return BLTelemetryClient(config: config, client: platformClient)
}
func testInstallIdIsStable() {
let client = makeClient()
let id1 = client.getInstallId()
let id2 = client.getInstallId()
XCTAssertEqual(id1, id2)
XCTAssertFalse(id1.isEmpty)
}
func testSessionIdIsNotEmpty() {
let client = makeClient()
XCTAssertFalse(client.getSessionId().isEmpty)
}
func testStartGeneratesNewSessionId() {
let client = makeClient()
let sessionBefore = client.getSessionId()
client.start()
let sessionAfter = client.getSessionId()
// start() rotates session ID
XCTAssertNotEqual(sessionBefore, sessionAfter)
client.stop()
}
func testStopDoesNotCrash() {
let client = makeClient()
client.stop() // Stop without start
client.start()
client.stop()
client.stop() // Double stop
}
func testTrackEventDoesNotCrash() {
let client = makeClient()
client.trackEvent("info", module: "test", name: "unit_test")
client.trackEvent("error", module: "test", name: "fail", message: "test error")
client.trackEvent("info", module: "test", name: "tagged", tags: ["key": "value"])
client.trackEvent("info", module: "test", name: "measured", metrics: ["duration": 1.5])
}
func testTrackScreenDoesNotCrash() {
let client = makeClient()
client.trackScreen("home")
client.trackScreen("settings")
}
func testFlushDoesNotCrashWhenEmpty() {
let client = makeClient()
client.flush() // Empty queue
}
}

View File

@ -0,0 +1,67 @@
import XCTest
@testable import ByteLystPlatformSDK
final class ByteLystPlatformTests: XCTestCase {
private func makeConfig() -> BLPlatformConfig {
BLPlatformConfig(
productId: "testapp",
baseURL: "http://localhost:4003/api",
platform: "ios",
channel: "native",
bundleId: "com.bytelyst.test"
)
}
func testInitCreatesAllServices() {
let platform = ByteLystPlatform(config: makeConfig())
XCTAssertEqual(platform.config.productId, "testapp")
XCTAssertNotNil(platform.client)
XCTAssertNotNil(platform.telemetry)
XCTAssertNotNil(platform.flags)
XCTAssertNotNil(platform.killSwitch)
XCTAssertNotNil(platform.crashReporter)
XCTAssertNotNil(platform.keychain)
XCTAssertNotNil(platform.auditLog)
XCTAssertNotNil(platform.auth)
}
func testStartSetsIsStarted() {
let platform = ByteLystPlatform(config: makeConfig())
XCTAssertFalse(platform.isStarted)
platform.start()
XCTAssertTrue(platform.isStarted)
}
func testStopClearsIsStarted() {
let platform = ByteLystPlatform(config: makeConfig())
platform.start()
XCTAssertTrue(platform.isStarted)
platform.stop()
XCTAssertFalse(platform.isStarted)
}
func testDoubleStartIsIdempotent() {
let platform = ByteLystPlatform(config: makeConfig())
platform.start()
platform.start() // Should not crash or double-start
XCTAssertTrue(platform.isStarted)
platform.stop()
}
func testDoubleStopIsIdempotent() {
let platform = ByteLystPlatform(config: makeConfig())
platform.start()
platform.stop()
platform.stop() // Should not crash
XCTAssertFalse(platform.isStarted)
}
func testKeychainAccessor() {
let accessor = BLKeychainAccessor(service: "com.bytelyst.test.accessor")
accessor.save(key: "test_key", value: "hello")
XCTAssertEqual(accessor.read(key: "test_key"), "hello")
accessor.delete(key: "test_key")
XCTAssertNil(accessor.read(key: "test_key"))
}
}