@@ -11,6 +11,17 @@ app.setActivationPolicy(.prohibited)
1111
1212let runner = TestRunner ( )
1313
14+ @ MainActor
15+ final class RecordingDiscovery : UpdateDiscoveryStarting {
16+ var calls : [ String ] = [ ]
17+ var failStart = false
18+ func start ( ) throws {
19+ calls . append ( " start " )
20+ if failStart { throw NSError ( domain : " test.discovery " , code : 1 ) }
21+ }
22+ func checkForUpdatesInBackground ( ) { calls . append ( " background " ) }
23+ }
24+
1425final class ApplicationMenuTarget : NSObject {
1526 @ objc func stopCodexCommanderAndQuit ( _ sender : Any ? ) { }
1627}
@@ -1890,7 +1901,7 @@ MainActor.assumeIsolated {
18901901 decide ? ( . install )
18911902 runner . equal ( disclosures , 1 , " retained reply consumed once " )
18921903 driver . dismissUpdateInstallation ( )
1893- runner . equal ( driver . updateAvailable , false )
1904+ runner . equal ( driver . updateAvailable , true , " dismissal preserves the known release " )
18941905 }
18951906
18961907 runner . test ( " updater UI: Sparkle scheduled offers stay hidden, including critical releases; manual offers open " ) {
@@ -1923,8 +1934,89 @@ MainActor.assumeIsolated {
19231934 " user action focuses the retained scheduled offer " )
19241935 }
19251936 driver . dismissUpdateInstallation ( )
1926- runner . equal ( driver . updateAvailable , false )
1937+ runner . equal ( driver . updateAvailable , true , " dismissal preserves the known release " )
1938+ }
1939+ }
1940+ }
1941+
1942+ runner . test ( " updater UI: launch discovery starts quietly only after successful updater startup " ) {
1943+ let updater = RecordingDiscovery ( )
1944+ try! AppUpdater . startDiscovery ( updater )
1945+ runner . equal ( updater . calls , [ " start " , " background " ] )
1946+ let failed = RecordingDiscovery ( )
1947+ failed . failStart = true
1948+ do { try AppUpdater . startDiscovery ( failed ) ; runner . expect ( false , " start must fail " ) } catch { }
1949+ runner . equal ( failed . calls , [ " start " ] )
1950+ let source = URL ( fileURLWithPath : #filePath ) . deletingLastPathComponent ( ) . deletingLastPathComponent ( ) . deletingLastPathComponent ( ) . appendingPathComponent ( " Info.plist " )
1951+ let info = try! PropertyListSerialization . propertyList ( from : Data ( contentsOf : source ) , format : nil ) as! [ String : Any ]
1952+ runner . equal ( info [ " SUScheduledCheckInterval " ] as? Int , 21600 )
1953+ runner . equal ( info [ " SUEnableAutomaticChecks " ] as? Bool , true )
1954+ }
1955+
1956+ runner . test ( " updater UI: known release survives network errors but authoritative no-update clears it " ) {
1957+ let driver = UpdateController ( hostBundle : . main , boundary : . init (
1958+ prepare : { _ in false } , persistArmed : { _ in } , cancelPreparation : { true } ,
1959+ stateChanged : { _ in } ) , confirmInstallation : { false } )
1960+ driver . reconcileStartup ( installerDisarmed : true )
1961+ driver . handleOffer ( target : " 2 " , stage : . notDownloaded , present : { $0 ( . dismiss ) } , reply : { _ in } )
1962+ driver . dismissUpdateInstallation ( )
1963+ driver . showUpdaterError ( NSError ( domain : " test.offline " , code : 1 ) ) { }
1964+ runner . equal ( driver . availableVersion , " 2 " )
1965+ driver . didNotFindEligibleUpdate ( )
1966+ runner . equal ( driver . updateAvailable , false , " background no-update result clears without UI " )
1967+ driver . handleOffer ( target : " 2 " , stage : . notDownloaded , present : { $0 ( . dismiss ) } , reply : { _ in } )
1968+ driver . dismissUpdateInstallation ( )
1969+ driver . showUpdateNotFoundWithError ( NSError ( domain : " test.no-update " , code : 1 ) ) { }
1970+ runner . equal ( driver . updateAvailable , false )
1971+ }
1972+
1973+ runner . test ( " updater UI: installed and older builds do not advertise availability " ) {
1974+ let folder = FileManager . default . temporaryDirectory . appendingPathComponent ( UUID ( ) . uuidString + " .app " )
1975+ let contents = folder . appendingPathComponent ( " Contents " )
1976+ try! FileManager . default . createDirectory ( at : contents , withIntermediateDirectories : true )
1977+ let info : [ String : Any ] = [ " CFBundleIdentifier " : " test.reminder. " + UUID ( ) . uuidString ,
1978+ " CFBundleName " : " Reminder test " , " CFBundleVersion " : " 20 " ]
1979+ try! PropertyListSerialization . data ( fromPropertyList : info , format : . xml , options : 0 ) . write ( to : contents . appendingPathComponent ( " Info.plist " ) )
1980+ defer { try? FileManager . default . removeItem ( at : folder ) }
1981+ let driver = UpdateController ( hostBundle : Bundle ( url : folder ) ! , boundary : . init (
1982+ prepare : { _ in false } , persistArmed : { _ in } , cancelPreparation : { true } , stateChanged : { _ in } ) )
1983+ driver . reconcileStartup ( installerDisarmed : true )
1984+ for ( version , available ) in [ ( " 21 " , true ) , ( " 20 " , false ) , ( " 9 " , false ) ] {
1985+ driver . handleOffer ( target : version , stage : . notDownloaded , present : { $0 ( . dismiss ) } , reply : { _ in } )
1986+ driver . dismissUpdateInstallation ( )
1987+ runner . equal ( driver . updateAvailable , available )
1988+ }
1989+ }
1990+
1991+ runner . test ( " updater UI: real Sparkle Later and close preserve reminder while Skip clears it " ) {
1992+ for action in [ " remindMeLater: " , " close " , " skipThisVersion: " ] {
1993+ let driver = UpdateController ( hostBundle : . main , boundary : . init (
1994+ prepare : { _ in fatalError ( " unsolicited preparation " ) } , persistArmed : { _ in } ,
1995+ cancelPreparation : { true } , stateChanged : { _ in } ) , confirmInstallation : { false } )
1996+ driver . reconcileStartup ( installerDisarmed : true )
1997+ let archive = NSKeyedArchiver ( requiringSecureCoding : true )
1998+ archive . encode ( 0 , forKey : " SPUUserUpdateStateStage " )
1999+ archive . encode ( true , forKey : " SPUUserUpdateStateUserInitiated " )
2000+ archive . finishEncoding ( )
2001+ let decoder = try! NSKeyedUnarchiver ( forReadingFrom : archive . encodedData )
2002+ let state = SPUUserUpdateState ( coder : decoder ) !
2003+ decoder . finishDecoding ( )
2004+ let item = SUAppcastItem ( dictionary : [ " enclosure " : [ " url " : " https://example.test/2.zip " , " sparkle:version " : " 2 " ] ] ) !
2005+ let existing = Set ( NSApp . windows . map ( ObjectIdentifier . init ) )
2006+ var choice : SPUUserUpdateChoice ?
2007+ driver . showUpdateFound ( with : item , state : state ) { choice = $0 }
2008+ let window = NSApp . windows . first { ! existing . contains ( ObjectIdentifier ( $0 ) ) && $0 . isVisible }
2009+ runner . expect ( window != nil , " real Sparkle offer is visible " )
2010+ if action == " close " { window ? . performClose ( nil ) }
2011+ else {
2012+ // Invoke Sparkle's actual button action even if this test host hides Later.
2013+ let target = window ? . windowController
2014+ runner . expect ( target ? . responds ( to : NSSelectorFromString ( action ) ) == true , " Sparkle action exists " )
2015+ _ = target ? . perform ( NSSelectorFromString ( action ) , with : nil )
19272016 }
2017+ runner . equal ( choice , action == " skipThisVersion: " ? . skip : . dismiss )
2018+ driver . dismissUpdateInstallation ( )
2019+ runner . equal ( driver . updateAvailable , action != " skipThisVersion: " )
19282020 }
19292021 }
19302022
0 commit comments