add astra draft
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import PolicyCore
|
||||
|
||||
final class PolicyCoreTests: XCTestCase {
|
||||
func date(_ value: String) -> Date { ISO8601DateFormatter().date(from: value)! }
|
||||
var noon: Date { date("2026-09-17T10:00:00Z") }
|
||||
func snapshot(_ policy: Policy = Policy(), bonus: Int = 0) -> Snapshot {
|
||||
Snapshot(deviceId: "device", childId: "child", childName: "Example", policyRevision: 1,
|
||||
policy: policy, day: "2026-09-17", earnedMinutes: bonus, serverTime: noon.timeIntervalSince1970)
|
||||
}
|
||||
func testRewardExtendsUsageNotWallClock() {
|
||||
var observation = Observation()
|
||||
observation.record(threshold: 30, day: "2026-09-17")
|
||||
XCTAssertTrue(Decision.evaluate(snapshot(), observation: observation, now: noon).shielded)
|
||||
XCTAssertFalse(Decision.evaluate(snapshot(bonus: 10), observation: observation, now: noon).shielded)
|
||||
observation.record(threshold: 40, day: "2026-09-17")
|
||||
XCTAssertTrue(Decision.evaluate(snapshot(bonus: 10), observation: observation, now: noon).shielded)
|
||||
}
|
||||
func testRefreshAndDuplicateCallbacksDoNotRefundUsage() {
|
||||
var observation = Observation()
|
||||
observation.record(threshold: 40, day: "2026-09-17")
|
||||
observation.record(threshold: 30, day: "2026-09-17")
|
||||
observation.record(threshold: 40, day: "2026-09-17")
|
||||
observation.roll(to: "2026-09-17")
|
||||
XCTAssertEqual(observation.reachedMinutes, 40)
|
||||
}
|
||||
func testBonusExpiresAndBaseResetsWithoutServer() {
|
||||
var observation = Observation()
|
||||
observation.record(threshold: 60, day: "2026-09-17")
|
||||
let decision = Decision.evaluate(snapshot(bonus: 30), observation: observation,
|
||||
now: date("2026-09-18T10:00:00Z"))
|
||||
XCTAssertFalse(decision.shielded)
|
||||
XCTAssertEqual(decision.budgetMinutes, 30)
|
||||
}
|
||||
func testBedtimeCannotBeBought() {
|
||||
XCTAssertTrue(Decision.evaluate(snapshot(bonus: 30), observation: Observation(),
|
||||
now: date("2026-09-17T18:00:00Z")).shielded)
|
||||
XCTAssertTrue(Decision.evaluate(snapshot(bonus: 30), observation: Observation(),
|
||||
now: date("2026-09-17T04:59:00Z")).shielded)
|
||||
XCTAssertFalse(Decision.evaluate(snapshot(), observation: Observation(),
|
||||
now: date("2026-09-17T05:00:00Z")).shielded)
|
||||
}
|
||||
func testZeroBaseAndZeroBonusFailClosed() {
|
||||
let policy = Policy(dailyMinutes: 0, maxBonusMinutes: 0)
|
||||
XCTAssertTrue(Decision.evaluate(snapshot(policy), observation: Observation(), now: noon).shielded)
|
||||
XCTAssertEqual(policy.thresholds(startingBonus: 0), [])
|
||||
}
|
||||
func testThresholdLadderIncludesPartialCapAndRebasedRewards() {
|
||||
let policy = Policy(bonusMinutes: 7, maxBonusMinutes: 23)
|
||||
let thresholds = policy.thresholds(startingBonus: 10)
|
||||
for budget in [30, 37, 44, 51, 53, 40, 47] { XCTAssertTrue(thresholds.contains(budget)) }
|
||||
XCTAssertEqual(thresholds, thresholds.sorted())
|
||||
}
|
||||
func testEveryReachableRewardHasAThreshold() {
|
||||
for step in 5...30 {
|
||||
for cap in 0...60 {
|
||||
let policy = Policy(dailyMinutes: 0, bonusMinutes: step, maxBonusMinutes: cap)
|
||||
for initial in 0...cap {
|
||||
let thresholds = Set(policy.thresholds(startingBonus: initial))
|
||||
var bonus = initial
|
||||
while bonus < cap {
|
||||
bonus = min(bonus + step, cap)
|
||||
XCTAssertTrue(thresholds.contains(bonus))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func testTimezoneMidnightAndDST() {
|
||||
let policy = Policy()
|
||||
XCTAssertEqual(policy.day(at: date("2026-09-17T22:01:00Z")), "2026-09-18")
|
||||
XCTAssertTrue(policy.isAllowed(at: date("2026-10-24T05:00:00Z")))
|
||||
XCTAssertFalse(policy.isAllowed(at: date("2026-10-25T05:00:00Z")))
|
||||
XCTAssertTrue(policy.isAllowed(at: date("2026-10-25T06:00:00Z")))
|
||||
}
|
||||
func testValidationRejectsUnsupportedAndUnsafePolicies() {
|
||||
XCTAssertThrowsError(try Policy(timezone: "Bogus/Zone").validate())
|
||||
XCTAssertThrowsError(try Policy(bonusMinutes: 0).validate())
|
||||
XCTAssertThrowsError(try Policy(allowedStart: 1200, allowedEnd: 420).validate())
|
||||
var bad = snapshot(); bad.schemaVersion = 2
|
||||
XCTAssertThrowsError(try bad.validate())
|
||||
bad = snapshot(); bad.day = "2099-01-01"
|
||||
XCTAssertThrowsError(try bad.validate())
|
||||
}
|
||||
func testWireRoundTripUsesServerFieldNames() throws {
|
||||
let original = snapshot(bonus: 10)
|
||||
let data = try WireCoding.encoder().encode(original)
|
||||
XCTAssertTrue(String(decoding: data, as: UTF8.self).contains("\"policy_revision\""))
|
||||
let decoded = try WireCoding.decoder().decode(Snapshot.self, from: data)
|
||||
XCTAssertEqual(original, decoded)
|
||||
try decoded.validate()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user