Skip to content

Commit f9800fe

Browse files
committed
Add auto-standby timer and lifecycle tests
1 parent 896ee1a commit f9800fe

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

lib/autostandby/controller_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,86 @@ func TestRunDegradesWhenStartupResyncFails(t *testing.T) {
455455
require.Equal(t, ReasonObserverError, status.Reason)
456456
}
457457

458+
func TestHandleStandbyTimerCallsStandbyAndClearsState(t *testing.T) {
459+
t.Parallel()
460+
461+
idleSince := time.Date(2026, 4, 6, 10, 55, 0, 0, time.UTC)
462+
store := newFakeInstanceStore([]Instance{{
463+
ID: "inst-standby",
464+
Name: "inst-standby",
465+
State: StateRunning,
466+
NetworkEnabled: true,
467+
IP: "192.168.100.61",
468+
AutoStandby: &Policy{Enabled: true, IdleTimeout: "1m"},
469+
Runtime: &Runtime{
470+
IdleSince: &idleSince,
471+
},
472+
}})
473+
controller := NewController(store, &fakeConnectionSource{}, ControllerOptions{
474+
Now: func() time.Time { return idleSince.Add(time.Minute) },
475+
})
476+
477+
require.NoError(t, controller.startupResync(context.Background()))
478+
479+
controller.handleStandbyTimer(context.Background(), "inst-standby")
480+
481+
require.Equal(t, []string{"inst-standby"}, store.standbyIDs)
482+
require.Nil(t, store.persistedRuntime["inst-standby"])
483+
484+
controller.mu.RLock()
485+
state := controller.states["inst-standby"]
486+
require.NotNil(t, state)
487+
assert.Nil(t, state.compiledPolicy)
488+
assert.Nil(t, state.activeInbound)
489+
assert.Nil(t, state.idleSince)
490+
assert.Nil(t, state.lastInboundAt)
491+
assert.Nil(t, state.nextStandbyAt)
492+
assert.False(t, state.standbyRequested)
493+
controller.mu.RUnlock()
494+
}
495+
496+
func TestHandleStandbyTimerFailureRearmsIdleCountdown(t *testing.T) {
497+
t.Parallel()
498+
499+
idleSince := time.Date(2026, 4, 6, 10, 55, 0, 0, time.UTC)
500+
now := idleSince.Add(time.Minute)
501+
store := newFakeInstanceStore([]Instance{{
502+
ID: "inst-standby-fail",
503+
Name: "inst-standby-fail",
504+
State: StateRunning,
505+
NetworkEnabled: true,
506+
IP: "192.168.100.62",
507+
AutoStandby: &Policy{Enabled: true, IdleTimeout: "1m"},
508+
Runtime: &Runtime{
509+
IdleSince: &idleSince,
510+
},
511+
}})
512+
store.standbyErr = errors.New("standby failed")
513+
controller := NewController(store, &fakeConnectionSource{}, ControllerOptions{
514+
Now: func() time.Time { return now },
515+
})
516+
517+
require.NoError(t, controller.startupResync(context.Background()))
518+
519+
controller.handleStandbyTimer(context.Background(), "inst-standby-fail")
520+
521+
require.Equal(t, []string{"inst-standby-fail"}, store.standbyIDs)
522+
require.NotNil(t, store.persistedRuntime["inst-standby-fail"])
523+
require.NotNil(t, store.persistedRuntime["inst-standby-fail"].IdleSince)
524+
assert.Equal(t, now, *store.persistedRuntime["inst-standby-fail"].IdleSince)
525+
526+
controller.mu.RLock()
527+
state := controller.states["inst-standby-fail"]
528+
require.NotNil(t, state)
529+
assert.NotNil(t, state.compiledPolicy)
530+
assert.False(t, state.standbyRequested)
531+
assert.NotNil(t, state.idleSince)
532+
assert.Equal(t, now, *state.idleSince)
533+
require.NotNil(t, state.nextStandbyAt)
534+
assert.Equal(t, now.Add(time.Minute), *state.nextStandbyAt)
535+
controller.mu.RUnlock()
536+
}
537+
458538
func mustAddr(raw string) netip.Addr {
459539
return netip.MustParseAddr(raw)
460540
}

lib/instances/update_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"testing"
7+
"time"
78

89
"github.com/kernel/hypeman/lib/autostandby"
910
"github.com/kernel/hypeman/lib/egressproxy"
@@ -239,3 +240,51 @@ func TestApplyUpdatedInstanceEnvSavesAutoStandbyAlongsideEnvWithoutMutatingOrigi
239240
assert.Equal(t, "5m0s", original.AutoStandby.IdleTimeout)
240241
assert.Equal(t, map[string]string{"OUTBOUND_OPENAI_KEY": "old"}, original.Env)
241242
}
243+
244+
func TestManagerUpdateInstanceAutoStandbyOnlyPublishesLifecycleUpdate(t *testing.T) {
245+
t.Parallel()
246+
247+
manager, _ := setupTestManager(t)
248+
id := "inst-auto-standby-update"
249+
require.NoError(t, manager.ensureDirectories(id))
250+
meta := &metadata{
251+
StoredMetadata: StoredMetadata{
252+
Id: id,
253+
Name: id,
254+
CreatedAt: time.Now(),
255+
DataDir: manager.paths.InstanceDir(id),
256+
SocketPath: manager.paths.InstanceSocket(id, "cloud-hypervisor.sock"),
257+
AutoStandby: &autostandby.Policy{
258+
Enabled: false,
259+
IdleTimeout: "5m0s",
260+
},
261+
},
262+
}
263+
require.NoError(t, manager.saveMetadata(meta))
264+
265+
events, unsubscribe := manager.SubscribeLifecycleEvents(LifecycleEventConsumerAutoStandby)
266+
defer unsubscribe()
267+
268+
updated, err := manager.UpdateInstance(context.Background(), id, UpdateInstanceRequest{
269+
AutoStandby: &autostandby.Policy{
270+
Enabled: true,
271+
IdleTimeout: "10m",
272+
},
273+
})
274+
require.NoError(t, err)
275+
require.NotNil(t, updated)
276+
require.NotNil(t, updated.AutoStandby)
277+
assert.True(t, updated.AutoStandby.Enabled)
278+
279+
select {
280+
case event := <-events:
281+
assert.Equal(t, LifecycleEventUpdate, event.Action)
282+
assert.Equal(t, id, event.InstanceID)
283+
require.NotNil(t, event.Instance)
284+
require.NotNil(t, event.Instance.AutoStandby)
285+
assert.True(t, event.Instance.AutoStandby.Enabled)
286+
assert.Equal(t, "10m0s", event.Instance.AutoStandby.IdleTimeout)
287+
case <-time.After(time.Second):
288+
t.Fatal("timed out waiting for lifecycle update event")
289+
}
290+
}

0 commit comments

Comments
 (0)