Release 1.6.1
- Sync reminders: new "Sync reminders when mirroring" option (global and per-route) copies source event alarms into mirrored placeholders. CLI flag --sync-reminders. - CLI polish: --help, --list-calendars, --status (all support --json). Scheduled/headless runs record last-run time/result/summary. Failure paths now exit with distinct nonzero codes (2 = no calendar access, 3 = no saved routes) instead of always exiting 0. - Fix settings silently wiped on upgrade: SettingsPayload's auto-synthesized Codable threw on any settings blob missing a field added since (e.g. the new syncReminders key), failing the whole decode and letting the next autosave persist an empty state over real routes/filters. Custom decode now falls back per-field like Route already did, and loadSettingsFromDefaults recovers routes from the legacy routes.v1 key if settings.v2 comes back empty, repairing installs already hit by this. - Roadmap: event-driven background sync (SMAppService + EKEventStoreChanged + wake notification) and an external MCP wrapper around the CLI queued next; decided against direct Google/CalDAV API integration since EventKit already covers it via System Settings accounts. All 47 unit tests pass (45 existing + 2 new SettingsPayloadTests). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,14 +5,15 @@ final class BlockMathTests: XCTestCase {
|
||||
|
||||
private let d = Date(timeIntervalSince1970: 0)
|
||||
|
||||
private func block(_ startMin: Int, _ endMin: Int, id: String? = nil) -> Block {
|
||||
private func block(_ startMin: Int, _ endMin: Int, id: String? = nil, alarmOffsets: [TimeInterval]? = nil) -> Block {
|
||||
Block(
|
||||
start: d.addingTimeInterval(TimeInterval(startMin * 60)),
|
||||
end: d.addingTimeInterval(TimeInterval(endMin * 60)),
|
||||
srcStableID: id,
|
||||
label: nil,
|
||||
notes: nil,
|
||||
occurrence: nil
|
||||
occurrence: nil,
|
||||
alarmOffsets: alarmOffsets
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,6 +45,21 @@ final class BlockMathTests: XCTestCase {
|
||||
XCTAssertTrue(mergeBlocks([], gapMinutes: 10).isEmpty)
|
||||
}
|
||||
|
||||
func testMergeBlocksPreservesFirstAlarms() {
|
||||
let b1 = block(0, 10, alarmOffsets: [-900, -3600])
|
||||
let b2 = block(10, 20, alarmOffsets: [-600])
|
||||
let merged = mergeBlocks([b1, b2], gapMinutes: 0)
|
||||
XCTAssertEqual(merged.count, 1)
|
||||
XCTAssertEqual(merged[0].alarmOffsets, [-900, -3600])
|
||||
}
|
||||
|
||||
func testBlockEqualityIgnoresAlarms() {
|
||||
let b1 = block(0, 10, alarmOffsets: [-900])
|
||||
let b2 = block(0, 10, alarmOffsets: [-1800])
|
||||
XCTAssertEqual(b1, b2)
|
||||
XCTAssertEqual(Set([b1, b2]).count, 1)
|
||||
}
|
||||
|
||||
func testMergeBlocksUnsortedInput() {
|
||||
let blocks = [block(30, 40), block(0, 10), block(10, 20)]
|
||||
let merged = mergeBlocks(blocks, gapMinutes: 0)
|
||||
@@ -150,8 +166,8 @@ final class BlockMathTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testUniqueBlocksByIDDifferentOccurrence() {
|
||||
let b1 = Block(start: d, end: d.addingTimeInterval(600), srcStableID: "a", label: nil, notes: nil, occurrence: d)
|
||||
let b2 = Block(start: d, end: d.addingTimeInterval(600), srcStableID: "a", label: nil, notes: nil, occurrence: d.addingTimeInterval(3600))
|
||||
let b1 = Block(start: d, end: d.addingTimeInterval(600), srcStableID: "a", label: nil, notes: nil, occurrence: d, alarmOffsets: nil)
|
||||
let b2 = Block(start: d, end: d.addingTimeInterval(600), srcStableID: "a", label: nil, notes: nil, occurrence: d.addingTimeInterval(3600), alarmOffsets: nil)
|
||||
let result = uniqueBlocks([b1, b2], trackByID: true)
|
||||
XCTAssertEqual(result.count, 2)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user