@@ -167,17 +167,8 @@ describe('activateWithServer', () => {
167167 vi . unstubAllGlobals ( ) ;
168168 } ) ;
169169
170- it ( 'applies grace period when server is unreachable and within 7 days ' , async ( ) => {
170+ it ( 'falls back to regex: returns pro_editor when server unreachable and key format is valid ' , async ( ) => {
171171 vi . useFakeTimers ( { shouldAdvanceTime : true } ) ;
172- const now = Date . now ( ) ;
173- const token = buildTestJwt ( {
174- product : 'pro_editor' ,
175- exp : pastUnix ( 1 ) , // expired
176- } ) ;
177- mockSecrets . set ( 'as-notes.activationToken' , token ) ;
178- mockSecrets . set ( 'as-notes.licenceKey' , VALID_KEY ) ;
179- mockSecrets . set ( 'as-notes.lastValidated' , ( now - 2 * 86_400_000 ) . toString ( ) ) ; // 2 days ago
180- mockSecrets . set ( 'as-notes.licenceState' , JSON . stringify ( { status : 'valid' , product : 'pro_editor' } ) ) ;
181172
182173 vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockRejectedValue ( new Error ( 'Network error' ) ) ) ;
183174
@@ -187,29 +178,28 @@ describe('activateWithServer', () => {
187178 const result = await resultPromise ;
188179 expect ( result . status ) . toBe ( 'valid' ) ;
189180 expect ( result . product ) . toBe ( 'pro_editor' ) ;
181+ expect ( result . serverUnreachable ) . toBe ( true ) ;
182+
183+ // Verify state was persisted
184+ expect ( mockSecrets . get ( 'as-notes.licenceKey' ) ) . toBe ( VALID_KEY ) ;
185+ const persisted = JSON . parse ( mockSecrets . get ( 'as-notes.licenceState' ) ! ) ;
186+ expect ( persisted . status ) . toBe ( 'valid' ) ;
187+ expect ( persisted . product ) . toBe ( 'pro_editor' ) ;
190188
191189 vi . unstubAllGlobals ( ) ;
192190 vi . useRealTimers ( ) ;
193191 } ) ;
194192
195- it ( 'returns not-entered when grace period exceeded ' , async ( ) => {
193+ it ( 'falls back to regex: returns not-entered when server unreachable and key format is invalid ' , async ( ) => {
196194 vi . useFakeTimers ( { shouldAdvanceTime : true } ) ;
197- const now = Date . now ( ) ;
198- const token = buildTestJwt ( {
199- product : 'pro_editor' ,
200- exp : pastUnix ( 1 ) ,
201- } ) ;
202- mockSecrets . set ( 'as-notes.activationToken' , token ) ;
203- mockSecrets . set ( 'as-notes.licenceKey' , VALID_KEY ) ;
204- mockSecrets . set ( 'as-notes.lastValidated' , ( now - 8 * 86_400_000 ) . toString ( ) ) ; // 8 days ago
205- mockSecrets . set ( 'as-notes.licenceState' , JSON . stringify ( { status : 'valid' , product : 'pro_editor' } ) ) ;
206195
207196 vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockRejectedValue ( new Error ( 'Network error' ) ) ) ;
208197
209- const resultPromise = activateWithServer ( VALID_KEY , buildContext ( ) as any ) ;
198+ const resultPromise = activateWithServer ( 'ASNO-ZZZZ-ZZZZ-ZZZZ-ZZZZ' , buildContext ( ) as any ) ;
210199 await vi . advanceTimersByTimeAsync ( 25_000 ) ;
211200 const result = await resultPromise ;
212- expect ( result . status ) . toBe ( 'not-entered' ) ; // falls back to default
201+ // Invalid format returns 'invalid' before reaching the server, so this never hits the fallback
202+ expect ( result . status ) . toBe ( 'invalid' ) ;
213203 expect ( result . product ) . toBeNull ( ) ;
214204
215205 vi . unstubAllGlobals ( ) ;
@@ -284,13 +274,12 @@ describe('validateWithServer', () => {
284274 vi . unstubAllGlobals ( ) ;
285275 } ) ;
286276
287- it ( 'applies grace period when server unreachable during validation' , async ( ) => {
277+ it ( 'falls back to regex: returns pro_editor when server unreachable during validation' , async ( ) => {
288278 vi . useFakeTimers ( { shouldAdvanceTime : true } ) ;
289- const now = Date . now ( ) ;
290279 const token = buildTestJwt ( { product : 'pro_editor' , exp : futureUnix ( 365 ) } ) ;
291280 mockSecrets . set ( 'as-notes.activationToken' , token ) ;
292281 mockSecrets . set ( 'as-notes.licenceKey' , VALID_KEY ) ;
293- mockSecrets . set ( 'as-notes.lastValidated' , ( now - 86_400_000 ) . toString ( ) ) ; // 1 day ago
282+ mockSecrets . set ( 'as-notes.lastValidated' , ( Date . now ( ) - 86_400_000 ) . toString ( ) ) ;
294283 mockSecrets . set ( 'as-notes.licenceState' , JSON . stringify ( { status : 'valid' , product : 'pro_editor' } ) ) ;
295284
296285 vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockRejectedValue ( new Error ( 'Network error' ) ) ) ;
@@ -300,8 +289,36 @@ describe('validateWithServer', () => {
300289 const result = await resultPromise ;
301290 expect ( result . status ) . toBe ( 'valid' ) ;
302291 expect ( result . product ) . toBe ( 'pro_editor' ) ;
292+ expect ( result . serverUnreachable ) . toBe ( true ) ;
293+
294+ // Verify state was persisted
295+ const persisted = JSON . parse ( mockSecrets . get ( 'as-notes.licenceState' ) ! ) ;
296+ expect ( persisted . status ) . toBe ( 'valid' ) ;
297+ expect ( persisted . product ) . toBe ( 'pro_editor' ) ;
303298
304299 vi . unstubAllGlobals ( ) ;
305300 vi . useRealTimers ( ) ;
306301 } ) ;
302+
303+ it ( 'persisted fallback state survives restart via loadCachedLicenceState' , async ( ) => {
304+ vi . useFakeTimers ( { shouldAdvanceTime : true } ) ;
305+
306+ vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockRejectedValue ( new Error ( 'Network error' ) ) ) ;
307+
308+ // Activate with server unreachable — fallback persists state
309+ const activatePromise = activateWithServer ( VALID_KEY , buildContext ( ) as any ) ;
310+ await vi . advanceTimersByTimeAsync ( 25_000 ) ;
311+ const result = await activatePromise ;
312+ expect ( result . status ) . toBe ( 'valid' ) ;
313+
314+ vi . unstubAllGlobals ( ) ;
315+ vi . useRealTimers ( ) ;
316+
317+ // Simulate restart: load cached state (no server call)
318+ const { loadCachedLicenceState } = await import ( '../LicenceActivationService.js' ) ;
319+ const cachedState = await loadCachedLicenceState ( buildContext ( ) as any ) ;
320+ expect ( cachedState . status ) . toBe ( 'valid' ) ;
321+ expect ( cachedState . product ) . toBe ( 'pro_editor' ) ;
322+ expect ( cachedState . serverUnreachable ) . toBe ( true ) ;
323+ } ) ;
307324} ) ;
0 commit comments