diff --git a/.github/scripts/fixtures/swift-test-xctest-failure-43186705883-sample.txt b/.github/scripts/fixtures/swift-test-xctest-failure-43186705883-sample.txt new file mode 100644 index 0000000000..9af9f07659 --- /dev/null +++ b/.github/scripts/fixtures/swift-test-xctest-failure-43186705883-sample.txt @@ -0,0 +1,11 @@ +Test Suite 'FocusedEventShiftNudgeTests' failed at 2026-11-12 08:59:44.322. +Test Suite 'NativeCalendarRootModelAllDayTests' failed at 2026-11-12 08:59:51.205. +Test Suite 'NativeCalendarRootModelInitialWeekAnchorTests' failed at 2026-11-12 09:00:02.882. +Test Suite 'CompassDataPackageTests.xctest' failed at 2026-11-12 09:00:29.633. +Test Suite 'All tests' failed at 2026-11-12 09:00:29.633. + Executed 327 tests, with 3 tests skipped and 6 failures (0 unexpected) in 55.567 (55.666) seconds + Executed 327 tests, with 3 tests skipped and 6 failures (0 unexpected) in 55.567 (55.671) seconds +◇ Test run started. +↳ Testing Library Version: 1902 +✗ ExampleSuite.example() failed after 0.001 seconds +✔ Test run with 0 tests in 0 suites passed after 0.001 seconds. diff --git a/.github/scripts/selftest-swift-packages-exit.sh b/.github/scripts/selftest-swift-packages-exit.sh new file mode 100644 index 0000000000..17b406f903 --- /dev/null +++ b/.github/scripts/selftest-swift-packages-exit.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Verify swift-packages log parsing catches XCTest failures even when swift exits 0. +set -euo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +# shellcheck source=swift-packages-log-detect.sh +source "${ROOT}/.github/scripts/swift-packages-log-detect.sh" + +fixture="${ROOT}/.github/scripts/fixtures/swift-test-xctest-failure-43186705883-sample.txt" +if ! swift_package_log_has_test_failures "$fixture"; then + echo "selftest: expected fixture log to count as failures" >&2 + exit 1 +fi + +pass_log=$(mktemp) +trap 'rm -f "$pass_log"' EXIT +cat >"$pass_log" <<'EOF' +Test Suite 'All tests' passed at 2026-01-01 00:00:00.000. + Executed 12 tests, with 0 failures (0 unexpected) in 0.010 (0.012) seconds +◇ Test run started. +↳ Testing Library Version: 1902 +✔ Test run with 0 tests in 0 suites passed after 0.001 seconds. +compiled with 3 issues in other contexts +EOF +if swift_package_log_has_test_failures "$pass_log"; then + echo "selftest: pass log must not count as failures" >&2 + exit 1 +fi + +skipped_pass_log=$(mktemp) +trap 'rm -f "$pass_log" "$skipped_pass_log"' EXIT +cat >"$skipped_pass_log" <<'EOF' +Test Suite 'All tests' passed at 2026-01-01 00:00:00.000. + Executed 100 tests, with 3 tests skipped and 0 failures (0 unexpected) in 1.0 (1.1) seconds +EOF +if swift_package_log_has_test_failures "$skipped_pass_log"; then + echo "selftest: skipped-only summary must not count as failures" >&2 + exit 1 +fi + +echo "selftest-swift-packages-exit: ok" diff --git a/.github/scripts/swift-packages-log-detect.sh b/.github/scripts/swift-packages-log-detect.sh new file mode 100644 index 0000000000..d771433d25 --- /dev/null +++ b/.github/scripts/swift-packages-log-detect.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Shared swift-packages XCTest / Swift Testing failure detection (sourced, not executed). +swift_package_log_has_test_failures() { + local log="$1" + # XCTest aggregate line, including "with N tests skipped and M failures". + if grep -qE 'Executed [0-9]+ tests?, with ([0-9]+ tests? skipped and )?[1-9][0-9]* failures' "$log"; then + return 0 + fi + if grep -qE "Test Suite 'All tests' failed" "$log"; then + return 0 + fi + if grep -qE "Test Suite 'CompassDataPackageTests.xctest' failed" "$log"; then + return 0 + fi + # Swift Testing failed runs (exclude "0 tests" pass summaries). + if grep -qE 'Test run with [1-9][0-9]* tests.*failed' "$log"; then + return 0 + fi + if grep -qE '✗.*failed' "$log"; then + return 0 + fi + return 1 +} diff --git a/.github/scripts/test-macos-swift-packages.sh b/.github/scripts/test-macos-swift-packages.sh index 36f451704f..2c69c9cd98 100755 --- a/.github/scripts/test-macos-swift-packages.sh +++ b/.github/scripts/test-macos-swift-packages.sh @@ -7,6 +7,11 @@ export NSUnbufferedIO=YES ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) cd "$ROOT" +# shellcheck source=swift-packages-log-detect.sh +source "${ROOT}/.github/scripts/swift-packages-log-detect.sh" + +bash "${ROOT}/.github/scripts/selftest-swift-packages-exit.sh" + # CompassKitTests in `swift test --package-path CompassKit` mirror the # `apps/calendar-macos/CompassKitTests/` tree (diff -rq reports no differences). # macos-build already runs those cases via xcodebuild -only-testing:CompassKitTests @@ -47,8 +52,24 @@ run_package_tests() { echo "── swift ${args[*]} ──" local start start=$(date +%s) - swift "${args[@]}" + local log + log=$(mktemp) + local swift_ec=0 + set +e + swift "${args[@]}" 2>&1 | tee "$log" + swift_ec=${PIPESTATUS[0]} + set -e + local parsed_fail=0 + if swift_package_log_has_test_failures "$log"; then + echo "swift-packages: ${name} reported test failures in log (swift exit ${swift_ec})" >&2 + parsed_fail=1 + fi + rm -f "$log" echo "── ${name} tests finished in $(( $(date +%s) - start ))s ──" + if [ "$swift_ec" -ne 0 ] || [ "$parsed_fail" -ne 0 ]; then + return 1 + fi + return 0 } job_start=$(date +%s) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 4d2bbf6887..8eeef20f43 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -202,7 +202,7 @@ public final class NativeCalendarRootModel { ? CompassDevicePreferences.readPinnedTimeZoneResolvingAccidentalUtc() : nil EffectiveTimeZone.identifier = - demoTimeZone ?? pinnedTimeZone ?? TimeZone.current.identifier + demoTimeZone ?? pinnedTimeZone ?? CompassDeviceTimeZone.currentIdentifier eventsStore = environment.eventsStore hiddenEventsStore = environment.hiddenEventsStore calendarVisibilityStore = CalendarVisibilityStore() @@ -229,7 +229,7 @@ public final class NativeCalendarRootModel { anchorDate: anchor, visibleDayCount: CalendarWindowMath.weekDayCount, pinnedTimeZone: pinnedTimeZone, - browserTimeZone: demoTimeZone ?? TimeZone.current.identifier + browserTimeZone: demoTimeZone ?? CompassDeviceTimeZone.currentIdentifier ) if demoPresentation == nil { viewStore.setTimeTravelTimeZone(CompassDevicePreferences.readTimeTravelTimeZone()) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/Preferences/CompassDeviceTimeZone.swift b/apps/calendar-macos/CompassData/Sources/CompassData/Preferences/CompassDeviceTimeZone.swift new file mode 100644 index 0000000000..9b11c56e18 --- /dev/null +++ b/apps/calendar-macos/CompassData/Sources/CompassData/Preferences/CompassDeviceTimeZone.swift @@ -0,0 +1,20 @@ +import Foundation + +/// Device (browser) timezone at app bootstrap, overridable in tests without touching globals. +public enum CompassDeviceTimeZone { + private nonisolated(unsafe) static var identifierProvider: () -> String = { + TimeZone.current.identifier + } + + public static var currentIdentifier: String { + identifierProvider() + } + + public static func setIdentifierProviderForTests(_ provider: @escaping () -> String) { + identifierProvider = provider + } + + public static func resetIdentifierProviderForTests() { + identifierProvider = { TimeZone.current.identifier } + } +} diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelAllDayTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelAllDayTests.swift index 9520ef74cd..861d58d767 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelAllDayTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelAllDayTests.swift @@ -36,12 +36,46 @@ final class NativeCalendarRootModelAllDayTests: XCTestCase { EffectiveTimeZone.calendar.date(from: DateComponents(year: 2026, month: 3, day: day))! } + private func denverPrimaryCalendar() -> CompassCalendar { + CompassCalendar( + access: .owner, + accountEmail: "a@example.com", + backgroundColor: "#ffffff", + capabilities: .init( + canInviteAttendees: false, + canManage: false, + canReadAvailability: true, + canReadDetails: true, + canWatchEvents: false, + canWrite: true, + conferenceKinds: [] + ), + conference: nil, + createsGoogleMeet: false, + description: "", + foregroundColor: "#000000", + id: "denver-primary", + isActive: true, + isPrimary: true, + isVisible: true, + name: "Primary", + provider: "google", + timeZone: IANATimeZone(rawValue: "America/Denver") + ) + } + private func model(startDay: Int, endDay: Int) throws -> NativeCalendarRootModel { + CompassDeviceTimeZone.setIdentifierProviderForTests { "GMT" } let model = NativeCalendarRootModel(environment: try NativeCalendarEnvironment(inMemoryDatabase: true)) - EffectiveTimeZone.identifier = "America/Denver" + model.applyCalendarsForPresentationTesting([denverPrimaryCalendar()]) model.draftStore.startGridDraft(activity: .keyboardPlace, draft: GridEventDraft( clientId: .init(rawValue: "all-day"), schedule: .init(start: date(startDay), end: date(endDay), kind: .allDay), - calendarId: .init(rawValue: "cal-1"))) + calendarId: .init(rawValue: "denver-primary"))) return model } + + override func tearDown() async throws { + CompassDeviceTimeZone.resetIdentifierProviderForTests() + try await super.tearDown() + } } diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelInitialWeekAnchorTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelInitialWeekAnchorTests.swift index f295dbc0a7..95ddc026f4 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelInitialWeekAnchorTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelInitialWeekAnchorTests.swift @@ -4,22 +4,52 @@ import XCTest @MainActor final class NativeCalendarRootModelInitialWeekAnchorTests: XCTestCase { + override func tearDown() async throws { + CompassDeviceTimeZone.resetIdentifierProviderForTests() + _ = CompassDevicePreferences.writePinnedTimeZone(nil) + try await super.tearDown() + } + func testInitialWeekAnchorUsesDeviceZoneBeforeViewStore() throws { - let priorDefault = NSTimeZone.default - defer { NSTimeZone.default = priorDefault } - NSTimeZone.default = try XCTUnwrap(TimeZone(identifier: "America/Denver")) + CompassDeviceTimeZone.setIdentifierProviderForTests { "America/Denver" } + XCTAssertTrue(CompassDevicePreferences.writePinnedTimeZone(nil)) + // Monday 06:00 UTC is Monday 23:00 MST; local day differs from GMT so week anchors diverge. let referenceNow = try XCTUnwrap( CompassDateParsing.parseGridDateTime( - "2026-11-12T01:30:00.000", + "2026-11-10T06:00:00.000", timeZoneIdentifier: "UTC" ) ) let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) let model = NativeCalendarRootModel(environment: environment, now: { referenceNow }) - let expected = CalendarWindowMath.sundayWeekViewStart(containing: referenceNow) - XCTAssertEqual(model.viewStore.anchorDate, expected) XCTAssertEqual(EffectiveTimeZone.identifier, "America/Denver") + let denverAnchor = CalendarWindowMath.sundayWeekViewStart(containing: referenceNow) + XCTAssertEqual(model.viewStore.anchorDate, denverAnchor) + + EffectiveTimeZone.identifier = "GMT" + let gmtAnchor = CalendarWindowMath.sundayWeekViewStart(containing: referenceNow) + XCTAssertNotEqual(denverAnchor, gmtAnchor) + } + + func testInitialWeekAnchorHonorsPinnedZoneOverDevice() throws { + CompassDeviceTimeZone.setIdentifierProviderForTests { "GMT" } + XCTAssertTrue(CompassDevicePreferences.writePinnedTimeZone("America/Chicago")) + + let referenceNow = try XCTUnwrap( + CompassDateParsing.parseGridDateTime( + "2026-06-15T12:00:00.000", + timeZoneIdentifier: "UTC" + ) + ) + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, now: { referenceNow }) + + XCTAssertEqual(EffectiveTimeZone.identifier, "America/Chicago") + XCTAssertEqual( + model.viewStore.anchorDate, + CalendarWindowMath.sundayWeekViewStart(containing: referenceNow) + ) } } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift index d78004aab4..4dda3168a3 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift @@ -57,6 +57,7 @@ public struct EventFormView: View { model: model, draft: draft ) + .id(EventFormField.recurrence) .disabled(model.isScheduleLocked(for: draft)) } }