fix(recovery): preserve promotion lifecycle
- commit
- 64bb8b298f15c6acd37efea9df8024c995b8dacc
- author
- brice · · 2026-08-11 09:01 UTC
- parent
- 08343ae
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +25 -4
M internal/usecase/daemon/session_rename_purge_test.go +19 -0
| @@ -65,6 +65,25 @@ func TestRenameDefersCommittedRouteIdentityUntilFirstRouteSnapshot(t *testing.T) | |||
| 65 | 65 | require.False(t, identity.Ephemeral) | |
| 66 | 66 | } | |
| 67 | 67 | ||
| 68 | + | func TestEphemeralPromotionPreservesIncarnation(t *testing.T) { | |
| 69 | + | p, release := newBlockingPTY(t) | |
| 70 | + | defer release() | |
| 71 | + | store, state := newMockStore(t) | |
| 72 | + | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | |
| 73 | + | WithStore(t, store)(d) | |
| 74 | + | ||
| 75 | + | sess, err := createSessionForTest(d, "0", true, "/tmp", domain.Size{Cols: 80, Rows: 24}, terminalEnv{}, d.baseEnv) | |
| 76 | + | require.NoError(t, err) | |
| 77 | + | sess.mu.Lock() | |
| 78 | + | original := sess.incarnation | |
| 79 | + | sess.mu.Unlock() | |
| 80 | + | require.NotZero(t, original) | |
| 81 | + | ||
| 82 | + | require.NoError(t, d.renameSession(sess, "local")) | |
| 83 | + | require.Equal(t, original, sess.incarnation) | |
| 84 | + | require.Equal(t, original, state.record(t, "local").IncarnationID) | |
| 85 | + | } | |
| 86 | + | ||
| 68 | 87 | func TestRenamePreservesIncarnationSnapshotSources(t *testing.T) { | |
| 69 | 88 | d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{}) | |
| 70 | 89 | repository := &retryablePurgeRepository{} | |
| @@ -65,6 +65,25 @@ func TestRenameDefersCommittedRouteIdentityUntilFirstRouteSnapshot(t *testing.T) | |||||
| 65 | require.False(t, identity.Ephemeral) | 65 | require.False(t, identity.Ephemeral) | ||
| 66 | } | 66 | } | ||
| 67 | 67 | ||||
| 68 | + | func TestEphemeralPromotionPreservesIncarnation(t *testing.T) { | |||
| 69 | + | p, release := newBlockingPTY(t) | |||
| 70 | + | defer release() | |||
| 71 | + | store, state := newMockStore(t) | |||
| 72 | + | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | |||
| 73 | + | WithStore(t, store)(d) | |||
| 74 | + | ||||
| 75 | + | sess, err := createSessionForTest(d, "0", true, "/tmp", domain.Size{Cols: 80, Rows: 24}, terminalEnv{}, d.baseEnv) | |||
| 76 | + | require.NoError(t, err) | |||
| 77 | + | sess.mu.Lock() | |||
| 78 | + | original := sess.incarnation | |||
| 79 | + | sess.mu.Unlock() | |||
| 80 | + | require.NotZero(t, original) | |||
| 81 | + | ||||
| 82 | + | require.NoError(t, d.renameSession(sess, "local")) | |||
| 83 | + | require.Equal(t, original, sess.incarnation) | |||
| 84 | + | require.Equal(t, original, state.record(t, "local").IncarnationID) | |||
| 85 | + | } | |||
| 86 | + | ||||
| 68 | func TestRenamePreservesIncarnationSnapshotSources(t *testing.T) { | 87 | func TestRenamePreservesIncarnationSnapshotSources(t *testing.T) { | ||
| 69 | d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{}) | 88 | d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{}) | ||
| 70 | repository := &retryablePurgeRepository{} | 89 | repository := &retryablePurgeRepository{} | ||
M internal/usecase/recovery/coordinator.go +6 -4
| @@ -115,11 +115,13 @@ func (c *Coordinator) Create(ctx context.Context, record domain.CatalogueRecord) | |||
| 115 | 115 | } else if exists { | |
| 116 | 116 | return domain.CatalogueRecord{}, fmt.Errorf("recovery: session %q already exists", record.Name) | |
| 117 | 117 | } | |
| 118 | - | id, err := domain.NewIncarnationID(c.random) | |
| 119 | - | if err != nil { | |
| 120 | - | return domain.CatalogueRecord{}, fmt.Errorf("recovery: generate incarnation: %w", err) | |
| 118 | + | if record.IncarnationID == (domain.IncarnationID{}) { | |
| 119 | + | id, err := domain.NewIncarnationID(c.random) | |
| 120 | + | if err != nil { | |
| 121 | + | return domain.CatalogueRecord{}, fmt.Errorf("recovery: generate incarnation: %w", err) | |
| 122 | + | } | |
| 123 | + | record.IncarnationID = id | |
| 121 | 124 | } | |
| 122 | - | record.IncarnationID = id | |
| 123 | 125 | record.Committed = nil | |
| 124 | 126 | record.DegradedReason = "" | |
| 125 | 127 | if err := record.Validate(); err != nil { | |
| @@ -115,11 +115,13 @@ func (c *Coordinator) Create(ctx context.Context, record domain.CatalogueRecord) | |||||
| 115 | } else if exists { | 115 | } else if exists { | ||
| 116 | return domain.CatalogueRecord{}, fmt.Errorf("recovery: session %q already exists", record.Name) | 116 | return domain.CatalogueRecord{}, fmt.Errorf("recovery: session %q already exists", record.Name) | ||
| 117 | } | 117 | } | ||
| 118 | - | id, err := domain.NewIncarnationID(c.random) | 118 | + | if record.IncarnationID == (domain.IncarnationID{}) { |
| 119 | - | if err != nil { | 119 | + | id, err := domain.NewIncarnationID(c.random) |
| 120 | - | return domain.CatalogueRecord{}, fmt.Errorf("recovery: generate incarnation: %w", err) | 120 | + | if err != nil { |
| 121 | + | return domain.CatalogueRecord{}, fmt.Errorf("recovery: generate incarnation: %w", err) | |||
| 122 | + | } | |||
| 123 | + | record.IncarnationID = id | |||
| 121 | } | 124 | } | ||
| 122 | - | record.IncarnationID = id | |||
| 123 | record.Committed = nil | 125 | record.Committed = nil | ||
| 124 | record.DegradedReason = "" | 126 | record.DegradedReason = "" | ||
| 125 | if err := record.Validate(); err != nil { | 127 | if err := record.Validate(); err != nil { | ||