add astra draft

This commit is contained in:
2026-09-17 17:59:34 +02:00
parent 88588dcfcc
commit 41458afc76
38 changed files with 2392 additions and 0 deletions
@@ -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()
}
}