@@ -5,30 +5,12 @@ import {
55 getUpcomingRelativeLabel ,
66} from './utilities' ;
77
8- import type { AccountMaintenance , MaintenancePolicy } from '@linode/api-v4' ;
8+ import type { AccountMaintenance } from '@linode/api-v4' ;
99
1010// Freeze time to a stable reference so relative labels are deterministic
1111const NOW_ISO = '2025-10-27T12:00:00.000Z' ;
1212
1313describe ( 'Account Maintenance utilities' , ( ) => {
14- const policies : MaintenancePolicy [ ] = [
15- {
16- description : 'Migrate' ,
17- is_default : true ,
18- label : 'Migrate' ,
19- notification_period_sec : 3 * 60 * 60 , // 3 hours
20- slug : 'linode/migrate' ,
21- type : 'linode_migrate' ,
22- } ,
23- {
24- description : 'Power Off / On' ,
25- is_default : false ,
26- label : 'Power Off / Power On' ,
27- notification_period_sec : 72 * 60 * 60 , // 72 hours
28- slug : 'linode/power_off_on' ,
29- type : 'linode_power_off_on' ,
30- } ,
31- ] ;
3214
3315 const baseMaintenance : Omit < AccountMaintenance , 'when' > & { when : string } = {
3416 complete_time : null ,
@@ -60,33 +42,33 @@ describe('Account Maintenance utilities', () => {
6042 ...baseMaintenance ,
6143 start_time : '2025-10-27T12:00:00.000Z' ,
6244 } ;
63- expect ( deriveMaintenanceStartISO ( m , policies ) ) . toBe (
45+ expect ( deriveMaintenanceStartISO ( m ) ) . toBe (
6446 '2025-10-27T12:00:00.000Z'
6547 ) ;
6648 } ) ;
6749
68- it ( 'derives start_time from when + policy seconds when missing ' , ( ) => {
50+ it ( 'uses when directly as start time ( when already accounts for notification period) ' , ( ) => {
6951 const m : AccountMaintenance = {
7052 ...baseMaintenance ,
7153 start_time : null ,
72- when : '2025-10-27T09:00:00.000Z' , // +3h -> 12:00Z
54+ when : '2025-10-27T09:00:00.000Z' ,
7355 } ;
74- expect ( deriveMaintenanceStartISO ( m , policies ) ) . toBe (
75- '2025-10-27T12:00:00.000Z'
56+ // `when` already accounts for notification_period_sec, so it IS the start time
57+ expect ( deriveMaintenanceStartISO ( m ) ) . toBe (
58+ '2025-10-27T09:00:00.000Z'
7659 ) ;
7760 } ) ;
7861
79- it ( 'returns undefined when policy cannot be found ' , ( ) => {
62+ it ( 'uses when directly for all statuses without needing policies ' , ( ) => {
8063 const m : AccountMaintenance = {
8164 ...baseMaintenance ,
8265 start_time : null ,
83- // Use an intentionally unknown slug to exercise the no-policy fallback path.
84- // Even though the API default is typically 'linode/migrate', the client may
85- // not have policies loaded yet or could encounter a fetch error; this ensures
86- // we verify the graceful fallback behavior.
66+ status : 'pending' ,
67+ // Policies not needed - when IS the start time
8768 maintenance_policy_set : 'unknown/policy' as any ,
69+ when : '2025-10-27T09:00:00.000Z' ,
8870 } ;
89- expect ( deriveMaintenanceStartISO ( m , policies ) ) . toBeUndefined ( ) ;
71+ expect ( deriveMaintenanceStartISO ( m ) ) . toBe ( '2025-10-27T09:00:00.000Z' ) ;
9072 } ) ;
9173 } ) ;
9274
@@ -100,7 +82,7 @@ describe('Account Maintenance utilities', () => {
10082 when : '2025-10-27T10:00:00.000Z' ,
10183 } ;
10284 // NOW=12:00Z, when=10:00Z => "2 hours ago"
103- expect ( getUpcomingRelativeLabel ( m , policies ) ) . toContain ( 'hour' ) ;
85+ expect ( getUpcomingRelativeLabel ( m ) ) . toContain ( 'hour' ) ;
10486 } ) ;
10587
10688 it ( 'uses derived start to express time until maintenance (hours when <1 day)' , ( ) => {
@@ -110,16 +92,15 @@ describe('Account Maintenance utilities', () => {
11092 when : '2025-10-27T09:00:00.000Z' ,
11193 } ;
11294 // Allow any non-empty string; exact phrasing depends on Luxon locale
113- expect ( getUpcomingRelativeLabel ( m , policies ) ) . toBeTypeOf ( 'string' ) ;
95+ expect ( getUpcomingRelativeLabel ( m ) ) . toBeTypeOf ( 'string' ) ;
11496 } ) ;
11597
11698 it ( 'shows days+hours when >= 1 day away (avoids day-only rounding)' , ( ) => {
11799 const m : AccountMaintenance = {
118100 ...baseMaintenance ,
119- maintenance_policy_set : 'linode/power_off_on' , // 72h
120- when : '2025-10-25T20:00:00.000Z' , // +72h => 2025-10-28T20:00Z; from NOW (27 12:00Z) => 1 day 8 hours
101+ when : '2025-10-28T20:00:00.000Z' , // from NOW (27 12:00Z) => 1 day 8 hours
121102 } ;
122- const label = getUpcomingRelativeLabel ( m , policies ) ;
103+ const label = getUpcomingRelativeLabel ( m ) ;
123104 expect ( label ) . toBe ( 'in 1 day 8 hours' ) ;
124105 } ) ;
125106
@@ -129,7 +110,7 @@ describe('Account Maintenance utilities', () => {
129110 ...baseMaintenance ,
130111 start_time : '2025-10-30T04:00:00.000Z' ,
131112 } ;
132- const label = getUpcomingRelativeLabel ( m , policies ) ;
113+ const label = getUpcomingRelativeLabel ( m ) ;
133114 expect ( label ) . toBe ( 'in 2 days 16 hours' ) ;
134115 } ) ;
135116
@@ -139,7 +120,7 @@ describe('Account Maintenance utilities', () => {
139120 // NOW is 12:00Z; start in 37 minutes
140121 start_time : '2025-10-27T12:37:00.000Z' ,
141122 } ;
142- const label = getUpcomingRelativeLabel ( m , policies ) ;
123+ const label = getUpcomingRelativeLabel ( m ) ;
143124 expect ( label ) . toBe ( 'in 37 minutes' ) ;
144125 } ) ;
145126
@@ -149,8 +130,59 @@ describe('Account Maintenance utilities', () => {
149130 // NOW is 12:00Z; start in 30 seconds
150131 start_time : '2025-10-27T12:00:30.000Z' ,
151132 } ;
152- const label = getUpcomingRelativeLabel ( m , policies ) ;
133+ const label = getUpcomingRelativeLabel ( m ) ;
153134 expect ( label ) . toBe ( 'in 30 seconds' ) ;
154135 } ) ;
136+
137+ it ( 'uses when directly as start time (when already accounts for notification period)' , ( ) => {
138+ // Real-world scenario: API returns when=2025-11-06T16:12:41
139+ // `when` already accounts for notification_period_sec, so it IS the start time
140+ const m : AccountMaintenance = {
141+ ...baseMaintenance ,
142+ start_time : null ,
143+ when : '2025-11-06T16:12:41' , // No timezone indicator, should be parsed as UTC
144+ } ;
145+
146+ const derivedStart = deriveMaintenanceStartISO ( m ) ;
147+ // `when` equals start time (no addition needed)
148+ expect ( derivedStart ) . toBe ( '2025-11-06T16:12:41.000Z' ) ;
149+ } ) ;
150+
151+ it ( 'shows correct relative time (when equals start)' , ( ) => {
152+ // Scenario: when=2025-11-06T16:12:41 (when IS the start time)
153+ // If now is 2025-11-06T16:14:41 (2 minutes after when), should show "2 minutes ago"
154+ // Save original Date.now
155+ const originalDateNow = Date . now ;
156+
157+ // Mock "now" to be 2 minutes after when (which is the start time)
158+ const mockNow = '2025-11-06T16:14:41.000Z' ;
159+ Date . now = vi . fn ( ( ) => new Date ( mockNow ) . getTime ( ) ) ;
160+
161+ const m : AccountMaintenance = {
162+ ...baseMaintenance ,
163+ start_time : null ,
164+ when : '2025-11-06T16:12:41' ,
165+ } ;
166+
167+ const label = getUpcomingRelativeLabel ( m ) ;
168+ // when=start=16:12:41, now=16:14:41, difference is 2 minutes in the past
169+ expect ( label ) . toContain ( 'minute' ) ; // Should show "2 minutes ago" or similar
170+
171+ // Restore original Date.now
172+ Date . now = originalDateNow ;
173+ } ) ;
174+
175+ it ( 'handles date without timezone indicator correctly (parsed as UTC)' , ( ) => {
176+ // Verify that dates without timezone are parsed as UTC
177+ const m : AccountMaintenance = {
178+ ...baseMaintenance ,
179+ start_time : null ,
180+ when : '2025-11-06T16:12:41' , // No Z suffix or timezone
181+ } ;
182+
183+ const derivedStart = deriveMaintenanceStartISO ( m ) ;
184+ // `when` equals start time (no addition needed)
185+ expect ( derivedStart ) . toBe ( '2025-11-06T16:12:41.000Z' ) ;
186+ } ) ;
155187 } ) ;
156188} ) ;
0 commit comments