@@ -82,6 +82,8 @@ class FakeRuntime {
8282 healthChecks : string [ ] = [ ] ;
8383 terminations : string [ ] = [ ] ;
8484 previewMints = 0 ;
85+ previewExpiresAt = 1_900_000_000_000 ;
86+ startedAtMs ?: number ;
8587 terminateSucceeds = true ;
8688 launchError ?: Error ;
8789 healthError ?: Error ;
@@ -94,6 +96,7 @@ class FakeRuntime {
9496 microvmId : 'vm-app-1' ,
9597 endpoint : 'https://vm-app-1.test' ,
9698 state : 'RUNNING' ,
99+ startedAtMs : this . startedAtMs ,
97100 imageArn : runtimeConfig . imageArn ,
98101 imageVersion : runtimeConfig . imageVersion ,
99102 } ,
@@ -120,7 +123,7 @@ class FakeRuntime {
120123 return {
121124 headerName : 'X-aws-proxy-auth' ,
122125 token : `secret-token-${ this . previewMints } ` ,
123- expiresAtMs : 1_900_000_000_000 ,
126+ expiresAtMs : this . previewExpiresAt ,
124127 } ;
125128 }
126129 async terminate ( microvmId : string ) : Promise < boolean > {
@@ -215,6 +218,18 @@ describe('HostedAppControlPlane', () => {
215218 expect ( hostedAppPublicStatus ( expired , 100 ) . state ) . toBe ( 'stopped' ) ;
216219 } ) ;
217220
221+ test ( 'does not expose provider details persisted in an internal failure record' , ( ) => {
222+ const failed = {
223+ ...pendingRecord ( ) ,
224+ state : 'TERMINATED' as const ,
225+ last_error : 'AccessDenied for arn:aws:iam::123456789012:role/private' ,
226+ } ;
227+ const status = hostedAppPublicStatus ( failed ) ;
228+ expect ( status . state ) . toBe ( 'failed' ) ;
229+ expect ( status . error ) . toBe ( 'Hosted app operation failed' ) ;
230+ expect ( JSON . stringify ( status ) ) . not . toContain ( '123456789012' ) ;
231+ } ) ;
232+
218233 test ( 'checkpoints, launches, restores, starts, and persists only a sealed preview credential' , async ( ) => {
219234 const f = fixture ( ) ;
220235
@@ -230,6 +245,19 @@ describe('HostedAppControlPlane', () => {
230245 expect ( openHostedAppCredential ( 'happ_123' , sealed , f . credentialKey ) . token ) . toBe ( 'secret-token-1' ) ;
231246 } ) ;
232247
248+ test ( 'derives the advertised lease deadline from the provider start time' , async ( ) => {
249+ const runtime = new FakeRuntime ( ) ;
250+ runtime . startedAtMs = 1_800_000_000_500 ;
251+ const f = fixture ( { runtime } ) ;
252+
253+ await f . control . start ( input ) ;
254+
255+ expect ( f . registry . record ?. launched_at ) . toBe ( runtime . startedAtMs ) ;
256+ expect ( f . registry . record ?. hard_deadline_at ) . toBe (
257+ runtime . startedAtMs + runtimeConfig . maximumDurationSeconds * 1_000 - 60_000 ,
258+ ) ;
259+ } ) ;
260+
233261 test ( 'replays an exact pending launch intent without taking a different checkpoint' , async ( ) => {
234262 const registry = new MemoryRegistry ( ) ;
235263 registry . record = pendingRecord ( ) ;
@@ -365,6 +393,61 @@ describe('HostedAppControlPlane', () => {
365393 } ) ;
366394 } ) ;
367395
396+ test ( 'stop replays an ambiguous pending launch before terminating it' , async ( ) => {
397+ const registry = new MemoryRegistry ( ) ;
398+ registry . record = pendingRecord ( ) ;
399+ const f = fixture ( { registry } ) ;
400+
401+ const status = await f . control . stop ( input . hostedAppRuntimeId , input , input . signal ) ;
402+
403+ expect ( status . state ) . toBe ( 'stopped' ) ;
404+ expect ( f . runtime . launches ) . toEqual ( [ pendingRecord ( ) . launch_client_token as string ] ) ;
405+ expect ( f . runtime . terminations ) . toEqual ( [ 'vm-app-1' ] ) ;
406+ expect ( registry . writes . map ( record => record . state ) ) . toEqual ( [
407+ 'PENDING' ,
408+ 'TERMINATING' ,
409+ 'TERMINATED' ,
410+ ] ) ;
411+ expect ( registry . record ) . toMatchObject ( {
412+ state : 'TERMINATED' ,
413+ microvm_id : undefined ,
414+ endpoint : undefined ,
415+ } ) ;
416+ } ) ;
417+
418+ test ( 'stop preserves an ambiguous pending intent when recovery fails' , async ( ) => {
419+ const registry = new MemoryRegistry ( ) ;
420+ registry . record = pendingRecord ( ) ;
421+ const runtime = new FakeRuntime ( ) ;
422+ runtime . launchError = new HostedAppMicrovmError (
423+ 'hosted_app_launch_failed' ,
424+ 'connection reset after provider accepted the request' ,
425+ true ,
426+ ) ;
427+ const f = fixture ( { registry, runtime } ) ;
428+
429+ const error = await f . control . stop ( input . hostedAppRuntimeId , input , input . signal )
430+ . catch ( value => value ) ;
431+
432+ expect ( error . code ) . toBe ( 'hosted_app_launch_failed' ) ;
433+ expect ( registry . writes ) . toEqual ( [ ] ) ;
434+ expect ( registry . record ) . toEqual ( pendingRecord ( ) ) ;
435+ } ) ;
436+
437+ test ( 'stop does not overwrite a pending intent that current config cannot replay' , async ( ) => {
438+ const registry = new MemoryRegistry ( ) ;
439+ registry . record = { ...pendingRecord ( ) , launch_fingerprint : 'different-image' } ;
440+ const f = fixture ( { registry } ) ;
441+
442+ const error = await f . control . stop ( input . hostedAppRuntimeId , input , input . signal )
443+ . catch ( value => value ) ;
444+
445+ expect ( error . code ) . toBe ( 'hosted_app_stop_pending' ) ;
446+ expect ( error . transient ) . toBe ( true ) ;
447+ expect ( f . runtime . launches ) . toEqual ( [ ] ) ;
448+ expect ( registry . writes ) . toEqual ( [ ] ) ;
449+ } ) ;
450+
368451 test ( 'a failed cleanup never promotes a partial launch to running' , async ( ) => {
369452 const registry = new MemoryRegistry ( ) ;
370453 registry . record = {
@@ -408,4 +491,34 @@ describe('HostedAppControlPlane', () => {
408491 expect ( ambiguous . registry . record ?. state ) . toBe ( 'PENDING' ) ;
409492 expect ( ambiguous . registry . record ?. microvm_id ) . toBeUndefined ( ) ;
410493 } ) ;
494+
495+ test ( 'does not refresh a preview after its advertised hard deadline' , async ( ) => {
496+ const registry = new MemoryRegistry ( ) ;
497+ registry . record = {
498+ ...pendingRecord ( ) ,
499+ state : 'RUNNING' ,
500+ microvm_id : 'vm-existing' ,
501+ endpoint : 'https://vm-existing.test' ,
502+ hard_deadline_at : 1_800_000_000_000 ,
503+ } ;
504+ const f = fixture ( { registry } ) ;
505+
506+ const error = await f . control . refreshPreview ( input . hostedAppRuntimeId , input , input . signal )
507+ . catch ( value => value ) ;
508+
509+ expect ( error . code ) . toBe ( 'hosted_app_not_running' ) ;
510+ expect ( f . runtime . previewMints ) . toBe ( 0 ) ;
511+ } ) ;
512+
513+ test ( 'rejects an already-expired credential instead of publishing it' , async ( ) => {
514+ const runtime = new FakeRuntime ( ) ;
515+ runtime . previewExpiresAt = 1_800_000_000_000 ;
516+ const f = fixture ( { runtime } ) ;
517+
518+ const error = await f . control . start ( input ) . catch ( value => value ) ;
519+
520+ expect ( error . code ) . toBe ( 'hosted_app_preview_unavailable' ) ;
521+ expect ( runtime . terminations ) . toEqual ( [ 'vm-app-1' ] ) ;
522+ expect ( f . registry . record ) . toMatchObject ( { state : 'TERMINATED' } ) ;
523+ } ) ;
411524} ) ;
0 commit comments