refactor(daemon): route metadata through catalogue
- commit
- ff3aadc7bf087d3ca8232b5dc1410a911cdc142a
- author
- brice · · 2026-07-26 00:35 UTC
- parent
- 7579215
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
13 files · +204 -88
M internal/domain/recovery.go +22 -0
| @@ -42,6 +42,28 @@ type CatalogueRecord struct { | |||
| 42 | 42 | DegradedReason string | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | + | // CatalogueMetadataUpdate changes mutable runtime metadata for one catalogue | |
| 46 | + | // incarnation without carrying authority-owned recovery or checkpoint fields. | |
| 47 | + | type CatalogueMetadataUpdate struct { | |
| 48 | + | Name string | |
| 49 | + | IncarnationID IncarnationID | |
| 50 | + | Cwd *string | |
| 51 | + | UpdatedAt *int64 | |
| 52 | + | LastUsedSeq *uint64 | |
| 53 | + | TabNames *[]string | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // MetadataUpdate returns a complete mutable-metadata update for r. Catalogue | |
| 57 | + | // implementations retain all recovery and checkpoint fields they own. | |
| 58 | + | func (r CatalogueRecord) MetadataUpdate() CatalogueMetadataUpdate { | |
| 59 | + | cwd, updatedAt, lastUsedSeq := r.Cwd, r.UpdatedAt, r.LastUsedSeq | |
| 60 | + | tabNames := append([]string(nil), r.TabNames...) | |
| 61 | + | return CatalogueMetadataUpdate{ | |
| 62 | + | Name: r.Name, IncarnationID: r.IncarnationID, | |
| 63 | + | Cwd: &cwd, UpdatedAt: &updatedAt, LastUsedSeq: &lastUsedSeq, TabNames: &tabNames, | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | ||
| 45 | 67 | // DiscardIntent records a requested replacement of uncertain persisted state. | |
| 46 | 68 | type DiscardIntent struct { | |
| 47 | 69 | OldRecord CatalogueRecord | |
| @@ -42,6 +42,28 @@ type CatalogueRecord struct { | |||||
| 42 | DegradedReason string | 42 | DegradedReason string | ||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | + | // CatalogueMetadataUpdate changes mutable runtime metadata for one catalogue | |||
| 46 | + | // incarnation without carrying authority-owned recovery or checkpoint fields. | |||
| 47 | + | type CatalogueMetadataUpdate struct { | |||
| 48 | + | Name string | |||
| 49 | + | IncarnationID IncarnationID | |||
| 50 | + | Cwd *string | |||
| 51 | + | UpdatedAt *int64 | |||
| 52 | + | LastUsedSeq *uint64 | |||
| 53 | + | TabNames *[]string | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // MetadataUpdate returns a complete mutable-metadata update for r. Catalogue | |||
| 57 | + | // implementations retain all recovery and checkpoint fields they own. | |||
| 58 | + | func (r CatalogueRecord) MetadataUpdate() CatalogueMetadataUpdate { | |||
| 59 | + | cwd, updatedAt, lastUsedSeq := r.Cwd, r.UpdatedAt, r.LastUsedSeq | |||
| 60 | + | tabNames := append([]string(nil), r.TabNames...) | |||
| 61 | + | return CatalogueMetadataUpdate{ | |||
| 62 | + | Name: r.Name, IncarnationID: r.IncarnationID, | |||
| 63 | + | Cwd: &cwd, UpdatedAt: &updatedAt, LastUsedSeq: &lastUsedSeq, TabNames: &tabNames, | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | ||||
| 45 | // DiscardIntent records a requested replacement of uncertain persisted state. | 67 | // DiscardIntent records a requested replacement of uncertain persisted state. | ||
| 46 | type DiscardIntent struct { | 68 | type DiscardIntent struct { | ||
| 47 | OldRecord CatalogueRecord | 69 | OldRecord CatalogueRecord | ||
M internal/persist/catalogue_test.go +12 -2
| @@ -105,14 +105,24 @@ func testCatalogueMetadataUpdatePreservesAuthority(t *testing.T) { | |||
| 105 | 105 | } | |
| 106 | 106 | require.NoError(t, p.Create(original)) | |
| 107 | 107 | ||
| 108 | - | update := domain.CatalogueRecord{Name: "work", IncarnationID: original.IncarnationID, Cwd: "/new", UpdatedAt: 22, LastUsedSeq: 23, TabNames: []string{"editor", "logs"}} | |
| 108 | + | next := domain.CatalogueRecord{Name: "work", IncarnationID: original.IncarnationID, Cwd: "/new", UpdatedAt: 22, LastUsedSeq: 23, TabNames: []string{"editor", "logs"}} | |
| 109 | + | update := next.MetadataUpdate() | |
| 109 | 110 | require.NoError(t, p.UpdateMetadata(update)) | |
| 110 | 111 | got, ok := p.Record("work") | |
| 111 | 112 | require.True(t, ok) | |
| 112 | 113 | expected := original | |
| 113 | - | expected.Cwd, expected.UpdatedAt, expected.LastUsedSeq, expected.TabNames = update.Cwd, update.UpdatedAt, update.LastUsedSeq, update.TabNames | |
| 114 | + | expected.Cwd, expected.UpdatedAt, expected.LastUsedSeq, expected.TabNames = next.Cwd, next.UpdatedAt, next.LastUsedSeq, next.TabNames | |
| 114 | 115 | require.Equal(t, expected, got) | |
| 115 | 116 | ||
| 117 | + | lastUsedSeq := uint64(24) | |
| 118 | + | require.NoError(t, p.UpdateMetadata(domain.CatalogueMetadataUpdate{ | |
| 119 | + | Name: "work", IncarnationID: original.IncarnationID, LastUsedSeq: &lastUsedSeq, | |
| 120 | + | })) | |
| 121 | + | expected.LastUsedSeq = lastUsedSeq | |
| 122 | + | partiallyUpdated, ok := p.Record("work") | |
| 123 | + | require.True(t, ok) | |
| 124 | + | require.Equal(t, expected, partiallyUpdated, "a partial metadata update must not clear other mutable or authority-owned fields") | |
| 125 | + | ||
| 116 | 126 | stale := update | |
| 117 | 127 | stale.IncarnationID = domain.IncarnationID{2} | |
| 118 | 128 | require.Error(t, p.UpdateMetadata(stale)) | |
| @@ -105,14 +105,24 @@ func testCatalogueMetadataUpdatePreservesAuthority(t *testing.T) { | |||||
| 105 | } | 105 | } | ||
| 106 | require.NoError(t, p.Create(original)) | 106 | require.NoError(t, p.Create(original)) | ||
| 107 | 107 | ||||
| 108 | - | update := domain.CatalogueRecord{Name: "work", IncarnationID: original.IncarnationID, Cwd: "/new", UpdatedAt: 22, LastUsedSeq: 23, TabNames: []string{"editor", "logs"}} | 108 | + | next := domain.CatalogueRecord{Name: "work", IncarnationID: original.IncarnationID, Cwd: "/new", UpdatedAt: 22, LastUsedSeq: 23, TabNames: []string{"editor", "logs"}} |
| 109 | + | update := next.MetadataUpdate() | |||
| 109 | require.NoError(t, p.UpdateMetadata(update)) | 110 | require.NoError(t, p.UpdateMetadata(update)) | ||
| 110 | got, ok := p.Record("work") | 111 | got, ok := p.Record("work") | ||
| 111 | require.True(t, ok) | 112 | require.True(t, ok) | ||
| 112 | expected := original | 113 | expected := original | ||
| 113 | - | expected.Cwd, expected.UpdatedAt, expected.LastUsedSeq, expected.TabNames = update.Cwd, update.UpdatedAt, update.LastUsedSeq, update.TabNames | 114 | + | expected.Cwd, expected.UpdatedAt, expected.LastUsedSeq, expected.TabNames = next.Cwd, next.UpdatedAt, next.LastUsedSeq, next.TabNames |
| 114 | require.Equal(t, expected, got) | 115 | require.Equal(t, expected, got) | ||
| 115 | 116 | ||||
| 117 | + | lastUsedSeq := uint64(24) | |||
| 118 | + | require.NoError(t, p.UpdateMetadata(domain.CatalogueMetadataUpdate{ | |||
| 119 | + | Name: "work", IncarnationID: original.IncarnationID, LastUsedSeq: &lastUsedSeq, | |||
| 120 | + | })) | |||
| 121 | + | expected.LastUsedSeq = lastUsedSeq | |||
| 122 | + | partiallyUpdated, ok := p.Record("work") | |||
| 123 | + | require.True(t, ok) | |||
| 124 | + | require.Equal(t, expected, partiallyUpdated, "a partial metadata update must not clear other mutable or authority-owned fields") | |||
| 125 | + | ||||
| 116 | stale := update | 126 | stale := update | ||
| 117 | stale.IncarnationID = domain.IncarnationID{2} | 127 | stale.IncarnationID = domain.IncarnationID{2} | ||
| 118 | require.Error(t, p.UpdateMetadata(stale)) | 128 | require.Error(t, p.UpdateMetadata(stale)) | ||
M internal/persist/persist.go +14 -55
| @@ -208,79 +208,38 @@ func (p *Persister) Replace(name string, next domain.CatalogueRecord) error { | |||
| 208 | 208 | // UpdateMetadata atomically updates ordinary session metadata on the existing | |
| 209 | 209 | // authoritative incarnation. Recovery and transaction fields are retained from | |
| 210 | 210 | // the stored record rather than accepted from a potentially stale runtime copy. | |
| 211 | - | func (p *Persister) UpdateMetadata(next domain.CatalogueRecord) error { | |
| 211 | + | func (p *Persister) UpdateMetadata(update domain.CatalogueMetadataUpdate) error { | |
| 212 | 212 | if p == nil || p.store == nil { | |
| 213 | 213 | return nil | |
| 214 | 214 | } | |
| 215 | 215 | p.mu.Lock() | |
| 216 | 216 | defer p.mu.Unlock() | |
| 217 | - | value, ok := p.store.Get([]byte(next.Name)) | |
| 217 | + | value, ok := p.store.Get([]byte(update.Name)) | |
| 218 | 218 | if !ok { | |
| 219 | 219 | return errors.New("persist: session not found") | |
| 220 | 220 | } | |
| 221 | - | current, err := decodeRecordValue(next.Name, value) | |
| 221 | + | current, err := decodeRecordValue(update.Name, value) | |
| 222 | 222 | if err != nil { | |
| 223 | 223 | return err | |
| 224 | 224 | } | |
| 225 | - | if next.IncarnationID == (domain.IncarnationID{}) || current.IncarnationID != next.IncarnationID { | |
| 225 | + | if update.IncarnationID == (domain.IncarnationID{}) || current.IncarnationID != update.IncarnationID { | |
| 226 | 226 | return errors.New("persist: session incarnation changed") | |
| 227 | 227 | } | |
| 228 | - | current.Cwd = next.Cwd | |
| 229 | - | current.UpdatedAt = next.UpdatedAt | |
| 230 | - | current.LastUsedSeq = next.LastUsedSeq | |
| 231 | - | current.TabNames = append([]string(nil), next.TabNames...) | |
| 232 | - | return p.applyLocked(map[string]*domain.CatalogueRecord{current.Name: ¤t}) | |
| 233 | - | } | |
| 234 | - | ||
| 235 | - | // Touch updates metadata while retaining the durable identity and recovery state. | |
| 236 | - | func (p *Persister) Touch(name string, incarnation domain.IncarnationID, cwd string, at int64) error { | |
| 237 | - | if p == nil || p.store == nil { | |
| 238 | - | return nil | |
| 239 | - | } | |
| 240 | - | p.mu.Lock() | |
| 241 | - | defer p.mu.Unlock() | |
| 242 | - | value, ok := p.store.Get([]byte(name)) | |
| 243 | - | if !ok { | |
| 244 | - | return errors.New("persist: session not found") | |
| 245 | - | } | |
| 246 | - | record, err := decodeRecordValue(name, value) | |
| 247 | - | if err != nil { | |
| 248 | - | return err | |
| 249 | - | } | |
| 250 | - | if incarnation == (domain.IncarnationID{}) || record.IncarnationID != incarnation { | |
| 251 | - | return errors.New("persist: session incarnation changed") | |
| 252 | - | } | |
| 253 | - | record.Cwd, record.UpdatedAt = cwd, at | |
| 254 | - | encoded, err := encodeRecordValue(record) | |
| 255 | - | if err != nil { | |
| 256 | - | return err | |
| 228 | + | if update.Cwd != nil { | |
| 229 | + | current.Cwd = *update.Cwd | |
| 257 | 230 | } | |
| 258 | - | return p.store.Set([]byte(name), encoded) | |
| 259 | - | } | |
| 260 | - | func (p *Persister) TouchMRU(name string, incarnation domain.IncarnationID, sequence uint64) error { | |
| 261 | - | if p == nil || p.store == nil { | |
| 262 | - | return nil | |
| 231 | + | if update.UpdatedAt != nil { | |
| 232 | + | current.UpdatedAt = *update.UpdatedAt | |
| 263 | 233 | } | |
| 264 | - | p.mu.Lock() | |
| 265 | - | defer p.mu.Unlock() | |
| 266 | - | value, ok := p.store.Get([]byte(name)) | |
| 267 | - | if !ok { | |
| 268 | - | return errors.New("persist: session not found") | |
| 234 | + | if update.LastUsedSeq != nil { | |
| 235 | + | current.LastUsedSeq = *update.LastUsedSeq | |
| 269 | 236 | } | |
| 270 | - | record, err := decodeRecordValue(name, value) | |
| 271 | - | if err != nil { | |
| 272 | - | return err | |
| 237 | + | if update.TabNames != nil { | |
| 238 | + | current.TabNames = append([]string(nil), (*update.TabNames)...) | |
| 273 | 239 | } | |
| 274 | - | if incarnation == (domain.IncarnationID{}) || record.IncarnationID != incarnation { | |
| 275 | - | return errors.New("persist: session incarnation changed") | |
| 276 | - | } | |
| 277 | - | record.LastUsedSeq = sequence | |
| 278 | - | encoded, err := encodeRecordValue(record) | |
| 279 | - | if err != nil { | |
| 280 | - | return err | |
| 281 | - | } | |
| 282 | - | return p.store.Set([]byte(name), encoded) | |
| 240 | + | return p.applyLocked(map[string]*domain.CatalogueRecord{current.Name: ¤t}) | |
| 283 | 241 | } | |
| 242 | + | ||
| 284 | 243 | func (p *Persister) Delete(name string) error { | |
| 285 | 244 | return p.Apply(map[string]*domain.CatalogueRecord{name: nil}) | |
| 286 | 245 | } | |
| @@ -208,79 +208,38 @@ func (p *Persister) Replace(name string, next domain.CatalogueRecord) error { | |||||
| 208 | // UpdateMetadata atomically updates ordinary session metadata on the existing | 208 | // UpdateMetadata atomically updates ordinary session metadata on the existing | ||
| 209 | // authoritative incarnation. Recovery and transaction fields are retained from | 209 | // authoritative incarnation. Recovery and transaction fields are retained from | ||
| 210 | // the stored record rather than accepted from a potentially stale runtime copy. | 210 | // the stored record rather than accepted from a potentially stale runtime copy. | ||
| 211 | - | func (p *Persister) UpdateMetadata(next domain.CatalogueRecord) error { | 211 | + | func (p *Persister) UpdateMetadata(update domain.CatalogueMetadataUpdate) error { |
| 212 | if p == nil || p.store == nil { | 212 | if p == nil || p.store == nil { | ||
| 213 | return nil | 213 | return nil | ||
| 214 | } | 214 | } | ||
| 215 | p.mu.Lock() | 215 | p.mu.Lock() | ||
| 216 | defer p.mu.Unlock() | 216 | defer p.mu.Unlock() | ||
| 217 | - | value, ok := p.store.Get([]byte(next.Name)) | 217 | + | value, ok := p.store.Get([]byte(update.Name)) |
| 218 | if !ok { | 218 | if !ok { | ||
| 219 | return errors.New("persist: session not found") | 219 | return errors.New("persist: session not found") | ||
| 220 | } | 220 | } | ||
| 221 | - | current, err := decodeRecordValue(next.Name, value) | 221 | + | current, err := decodeRecordValue(update.Name, value) |
| 222 | if err != nil { | 222 | if err != nil { | ||
| 223 | return err | 223 | return err | ||
| 224 | } | 224 | } | ||
| 225 | - | if next.IncarnationID == (domain.IncarnationID{}) || current.IncarnationID != next.IncarnationID { | 225 | + | if update.IncarnationID == (domain.IncarnationID{}) || current.IncarnationID != update.IncarnationID { |
| 226 | return errors.New("persist: session incarnation changed") | 226 | return errors.New("persist: session incarnation changed") | ||
| 227 | } | 227 | } | ||
| 228 | - | current.Cwd = next.Cwd | 228 | + | if update.Cwd != nil { |
| 229 | - | current.UpdatedAt = next.UpdatedAt | 229 | + | current.Cwd = *update.Cwd |
| 230 | - | current.LastUsedSeq = next.LastUsedSeq | |||
| 231 | - | current.TabNames = append([]string(nil), next.TabNames...) | |||
| 232 | - | return p.applyLocked(map[string]*domain.CatalogueRecord{current.Name: ¤t}) | |||
| 233 | - | } | |||
| 234 | - | ||||
| 235 | - | // Touch updates metadata while retaining the durable identity and recovery state. | |||
| 236 | - | func (p *Persister) Touch(name string, incarnation domain.IncarnationID, cwd string, at int64) error { | |||
| 237 | - | if p == nil || p.store == nil { | |||
| 238 | - | return nil | |||
| 239 | - | } | |||
| 240 | - | p.mu.Lock() | |||
| 241 | - | defer p.mu.Unlock() | |||
| 242 | - | value, ok := p.store.Get([]byte(name)) | |||
| 243 | - | if !ok { | |||
| 244 | - | return errors.New("persist: session not found") | |||
| 245 | - | } | |||
| 246 | - | record, err := decodeRecordValue(name, value) | |||
| 247 | - | if err != nil { | |||
| 248 | - | return err | |||
| 249 | - | } | |||
| 250 | - | if incarnation == (domain.IncarnationID{}) || record.IncarnationID != incarnation { | |||
| 251 | - | return errors.New("persist: session incarnation changed") | |||
| 252 | - | } | |||
| 253 | - | record.Cwd, record.UpdatedAt = cwd, at | |||
| 254 | - | encoded, err := encodeRecordValue(record) | |||
| 255 | - | if err != nil { | |||
| 256 | - | return err | |||
| 257 | } | 230 | } | ||
| 258 | - | return p.store.Set([]byte(name), encoded) | 231 | + | if update.UpdatedAt != nil { |
| 259 | - | } | 232 | + | current.UpdatedAt = *update.UpdatedAt |
| 260 | - | func (p *Persister) TouchMRU(name string, incarnation domain.IncarnationID, sequence uint64) error { | |||
| 261 | - | if p == nil || p.store == nil { | |||
| 262 | - | return nil | |||
| 263 | } | 233 | } | ||
| 264 | - | p.mu.Lock() | 234 | + | if update.LastUsedSeq != nil { |
| 265 | - | defer p.mu.Unlock() | 235 | + | current.LastUsedSeq = *update.LastUsedSeq |
| 266 | - | value, ok := p.store.Get([]byte(name)) | |||
| 267 | - | if !ok { | |||
| 268 | - | return errors.New("persist: session not found") | |||
| 269 | } | 236 | } | ||
| 270 | - | record, err := decodeRecordValue(name, value) | 237 | + | if update.TabNames != nil { |
| 271 | - | if err != nil { | 238 | + | current.TabNames = append([]string(nil), (*update.TabNames)...) |
| 272 | - | return err | |||
| 273 | } | 239 | } | ||
| 274 | - | if incarnation == (domain.IncarnationID{}) || record.IncarnationID != incarnation { | 240 | + | return p.applyLocked(map[string]*domain.CatalogueRecord{current.Name: ¤t}) |
| 275 | - | return errors.New("persist: session incarnation changed") | |||
| 276 | - | } | |||
| 277 | - | record.LastUsedSeq = sequence | |||
| 278 | - | encoded, err := encodeRecordValue(record) | |||
| 279 | - | if err != nil { | |||
| 280 | - | return err | |||
| 281 | - | } | |||
| 282 | - | return p.store.Set([]byte(name), encoded) | |||
| 283 | } | 241 | } | ||
| 242 | + | ||||
| 284 | func (p *Persister) Delete(name string) error { | 243 | func (p *Persister) Delete(name string) error { | ||
| 285 | return p.Apply(map[string]*domain.CatalogueRecord{name: nil}) | 244 | return p.Apply(map[string]*domain.CatalogueRecord{name: nil}) | ||
| 286 | } | 245 | } | ||
M internal/persist/persist_test.go +1 -2
| @@ -69,8 +69,7 @@ func TestCatalogueNilStoreIsNoOp(t *testing.T) { | |||
| 69 | 69 | for _, p := range persisters { | |
| 70 | 70 | require.NoError(t, p.Save(validRecord("one", 1))) | |
| 71 | 71 | require.NoError(t, p.Apply(map[string]*domain.CatalogueRecord{"one": nil})) | |
| 72 | - | require.NoError(t, p.Touch("one", domain.IncarnationID{1}, "/tmp", 1)) | |
| 73 | - | require.NoError(t, p.TouchMRU("one", domain.IncarnationID{1}, 1)) | |
| 72 | + | require.NoError(t, p.UpdateMetadata(validRecord("one", 1).MetadataUpdate())) | |
| 74 | 73 | require.NoError(t, p.Delete("one")) | |
| 75 | 74 | records, err := p.LoadAll() | |
| 76 | 75 | require.NoError(t, err) | |
| @@ -69,8 +69,7 @@ func TestCatalogueNilStoreIsNoOp(t *testing.T) { | |||||
| 69 | for _, p := range persisters { | 69 | for _, p := range persisters { | ||
| 70 | require.NoError(t, p.Save(validRecord("one", 1))) | 70 | require.NoError(t, p.Save(validRecord("one", 1))) | ||
| 71 | require.NoError(t, p.Apply(map[string]*domain.CatalogueRecord{"one": nil})) | 71 | require.NoError(t, p.Apply(map[string]*domain.CatalogueRecord{"one": nil})) | ||
| 72 | - | require.NoError(t, p.Touch("one", domain.IncarnationID{1}, "/tmp", 1)) | 72 | + | require.NoError(t, p.UpdateMetadata(validRecord("one", 1).MetadataUpdate())) |
| 73 | - | require.NoError(t, p.TouchMRU("one", domain.IncarnationID{1}, 1)) | |||
| 74 | require.NoError(t, p.Delete("one")) | 73 | require.NoError(t, p.Delete("one")) | ||
| 75 | records, err := p.LoadAll() | 74 | records, err := p.LoadAll() | ||
| 76 | require.NoError(t, err) | 75 | require.NoError(t, err) | ||
M internal/ports/mocks/recovery_mock.go +51 -0
| @@ -998,6 +998,57 @@ func (_c *MockCatalogue_Replace_Call) RunAndReturn(run func(s string, catalogueR | |||
| 998 | 998 | return _c | |
| 999 | 999 | } | |
| 1000 | 1000 | ||
| 1001 | + | // UpdateMetadata provides a mock function for the type MockCatalogue | |
| 1002 | + | func (_mock *MockCatalogue) UpdateMetadata(catalogueMetadataUpdate domain.CatalogueMetadataUpdate) error { | |
| 1003 | + | ret := _mock.Called(catalogueMetadataUpdate) | |
| 1004 | + | ||
| 1005 | + | if len(ret) == 0 { | |
| 1006 | + | panic("no return value specified for UpdateMetadata") | |
| 1007 | + | } | |
| 1008 | + | ||
| 1009 | + | var r0 error | |
| 1010 | + | if returnFunc, ok := ret.Get(0).(func(domain.CatalogueMetadataUpdate) error); ok { | |
| 1011 | + | r0 = returnFunc(catalogueMetadataUpdate) | |
| 1012 | + | } else { | |
| 1013 | + | r0 = ret.Error(0) | |
| 1014 | + | } | |
| 1015 | + | return r0 | |
| 1016 | + | } | |
| 1017 | + | ||
| 1018 | + | // MockCatalogue_UpdateMetadata_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateMetadata' | |
| 1019 | + | type MockCatalogue_UpdateMetadata_Call struct { | |
| 1020 | + | *mock.Call | |
| 1021 | + | } | |
| 1022 | + | ||
| 1023 | + | // UpdateMetadata is a helper method to define mock.On call | |
| 1024 | + | // - catalogueMetadataUpdate domain.CatalogueMetadataUpdate | |
| 1025 | + | func (_e *MockCatalogue_Expecter) UpdateMetadata(catalogueMetadataUpdate any) *MockCatalogue_UpdateMetadata_Call { | |
| 1026 | + | return &MockCatalogue_UpdateMetadata_Call{Call: _e.mock.On("UpdateMetadata", catalogueMetadataUpdate)} | |
| 1027 | + | } | |
| 1028 | + | ||
| 1029 | + | func (_c *MockCatalogue_UpdateMetadata_Call) Run(run func(catalogueMetadataUpdate domain.CatalogueMetadataUpdate)) *MockCatalogue_UpdateMetadata_Call { | |
| 1030 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1031 | + | var arg0 domain.CatalogueMetadataUpdate | |
| 1032 | + | if args[0] != nil { | |
| 1033 | + | arg0 = args[0].(domain.CatalogueMetadataUpdate) | |
| 1034 | + | } | |
| 1035 | + | run( | |
| 1036 | + | arg0, | |
| 1037 | + | ) | |
| 1038 | + | }) | |
| 1039 | + | return _c | |
| 1040 | + | } | |
| 1041 | + | ||
| 1042 | + | func (_c *MockCatalogue_UpdateMetadata_Call) Return(err error) *MockCatalogue_UpdateMetadata_Call { | |
| 1043 | + | _c.Call.Return(err) | |
| 1044 | + | return _c | |
| 1045 | + | } | |
| 1046 | + | ||
| 1047 | + | func (_c *MockCatalogue_UpdateMetadata_Call) RunAndReturn(run func(catalogueMetadataUpdate domain.CatalogueMetadataUpdate) error) *MockCatalogue_UpdateMetadata_Call { | |
| 1048 | + | _c.Call.Return(run) | |
| 1049 | + | return _c | |
| 1050 | + | } | |
| 1051 | + | ||
| 1001 | 1052 | // NewMockSnapshotRepository creates a new instance of MockSnapshotRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 1002 | 1053 | // The first argument is typically a *testing.T value. | |
| 1003 | 1054 | func NewMockSnapshotRepository(t interface { | |
| @@ -998,6 +998,57 @@ func (_c *MockCatalogue_Replace_Call) RunAndReturn(run func(s string, catalogueR | |||||
| 998 | return _c | 998 | return _c | ||
| 999 | } | 999 | } | ||
| 1000 | 1000 | ||||
| 1001 | + | // UpdateMetadata provides a mock function for the type MockCatalogue | |||
| 1002 | + | func (_mock *MockCatalogue) UpdateMetadata(catalogueMetadataUpdate domain.CatalogueMetadataUpdate) error { | |||
| 1003 | + | ret := _mock.Called(catalogueMetadataUpdate) | |||
| 1004 | + | ||||
| 1005 | + | if len(ret) == 0 { | |||
| 1006 | + | panic("no return value specified for UpdateMetadata") | |||
| 1007 | + | } | |||
| 1008 | + | ||||
| 1009 | + | var r0 error | |||
| 1010 | + | if returnFunc, ok := ret.Get(0).(func(domain.CatalogueMetadataUpdate) error); ok { | |||
| 1011 | + | r0 = returnFunc(catalogueMetadataUpdate) | |||
| 1012 | + | } else { | |||
| 1013 | + | r0 = ret.Error(0) | |||
| 1014 | + | } | |||
| 1015 | + | return r0 | |||
| 1016 | + | } | |||
| 1017 | + | ||||
| 1018 | + | // MockCatalogue_UpdateMetadata_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateMetadata' | |||
| 1019 | + | type MockCatalogue_UpdateMetadata_Call struct { | |||
| 1020 | + | *mock.Call | |||
| 1021 | + | } | |||
| 1022 | + | ||||
| 1023 | + | // UpdateMetadata is a helper method to define mock.On call | |||
| 1024 | + | // - catalogueMetadataUpdate domain.CatalogueMetadataUpdate | |||
| 1025 | + | func (_e *MockCatalogue_Expecter) UpdateMetadata(catalogueMetadataUpdate any) *MockCatalogue_UpdateMetadata_Call { | |||
| 1026 | + | return &MockCatalogue_UpdateMetadata_Call{Call: _e.mock.On("UpdateMetadata", catalogueMetadataUpdate)} | |||
| 1027 | + | } | |||
| 1028 | + | ||||
| 1029 | + | func (_c *MockCatalogue_UpdateMetadata_Call) Run(run func(catalogueMetadataUpdate domain.CatalogueMetadataUpdate)) *MockCatalogue_UpdateMetadata_Call { | |||
| 1030 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 1031 | + | var arg0 domain.CatalogueMetadataUpdate | |||
| 1032 | + | if args[0] != nil { | |||
| 1033 | + | arg0 = args[0].(domain.CatalogueMetadataUpdate) | |||
| 1034 | + | } | |||
| 1035 | + | run( | |||
| 1036 | + | arg0, | |||
| 1037 | + | ) | |||
| 1038 | + | }) | |||
| 1039 | + | return _c | |||
| 1040 | + | } | |||
| 1041 | + | ||||
| 1042 | + | func (_c *MockCatalogue_UpdateMetadata_Call) Return(err error) *MockCatalogue_UpdateMetadata_Call { | |||
| 1043 | + | _c.Call.Return(err) | |||
| 1044 | + | return _c | |||
| 1045 | + | } | |||
| 1046 | + | ||||
| 1047 | + | func (_c *MockCatalogue_UpdateMetadata_Call) RunAndReturn(run func(catalogueMetadataUpdate domain.CatalogueMetadataUpdate) error) *MockCatalogue_UpdateMetadata_Call { | |||
| 1048 | + | _c.Call.Return(run) | |||
| 1049 | + | return _c | |||
| 1050 | + | } | |||
| 1051 | + | ||||
| 1001 | // NewMockSnapshotRepository creates a new instance of MockSnapshotRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | 1052 | // NewMockSnapshotRepository creates a new instance of MockSnapshotRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | ||
| 1002 | // The first argument is typically a *testing.T value. | 1053 | // The first argument is typically a *testing.T value. | ||
| 1003 | func NewMockSnapshotRepository(t interface { | 1054 | func NewMockSnapshotRepository(t interface { | ||
M internal/ports/recovery.go +1 -0
| @@ -138,6 +138,7 @@ type Catalogue interface { | |||
| 138 | 138 | Records() []domain.CatalogueRecord | |
| 139 | 139 | Record(string) (domain.CatalogueRecord, bool) | |
| 140 | 140 | Create(domain.CatalogueRecord) error | |
| 141 | + | UpdateMetadata(domain.CatalogueMetadataUpdate) error | |
| 141 | 142 | Replace(string, domain.CatalogueRecord) error | |
| 142 | 143 | Rename(string, domain.CatalogueRecord) error | |
| 143 | 144 | Delete(string) error | |
| @@ -138,6 +138,7 @@ type Catalogue interface { | |||||
| 138 | Records() []domain.CatalogueRecord | 138 | Records() []domain.CatalogueRecord | ||
| 139 | Record(string) (domain.CatalogueRecord, bool) | 139 | Record(string) (domain.CatalogueRecord, bool) | ||
| 140 | Create(domain.CatalogueRecord) error | 140 | Create(domain.CatalogueRecord) error | ||
| 141 | + | UpdateMetadata(domain.CatalogueMetadataUpdate) error | |||
| 141 | Replace(string, domain.CatalogueRecord) error | 142 | Replace(string, domain.CatalogueRecord) error | ||
| 142 | Rename(string, domain.CatalogueRecord) error | 143 | Rename(string, domain.CatalogueRecord) error | ||
| 143 | Delete(string) error | 144 | Delete(string) error | ||
A internal/usecase/daemon/catalogue.go +36 -0
| @@ -0,0 +1,36 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/vev/internal/domain" | |
| 5 | + | ) | |
| 6 | + | ||
| 7 | + | // Catalogue helpers keep persistence optional for ephemeral and focused unit | |
| 8 | + | // tests while ensuring every durable named-session mutation uses the authority | |
| 9 | + | // port when one is configured. | |
| 10 | + | func (d *Daemon) catalogueRecord(name string) (domain.CatalogueRecord, bool) { | |
| 11 | + | if d == nil || d.catalogue == nil { | |
| 12 | + | return domain.CatalogueRecord{}, false | |
| 13 | + | } | |
| 14 | + | return d.catalogue.Record(name) | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | func (d *Daemon) createCatalogueRecord(record domain.CatalogueRecord) error { | |
| 18 | + | if d == nil || d.catalogue == nil { | |
| 19 | + | return nil | |
| 20 | + | } | |
| 21 | + | return d.catalogue.Create(record) | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func (d *Daemon) updateCatalogueMetadata(update domain.CatalogueMetadataUpdate) error { | |
| 25 | + | if d == nil || d.catalogue == nil { | |
| 26 | + | return nil | |
| 27 | + | } | |
| 28 | + | return d.catalogue.UpdateMetadata(update) | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func (d *Daemon) deleteCatalogueRecord(name string) error { | |
| 32 | + | if d == nil || d.catalogue == nil { | |
| 33 | + | return nil | |
| 34 | + | } | |
| 35 | + | return d.catalogue.Delete(name) | |
| 36 | + | } | |
| @@ -0,0 +1,36 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/vev/internal/domain" | |||
| 5 | + | ) | |||
| 6 | + | ||||
| 7 | + | // Catalogue helpers keep persistence optional for ephemeral and focused unit | |||
| 8 | + | // tests while ensuring every durable named-session mutation uses the authority | |||
| 9 | + | // port when one is configured. | |||
| 10 | + | func (d *Daemon) catalogueRecord(name string) (domain.CatalogueRecord, bool) { | |||
| 11 | + | if d == nil || d.catalogue == nil { | |||
| 12 | + | return domain.CatalogueRecord{}, false | |||
| 13 | + | } | |||
| 14 | + | return d.catalogue.Record(name) | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | func (d *Daemon) createCatalogueRecord(record domain.CatalogueRecord) error { | |||
| 18 | + | if d == nil || d.catalogue == nil { | |||
| 19 | + | return nil | |||
| 20 | + | } | |||
| 21 | + | return d.catalogue.Create(record) | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func (d *Daemon) updateCatalogueMetadata(update domain.CatalogueMetadataUpdate) error { | |||
| 25 | + | if d == nil || d.catalogue == nil { | |||
| 26 | + | return nil | |||
| 27 | + | } | |||
| 28 | + | return d.catalogue.UpdateMetadata(update) | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func (d *Daemon) deleteCatalogueRecord(name string) error { | |||
| 32 | + | if d == nil || d.catalogue == nil { | |||
| 33 | + | return nil | |||
| 34 | + | } | |||
| 35 | + | return d.catalogue.Delete(name) | |||
| 36 | + | } | |||
M internal/usecase/daemon/daemon.go +1 -3
| @@ -256,6 +256,7 @@ func WithShell(cmd string, args []string) Option { | |||
| 256 | 256 | func WithStore(store ports.Store) Option { | |
| 257 | 257 | return func(d *Daemon) { | |
| 258 | 258 | d.persist = persist.New(store) | |
| 259 | + | d.catalogue = d.persist | |
| 259 | 260 | d.persistEnabled = store != nil | |
| 260 | 261 | } | |
| 261 | 262 | } | |
| @@ -265,9 +266,6 @@ func WithStore(store ports.Store) Option { | |||
| 265 | 266 | func WithCatalogue(catalogue ports.Catalogue, records []domain.CatalogueRecord) Option { | |
| 266 | 267 | return func(d *Daemon) { | |
| 267 | 268 | d.catalogue = catalogue | |
| 268 | - | if persister, ok := catalogue.(*persist.Persister); ok { | |
| 269 | - | d.persist = persister | |
| 270 | - | } | |
| 271 | 269 | d.persistEnabled = catalogue != nil | |
| 272 | 270 | d.catalogueRecords = append([]domain.CatalogueRecord(nil), records...) | |
| 273 | 271 | d.catalogueRecordsProvided = true | |
| @@ -256,6 +256,7 @@ func WithShell(cmd string, args []string) Option { | |||||
| 256 | func WithStore(store ports.Store) Option { | 256 | func WithStore(store ports.Store) Option { | ||
| 257 | return func(d *Daemon) { | 257 | return func(d *Daemon) { | ||
| 258 | d.persist = persist.New(store) | 258 | d.persist = persist.New(store) | ||
| 259 | + | d.catalogue = d.persist | |||
| 259 | d.persistEnabled = store != nil | 260 | d.persistEnabled = store != nil | ||
| 260 | } | 261 | } | ||
| 261 | } | 262 | } | ||
| @@ -265,9 +266,6 @@ func WithStore(store ports.Store) Option { | |||||
| 265 | func WithCatalogue(catalogue ports.Catalogue, records []domain.CatalogueRecord) Option { | 266 | func WithCatalogue(catalogue ports.Catalogue, records []domain.CatalogueRecord) Option { | ||
| 266 | return func(d *Daemon) { | 267 | return func(d *Daemon) { | ||
| 267 | d.catalogue = catalogue | 268 | d.catalogue = catalogue | ||
| 268 | - | if persister, ok := catalogue.(*persist.Persister); ok { | |||
| 269 | - | d.persist = persister | |||
| 270 | - | } | |||
| 271 | d.persistEnabled = catalogue != nil | 269 | d.persistEnabled = catalogue != nil | ||
| 272 | d.catalogueRecords = append([]domain.CatalogueRecord(nil), records...) | 270 | d.catalogueRecords = append([]domain.CatalogueRecord(nil), records...) | ||
| 273 | d.catalogueRecordsProvided = true | 271 | d.catalogueRecordsProvided = true | ||
M internal/usecase/daemon/durable_recovery_test.go +36 -2
| @@ -22,8 +22,9 @@ import ( | |||
| 22 | 22 | ||
| 23 | 23 | type durableRecoveryCatalogue struct { | |
| 24 | 24 | ports.Catalogue | |
| 25 | - | mu sync.Mutex | |
| 26 | - | records map[string]domain.CatalogueRecord | |
| 25 | + | mu sync.Mutex | |
| 26 | + | records map[string]domain.CatalogueRecord | |
| 27 | + | metadataUpdates []domain.CatalogueMetadataUpdate | |
| 27 | 28 | } | |
| 28 | 29 | ||
| 29 | 30 | func newDurableRecoveryCatalogue(records []domain.CatalogueRecord) *durableRecoveryCatalogue { | |
| @@ -61,6 +62,39 @@ func (c *durableRecoveryCatalogue) Replace(name string, record domain.CatalogueR | |||
| 61 | 62 | return nil | |
| 62 | 63 | } | |
| 63 | 64 | ||
| 65 | + | func (c *durableRecoveryCatalogue) UpdateMetadata(update domain.CatalogueMetadataUpdate) error { | |
| 66 | + | c.mu.Lock() | |
| 67 | + | defer c.mu.Unlock() | |
| 68 | + | record, ok := c.records[update.Name] | |
| 69 | + | if !ok { | |
| 70 | + | return errors.New("catalogue record not found") | |
| 71 | + | } | |
| 72 | + | if update.IncarnationID == (domain.IncarnationID{}) || record.IncarnationID != update.IncarnationID { | |
| 73 | + | return errors.New("catalogue incarnation changed") | |
| 74 | + | } | |
| 75 | + | if update.Cwd != nil { | |
| 76 | + | record.Cwd = *update.Cwd | |
| 77 | + | } | |
| 78 | + | if update.UpdatedAt != nil { | |
| 79 | + | record.UpdatedAt = *update.UpdatedAt | |
| 80 | + | } | |
| 81 | + | if update.LastUsedSeq != nil { | |
| 82 | + | record.LastUsedSeq = *update.LastUsedSeq | |
| 83 | + | } | |
| 84 | + | if update.TabNames != nil { | |
| 85 | + | record.TabNames = append([]string(nil), (*update.TabNames)...) | |
| 86 | + | } | |
| 87 | + | c.records[update.Name] = record | |
| 88 | + | c.metadataUpdates = append(c.metadataUpdates, update) | |
| 89 | + | return nil | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | func (c *durableRecoveryCatalogue) MetadataUpdates() []domain.CatalogueMetadataUpdate { | |
| 93 | + | c.mu.Lock() | |
| 94 | + | defer c.mu.Unlock() | |
| 95 | + | return append([]domain.CatalogueMetadataUpdate(nil), c.metadataUpdates...) | |
| 96 | + | } | |
| 97 | + | ||
| 64 | 98 | func (c *durableRecoveryCatalogue) Close() error { return nil } | |
| 65 | 99 | ||
| 66 | 100 | type durableRecoveryRepository struct { | |
| @@ -22,8 +22,9 @@ import ( | |||||
| 22 | 22 | ||||
| 23 | type durableRecoveryCatalogue struct { | 23 | type durableRecoveryCatalogue struct { | ||
| 24 | ports.Catalogue | 24 | ports.Catalogue | ||
| 25 | - | mu sync.Mutex | 25 | + | mu sync.Mutex |
| 26 | - | records map[string]domain.CatalogueRecord | 26 | + | records map[string]domain.CatalogueRecord |
| 27 | + | metadataUpdates []domain.CatalogueMetadataUpdate | |||
| 27 | } | 28 | } | ||
| 28 | 29 | ||||
| 29 | func newDurableRecoveryCatalogue(records []domain.CatalogueRecord) *durableRecoveryCatalogue { | 30 | func newDurableRecoveryCatalogue(records []domain.CatalogueRecord) *durableRecoveryCatalogue { | ||
| @@ -61,6 +62,39 @@ func (c *durableRecoveryCatalogue) Replace(name string, record domain.CatalogueR | |||||
| 61 | return nil | 62 | return nil | ||
| 62 | } | 63 | } | ||
| 63 | 64 | ||||
| 65 | + | func (c *durableRecoveryCatalogue) UpdateMetadata(update domain.CatalogueMetadataUpdate) error { | |||
| 66 | + | c.mu.Lock() | |||
| 67 | + | defer c.mu.Unlock() | |||
| 68 | + | record, ok := c.records[update.Name] | |||
| 69 | + | if !ok { | |||
| 70 | + | return errors.New("catalogue record not found") | |||
| 71 | + | } | |||
| 72 | + | if update.IncarnationID == (domain.IncarnationID{}) || record.IncarnationID != update.IncarnationID { | |||
| 73 | + | return errors.New("catalogue incarnation changed") | |||
| 74 | + | } | |||
| 75 | + | if update.Cwd != nil { | |||
| 76 | + | record.Cwd = *update.Cwd | |||
| 77 | + | } | |||
| 78 | + | if update.UpdatedAt != nil { | |||
| 79 | + | record.UpdatedAt = *update.UpdatedAt | |||
| 80 | + | } | |||
| 81 | + | if update.LastUsedSeq != nil { | |||
| 82 | + | record.LastUsedSeq = *update.LastUsedSeq | |||
| 83 | + | } | |||
| 84 | + | if update.TabNames != nil { | |||
| 85 | + | record.TabNames = append([]string(nil), (*update.TabNames)...) | |||
| 86 | + | } | |||
| 87 | + | c.records[update.Name] = record | |||
| 88 | + | c.metadataUpdates = append(c.metadataUpdates, update) | |||
| 89 | + | return nil | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | func (c *durableRecoveryCatalogue) MetadataUpdates() []domain.CatalogueMetadataUpdate { | |||
| 93 | + | c.mu.Lock() | |||
| 94 | + | defer c.mu.Unlock() | |||
| 95 | + | return append([]domain.CatalogueMetadataUpdate(nil), c.metadataUpdates...) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 64 | func (c *durableRecoveryCatalogue) Close() error { return nil } | 98 | func (c *durableRecoveryCatalogue) Close() error { return nil } | ||
| 65 | 99 | ||||
| 66 | type durableRecoveryRepository struct { | 100 | type durableRecoveryRepository struct { | ||
M internal/usecase/daemon/session.go +16 -15
| @@ -186,8 +186,8 @@ func (d *Daemon) touchMRU(sess *session) { | |||
| 186 | 186 | incarnation := sess.incarnation | |
| 187 | 187 | ephemeral := sess.ephemeral | |
| 188 | 188 | sess.mu.Unlock() | |
| 189 | - | if !ephemeral && d.persist != nil { | |
| 190 | - | if err := d.persist.TouchMRU(name, incarnation, seq); err != nil { | |
| 189 | + | if !ephemeral { | |
| 190 | + | if err := d.updateCatalogueMetadata(domain.CatalogueMetadataUpdate{Name: name, IncarnationID: incarnation, LastUsedSeq: &seq}); err != nil { | |
| 191 | 191 | d.log.Warn("touching persisted session recency failed", "err", err, "session", name) | |
| 192 | 192 | } | |
| 193 | 193 | } | |
| @@ -204,7 +204,7 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||
| 204 | 204 | // the persisted identity rather than receive a fresh timestamp. | |
| 205 | 205 | createdAt = stopped.createdAt | |
| 206 | 206 | incarnation = stopped.incarnation | |
| 207 | - | if authoritative, ok := d.persist.Record(name); ok { | |
| 207 | + | if authoritative, ok := d.catalogueRecord(name); ok { | |
| 208 | 208 | incarnation = authoritative.IncarnationID | |
| 209 | 209 | createdAt = authoritative.CreatedAt | |
| 210 | 210 | } | |
| @@ -290,13 +290,13 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||
| 290 | 290 | record := persist.Record{Name: name, IncarnationID: incarnation, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: createdAt, LastUsedSeq: lastUsedSeq, TabNames: names, RecoveryState: domain.RecoveryFresh} | |
| 291 | 291 | var err error | |
| 292 | 292 | if resuming { | |
| 293 | - | if _, ok := d.persist.Record(name); ok { | |
| 294 | - | err = d.persist.UpdateMetadata(record) | |
| 293 | + | if _, ok := d.catalogueRecord(name); ok { | |
| 294 | + | err = d.updateCatalogueMetadata(record.MetadataUpdate()) | |
| 295 | 295 | } else { | |
| 296 | - | err = d.persist.Create(record) | |
| 296 | + | err = d.createCatalogueRecord(record) | |
| 297 | 297 | } | |
| 298 | 298 | } else { | |
| 299 | - | err = d.persist.Create(record) | |
| 299 | + | err = d.createCatalogueRecord(record) | |
| 300 | 300 | } | |
| 301 | 301 | if err != nil { | |
| 302 | 302 | closeTabs(tabs) | |
| @@ -444,7 +444,7 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||
| 444 | 444 | record := sess.persistRecordLocked(time.Now().UnixNano()) | |
| 445 | 445 | ephemeral := sess.ephemeral | |
| 446 | 446 | if !ephemeral { | |
| 447 | - | if err := d.persist.UpdateMetadata(record); err != nil { | |
| 447 | + | if err := d.updateCatalogueMetadata(record.MetadataUpdate()); err != nil { | |
| 448 | 448 | sess.tabs = sess.tabs[:len(sess.tabs)-1] | |
| 449 | 449 | sess.active = oldActive | |
| 450 | 450 | if tb.cancel != nil { | |
| @@ -714,7 +714,7 @@ func (d *Daemon) renameSession(sess *session, name string) error { | |||
| 714 | 714 | // the old tombstone transaction. Ephemeral promotion has no old durable | |
| 715 | 715 | // identity and can create its first record directly. | |
| 716 | 716 | if (wasEphemeral || oldName != name) && d.persistEnabled { | |
| 717 | - | if err := d.persist.Create(record); err != nil { | |
| 717 | + | if err := d.createCatalogueRecord(record); err != nil { | |
| 718 | 718 | return rollback(err) | |
| 719 | 719 | } | |
| 720 | 720 | } | |
| @@ -774,7 +774,7 @@ func (d *Daemon) renameTab(sess *session, tb *tab, name string) error { | |||
| 774 | 774 | record := sess.persistRecordLocked(time.Now().UnixNano()) | |
| 775 | 775 | ephemeral := sess.ephemeral | |
| 776 | 776 | if !ephemeral { | |
| 777 | - | if err := d.persist.UpdateMetadata(record); err != nil { | |
| 777 | + | if err := d.updateCatalogueMetadata(record.MetadataUpdate()); err != nil { | |
| 778 | 778 | tb.name = oldName | |
| 779 | 779 | sess.mu.Unlock() | |
| 780 | 780 | d.mu.Unlock() | |
| @@ -853,7 +853,7 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) error { | |||
| 853 | 853 | record := sess.persistRecordLocked(time.Now().UnixNano()) | |
| 854 | 854 | ephemeral := sess.ephemeral | |
| 855 | 855 | if !ephemeral { | |
| 856 | - | if err := d.persist.UpdateMetadata(record); err != nil { | |
| 856 | + | if err := d.updateCatalogueMetadata(record.MetadataUpdate()); err != nil { | |
| 857 | 857 | d.log.Warn("persisting closed tab failed", "err", err, "session", name) | |
| 858 | 858 | } | |
| 859 | 859 | } | |
| @@ -910,12 +910,12 @@ func (d *Daemon) beginSnapshotPurge(name string, incarnation domain.IncarnationI | |||
| 910 | 910 | // a later live/offline kill can retry the idempotent source deletes. | |
| 911 | 911 | func (d *Daemon) finishSnapshotPurge(name string, incarnation domain.IncarnationID) error { | |
| 912 | 912 | if d.snapshotRepository == nil { | |
| 913 | - | return d.persist.Delete(name) | |
| 913 | + | return d.deleteCatalogueRecord(name) | |
| 914 | 914 | } | |
| 915 | 915 | if err := d.snapshotRepository.QuarantineDeletionSources(context.Background(), domain.DeletionTombstone{Name: name, IncarnationID: incarnation}, false); err != nil { | |
| 916 | 916 | return err | |
| 917 | 917 | } | |
| 918 | - | if err := d.persist.Delete(name); err != nil { | |
| 918 | + | if err := d.deleteCatalogueRecord(name); err != nil { | |
| 919 | 919 | return err | |
| 920 | 920 | } | |
| 921 | 921 | return d.snapshotRepository.DeleteDeletionTombstone(context.Background(), incarnation) | |
| @@ -935,7 +935,7 @@ func (d *Daemon) retryStoppedPurge(name string) error { | |||
| 935 | 935 | // stopped record retains the metadata-only deletion behavior. | |
| 936 | 936 | if d.snapshotRepository == nil { | |
| 937 | 937 | d.mu.Unlock() | |
| 938 | - | if err := d.persist.Delete(name); err != nil { | |
| 938 | + | if err := d.deleteCatalogueRecord(name); err != nil { | |
| 939 | 939 | return err | |
| 940 | 940 | } | |
| 941 | 941 | d.mu.Lock() | |
| @@ -1358,7 +1358,8 @@ func (d *Daemon) refreshSessionCwd(sess *session) { | |||
| 1358 | 1358 | ephemeral := sess.ephemeral | |
| 1359 | 1359 | sess.mu.Unlock() | |
| 1360 | 1360 | if !ephemeral { | |
| 1361 | - | if err := d.persist.Touch(name, incarnation, cwd, time.Now().UnixNano()); err != nil { | |
| 1361 | + | updatedAt := time.Now().UnixNano() | |
| 1362 | + | if err := d.updateCatalogueMetadata(domain.CatalogueMetadataUpdate{Name: name, IncarnationID: incarnation, Cwd: &cwd, UpdatedAt: &updatedAt}); err != nil { | |
| 1362 | 1363 | d.log.Warn("touching persisted session cwd failed", "err", err, "session", name) | |
| 1363 | 1364 | } | |
| 1364 | 1365 | markSnapshotDirty(sess) | |
| @@ -186,8 +186,8 @@ func (d *Daemon) touchMRU(sess *session) { | |||||
| 186 | incarnation := sess.incarnation | 186 | incarnation := sess.incarnation | ||
| 187 | ephemeral := sess.ephemeral | 187 | ephemeral := sess.ephemeral | ||
| 188 | sess.mu.Unlock() | 188 | sess.mu.Unlock() | ||
| 189 | - | if !ephemeral && d.persist != nil { | 189 | + | if !ephemeral { |
| 190 | - | if err := d.persist.TouchMRU(name, incarnation, seq); err != nil { | 190 | + | if err := d.updateCatalogueMetadata(domain.CatalogueMetadataUpdate{Name: name, IncarnationID: incarnation, LastUsedSeq: &seq}); err != nil { |
| 191 | d.log.Warn("touching persisted session recency failed", "err", err, "session", name) | 191 | d.log.Warn("touching persisted session recency failed", "err", err, "session", name) | ||
| 192 | } | 192 | } | ||
| 193 | } | 193 | } | ||
| @@ -204,7 +204,7 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||||
| 204 | // the persisted identity rather than receive a fresh timestamp. | 204 | // the persisted identity rather than receive a fresh timestamp. | ||
| 205 | createdAt = stopped.createdAt | 205 | createdAt = stopped.createdAt | ||
| 206 | incarnation = stopped.incarnation | 206 | incarnation = stopped.incarnation | ||
| 207 | - | if authoritative, ok := d.persist.Record(name); ok { | 207 | + | if authoritative, ok := d.catalogueRecord(name); ok { |
| 208 | incarnation = authoritative.IncarnationID | 208 | incarnation = authoritative.IncarnationID | ||
| 209 | createdAt = authoritative.CreatedAt | 209 | createdAt = authoritative.CreatedAt | ||
| 210 | } | 210 | } | ||
| @@ -290,13 +290,13 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||||
| 290 | record := persist.Record{Name: name, IncarnationID: incarnation, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: createdAt, LastUsedSeq: lastUsedSeq, TabNames: names, RecoveryState: domain.RecoveryFresh} | 290 | record := persist.Record{Name: name, IncarnationID: incarnation, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: createdAt, LastUsedSeq: lastUsedSeq, TabNames: names, RecoveryState: domain.RecoveryFresh} | ||
| 291 | var err error | 291 | var err error | ||
| 292 | if resuming { | 292 | if resuming { | ||
| 293 | - | if _, ok := d.persist.Record(name); ok { | 293 | + | if _, ok := d.catalogueRecord(name); ok { |
| 294 | - | err = d.persist.UpdateMetadata(record) | 294 | + | err = d.updateCatalogueMetadata(record.MetadataUpdate()) |
| 295 | } else { | 295 | } else { | ||
| 296 | - | err = d.persist.Create(record) | 296 | + | err = d.createCatalogueRecord(record) |
| 297 | } | 297 | } | ||
| 298 | } else { | 298 | } else { | ||
| 299 | - | err = d.persist.Create(record) | 299 | + | err = d.createCatalogueRecord(record) |
| 300 | } | 300 | } | ||
| 301 | if err != nil { | 301 | if err != nil { | ||
| 302 | closeTabs(tabs) | 302 | closeTabs(tabs) | ||
| @@ -444,7 +444,7 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||||
| 444 | record := sess.persistRecordLocked(time.Now().UnixNano()) | 444 | record := sess.persistRecordLocked(time.Now().UnixNano()) | ||
| 445 | ephemeral := sess.ephemeral | 445 | ephemeral := sess.ephemeral | ||
| 446 | if !ephemeral { | 446 | if !ephemeral { | ||
| 447 | - | if err := d.persist.UpdateMetadata(record); err != nil { | 447 | + | if err := d.updateCatalogueMetadata(record.MetadataUpdate()); err != nil { |
| 448 | sess.tabs = sess.tabs[:len(sess.tabs)-1] | 448 | sess.tabs = sess.tabs[:len(sess.tabs)-1] | ||
| 449 | sess.active = oldActive | 449 | sess.active = oldActive | ||
| 450 | if tb.cancel != nil { | 450 | if tb.cancel != nil { | ||
| @@ -714,7 +714,7 @@ func (d *Daemon) renameSession(sess *session, name string) error { | |||||
| 714 | // the old tombstone transaction. Ephemeral promotion has no old durable | 714 | // the old tombstone transaction. Ephemeral promotion has no old durable | ||
| 715 | // identity and can create its first record directly. | 715 | // identity and can create its first record directly. | ||
| 716 | if (wasEphemeral || oldName != name) && d.persistEnabled { | 716 | if (wasEphemeral || oldName != name) && d.persistEnabled { | ||
| 717 | - | if err := d.persist.Create(record); err != nil { | 717 | + | if err := d.createCatalogueRecord(record); err != nil { |
| 718 | return rollback(err) | 718 | return rollback(err) | ||
| 719 | } | 719 | } | ||
| 720 | } | 720 | } | ||
| @@ -774,7 +774,7 @@ func (d *Daemon) renameTab(sess *session, tb *tab, name string) error { | |||||
| 774 | record := sess.persistRecordLocked(time.Now().UnixNano()) | 774 | record := sess.persistRecordLocked(time.Now().UnixNano()) | ||
| 775 | ephemeral := sess.ephemeral | 775 | ephemeral := sess.ephemeral | ||
| 776 | if !ephemeral { | 776 | if !ephemeral { | ||
| 777 | - | if err := d.persist.UpdateMetadata(record); err != nil { | 777 | + | if err := d.updateCatalogueMetadata(record.MetadataUpdate()); err != nil { |
| 778 | tb.name = oldName | 778 | tb.name = oldName | ||
| 779 | sess.mu.Unlock() | 779 | sess.mu.Unlock() | ||
| 780 | d.mu.Unlock() | 780 | d.mu.Unlock() | ||
| @@ -853,7 +853,7 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) error { | |||||
| 853 | record := sess.persistRecordLocked(time.Now().UnixNano()) | 853 | record := sess.persistRecordLocked(time.Now().UnixNano()) | ||
| 854 | ephemeral := sess.ephemeral | 854 | ephemeral := sess.ephemeral | ||
| 855 | if !ephemeral { | 855 | if !ephemeral { | ||
| 856 | - | if err := d.persist.UpdateMetadata(record); err != nil { | 856 | + | if err := d.updateCatalogueMetadata(record.MetadataUpdate()); err != nil { |
| 857 | d.log.Warn("persisting closed tab failed", "err", err, "session", name) | 857 | d.log.Warn("persisting closed tab failed", "err", err, "session", name) | ||
| 858 | } | 858 | } | ||
| 859 | } | 859 | } | ||
| @@ -910,12 +910,12 @@ func (d *Daemon) beginSnapshotPurge(name string, incarnation domain.IncarnationI | |||||
| 910 | // a later live/offline kill can retry the idempotent source deletes. | 910 | // a later live/offline kill can retry the idempotent source deletes. | ||
| 911 | func (d *Daemon) finishSnapshotPurge(name string, incarnation domain.IncarnationID) error { | 911 | func (d *Daemon) finishSnapshotPurge(name string, incarnation domain.IncarnationID) error { | ||
| 912 | if d.snapshotRepository == nil { | 912 | if d.snapshotRepository == nil { | ||
| 913 | - | return d.persist.Delete(name) | 913 | + | return d.deleteCatalogueRecord(name) |
| 914 | } | 914 | } | ||
| 915 | if err := d.snapshotRepository.QuarantineDeletionSources(context.Background(), domain.DeletionTombstone{Name: name, IncarnationID: incarnation}, false); err != nil { | 915 | if err := d.snapshotRepository.QuarantineDeletionSources(context.Background(), domain.DeletionTombstone{Name: name, IncarnationID: incarnation}, false); err != nil { | ||
| 916 | return err | 916 | return err | ||
| 917 | } | 917 | } | ||
| 918 | - | if err := d.persist.Delete(name); err != nil { | 918 | + | if err := d.deleteCatalogueRecord(name); err != nil { |
| 919 | return err | 919 | return err | ||
| 920 | } | 920 | } | ||
| 921 | return d.snapshotRepository.DeleteDeletionTombstone(context.Background(), incarnation) | 921 | return d.snapshotRepository.DeleteDeletionTombstone(context.Background(), incarnation) | ||
| @@ -935,7 +935,7 @@ func (d *Daemon) retryStoppedPurge(name string) error { | |||||
| 935 | // stopped record retains the metadata-only deletion behavior. | 935 | // stopped record retains the metadata-only deletion behavior. | ||
| 936 | if d.snapshotRepository == nil { | 936 | if d.snapshotRepository == nil { | ||
| 937 | d.mu.Unlock() | 937 | d.mu.Unlock() | ||
| 938 | - | if err := d.persist.Delete(name); err != nil { | 938 | + | if err := d.deleteCatalogueRecord(name); err != nil { |
| 939 | return err | 939 | return err | ||
| 940 | } | 940 | } | ||
| 941 | d.mu.Lock() | 941 | d.mu.Lock() | ||
| @@ -1358,7 +1358,8 @@ func (d *Daemon) refreshSessionCwd(sess *session) { | |||||
| 1358 | ephemeral := sess.ephemeral | 1358 | ephemeral := sess.ephemeral | ||
| 1359 | sess.mu.Unlock() | 1359 | sess.mu.Unlock() | ||
| 1360 | if !ephemeral { | 1360 | if !ephemeral { | ||
| 1361 | - | if err := d.persist.Touch(name, incarnation, cwd, time.Now().UnixNano()); err != nil { | 1361 | + | updatedAt := time.Now().UnixNano() |
| 1362 | + | if err := d.updateCatalogueMetadata(domain.CatalogueMetadataUpdate{Name: name, IncarnationID: incarnation, Cwd: &cwd, UpdatedAt: &updatedAt}); err != nil { | |||
| 1362 | d.log.Warn("touching persisted session cwd failed", "err", err, "session", name) | 1363 | d.log.Warn("touching persisted session cwd failed", "err", err, "session", name) | ||
| 1363 | } | 1364 | } | ||
| 1364 | markSnapshotDirty(sess) | 1365 | markSnapshotDirty(sess) | ||
M internal/usecase/daemon/snapshot_restore.go +7 -5
| @@ -268,11 +268,13 @@ func (d *Daemon) persistAndRegisterRestoredSession(ctx context.Context, sess *se | |||
| 268 | 268 | } | |
| 269 | 269 | } | |
| 270 | 270 | } | |
| 271 | - | // The catalogue checkpoint already authorizes this runtime. Rewriting it | |
| 272 | - | // during restoration would discard committed/fallback recovery metadata. | |
| 273 | - | if d.catalogue == nil && d.persistEnabled { | |
| 274 | - | if err := d.persist.Create(sess.persistRecordLocked(sess.createdAt)); err != nil { | |
| 275 | - | return false, err | |
| 271 | + | // An existing catalogue checkpoint already authorizes this runtime. Only a | |
| 272 | + | // legacy snapshot without a catalogue record creates fresh metadata here. | |
| 273 | + | if d.persistEnabled { | |
| 274 | + | if _, ok := d.catalogueRecord(sess.name); !ok { | |
| 275 | + | if err := d.createCatalogueRecord(sess.persistRecordLocked(sess.createdAt)); err != nil { | |
| 276 | + | return false, err | |
| 277 | + | } | |
| 276 | 278 | } | |
| 277 | 279 | } | |
| 278 | 280 | d.mu.Lock() | |
| @@ -268,11 +268,13 @@ func (d *Daemon) persistAndRegisterRestoredSession(ctx context.Context, sess *se | |||||
| 268 | } | 268 | } | ||
| 269 | } | 269 | } | ||
| 270 | } | 270 | } | ||
| 271 | - | // The catalogue checkpoint already authorizes this runtime. Rewriting it | 271 | + | // An existing catalogue checkpoint already authorizes this runtime. Only a |
| 272 | - | // during restoration would discard committed/fallback recovery metadata. | 272 | + | // legacy snapshot without a catalogue record creates fresh metadata here. |
| 273 | - | if d.catalogue == nil && d.persistEnabled { | 273 | + | if d.persistEnabled { |
| 274 | - | if err := d.persist.Create(sess.persistRecordLocked(sess.createdAt)); err != nil { | 274 | + | if _, ok := d.catalogueRecord(sess.name); !ok { |
| 275 | - | return false, err | 275 | + | if err := d.createCatalogueRecord(sess.persistRecordLocked(sess.createdAt)); err != nil { |
| 276 | + | return false, err | |||
| 277 | + | } | |||
| 276 | } | 278 | } | ||
| 277 | } | 279 | } | ||
| 278 | d.mu.Lock() | 280 | d.mu.Lock() | ||
M internal/usecase/daemon/snapshot_restore_test.go +4 -4
| @@ -10,7 +10,6 @@ import ( | |||
| 10 | 10 | "github.com/stretchr/testify/require" | |
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/vev/internal/domain" | |
| 13 | - | "github.com/bnema/vev/internal/persist" | |
| 14 | 13 | "github.com/bnema/vev/internal/ports" | |
| 15 | 14 | "github.com/bnema/vev/internal/usecase/layout" | |
| 16 | 15 | recoveryusecase "github.com/bnema/vev/internal/usecase/recovery" | |
| @@ -263,8 +262,6 @@ func TestRestoredSessionMetadataUpdatePreservesCheckpointLineage(t *testing.T) { | |||
| 263 | 262 | snapshot := restoreAcceptanceSession(t, "restored") | |
| 264 | 263 | generation := acceptanceGeneration(t, snapshot, 9) | |
| 265 | 264 | repository := &snapshotAcceptanceRepository{names: []string{snapshot.Name}, generations: map[string]ports.SnapshotGeneration{snapshot.Name: generation}} | |
| 266 | - | store, _ := newMockStore(t) | |
| 267 | - | catalogue := persist.New(store) | |
| 268 | 265 | committed := domain.CheckpointRef{Generation: generation.Generation, ManifestDigest: snapcodec.ManifestDigest(generation.Manifest)} | |
| 269 | 266 | fallback1 := domain.CheckpointRef{Generation: 8, ManifestDigest: [32]byte{8}} | |
| 270 | 267 | fallback2 := domain.CheckpointRef{Generation: 7, ManifestDigest: [32]byte{7}} | |
| @@ -274,7 +271,7 @@ func TestRestoredSessionMetadataUpdatePreservesCheckpointLineage(t *testing.T) { | |||
| 274 | 271 | TabNames: []string{"before"}, RecoveryState: domain.RecoveryHealthy, | |
| 275 | 272 | Committed: &committed, Fallbacks: [2]*domain.CheckpointRef{&fallback1, &fallback2}, | |
| 276 | 273 | } | |
| 277 | - | require.NoError(t, catalogue.Create(record)) | |
| 274 | + | catalogue := newDurableRecoveryCatalogue([]domain.CatalogueRecord{record}) | |
| 278 | 275 | ||
| 279 | 276 | pty, release := newBlockingPTY(t) | |
| 280 | 277 | d := newTestDaemon(t, newFactory(t, pty), stubClock{}) | |
| @@ -291,6 +288,9 @@ func TestRestoredSessionMetadataUpdatePreservesCheckpointLineage(t *testing.T) { | |||
| 291 | 288 | require.NotNil(t, restored) | |
| 292 | 289 | require.NoError(t, d.renameTab(restored, restored.tabs[0], "after")) | |
| 293 | 290 | ||
| 291 | + | updates := catalogue.MetadataUpdates() | |
| 292 | + | require.Len(t, updates, 1, "the authoritative Catalogue port must receive restored-session metadata updates") | |
| 293 | + | require.Equal(t, record.IncarnationID, updates[0].IncarnationID) | |
| 294 | 294 | updated, ok := catalogue.Record(record.Name) | |
| 295 | 295 | require.True(t, ok) | |
| 296 | 296 | require.Equal(t, record.IncarnationID, updated.IncarnationID) | |
| @@ -10,7 +10,6 @@ import ( | |||||
| 10 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| 11 | 11 | ||||
| 12 | "github.com/bnema/vev/internal/domain" | 12 | "github.com/bnema/vev/internal/domain" | ||
| 13 | - | "github.com/bnema/vev/internal/persist" | |||
| 14 | "github.com/bnema/vev/internal/ports" | 13 | "github.com/bnema/vev/internal/ports" | ||
| 15 | "github.com/bnema/vev/internal/usecase/layout" | 14 | "github.com/bnema/vev/internal/usecase/layout" | ||
| 16 | recoveryusecase "github.com/bnema/vev/internal/usecase/recovery" | 15 | recoveryusecase "github.com/bnema/vev/internal/usecase/recovery" | ||
| @@ -263,8 +262,6 @@ func TestRestoredSessionMetadataUpdatePreservesCheckpointLineage(t *testing.T) { | |||||
| 263 | snapshot := restoreAcceptanceSession(t, "restored") | 262 | snapshot := restoreAcceptanceSession(t, "restored") | ||
| 264 | generation := acceptanceGeneration(t, snapshot, 9) | 263 | generation := acceptanceGeneration(t, snapshot, 9) | ||
| 265 | repository := &snapshotAcceptanceRepository{names: []string{snapshot.Name}, generations: map[string]ports.SnapshotGeneration{snapshot.Name: generation}} | 264 | repository := &snapshotAcceptanceRepository{names: []string{snapshot.Name}, generations: map[string]ports.SnapshotGeneration{snapshot.Name: generation}} | ||
| 266 | - | store, _ := newMockStore(t) | |||
| 267 | - | catalogue := persist.New(store) | |||
| 268 | committed := domain.CheckpointRef{Generation: generation.Generation, ManifestDigest: snapcodec.ManifestDigest(generation.Manifest)} | 265 | committed := domain.CheckpointRef{Generation: generation.Generation, ManifestDigest: snapcodec.ManifestDigest(generation.Manifest)} | ||
| 269 | fallback1 := domain.CheckpointRef{Generation: 8, ManifestDigest: [32]byte{8}} | 266 | fallback1 := domain.CheckpointRef{Generation: 8, ManifestDigest: [32]byte{8}} | ||
| 270 | fallback2 := domain.CheckpointRef{Generation: 7, ManifestDigest: [32]byte{7}} | 267 | fallback2 := domain.CheckpointRef{Generation: 7, ManifestDigest: [32]byte{7}} | ||
| @@ -274,7 +271,7 @@ func TestRestoredSessionMetadataUpdatePreservesCheckpointLineage(t *testing.T) { | |||||
| 274 | TabNames: []string{"before"}, RecoveryState: domain.RecoveryHealthy, | 271 | TabNames: []string{"before"}, RecoveryState: domain.RecoveryHealthy, | ||
| 275 | Committed: &committed, Fallbacks: [2]*domain.CheckpointRef{&fallback1, &fallback2}, | 272 | Committed: &committed, Fallbacks: [2]*domain.CheckpointRef{&fallback1, &fallback2}, | ||
| 276 | } | 273 | } | ||
| 277 | - | require.NoError(t, catalogue.Create(record)) | 274 | + | catalogue := newDurableRecoveryCatalogue([]domain.CatalogueRecord{record}) |
| 278 | 275 | ||||
| 279 | pty, release := newBlockingPTY(t) | 276 | pty, release := newBlockingPTY(t) | ||
| 280 | d := newTestDaemon(t, newFactory(t, pty), stubClock{}) | 277 | d := newTestDaemon(t, newFactory(t, pty), stubClock{}) | ||
| @@ -291,6 +288,9 @@ func TestRestoredSessionMetadataUpdatePreservesCheckpointLineage(t *testing.T) { | |||||
| 291 | require.NotNil(t, restored) | 288 | require.NotNil(t, restored) | ||
| 292 | require.NoError(t, d.renameTab(restored, restored.tabs[0], "after")) | 289 | require.NoError(t, d.renameTab(restored, restored.tabs[0], "after")) | ||
| 293 | 290 | ||||
| 291 | + | updates := catalogue.MetadataUpdates() | |||
| 292 | + | require.Len(t, updates, 1, "the authoritative Catalogue port must receive restored-session metadata updates") | |||
| 293 | + | require.Equal(t, record.IncarnationID, updates[0].IncarnationID) | |||
| 294 | updated, ok := catalogue.Record(record.Name) | 294 | updated, ok := catalogue.Record(record.Name) | ||
| 295 | require.True(t, ok) | 295 | require.True(t, ok) | ||
| 296 | require.Equal(t, record.IncarnationID, updated.IncarnationID) | 296 | require.Equal(t, record.IncarnationID, updated.IncarnationID) | ||
M internal/usecase/recovery/checkpoint_test.go +3 -0
| @@ -29,6 +29,9 @@ func (c *checkpointCatalogue) Record(name string) (domain.CatalogueRecord, bool) | |||
| 29 | 29 | return c.record, c.record.Name == name | |
| 30 | 30 | } | |
| 31 | 31 | func (c *checkpointCatalogue) Create(domain.CatalogueRecord) error { return nil } | |
| 32 | + | func (c *checkpointCatalogue) UpdateMetadata(domain.CatalogueMetadataUpdate) error { | |
| 33 | + | return nil | |
| 34 | + | } | |
| 32 | 35 | func (c *checkpointCatalogue) Replace(name string, record domain.CatalogueRecord) error { | |
| 33 | 36 | c.mu.Lock() | |
| 34 | 37 | defer c.mu.Unlock() | |
| @@ -29,6 +29,9 @@ func (c *checkpointCatalogue) Record(name string) (domain.CatalogueRecord, bool) | |||||
| 29 | return c.record, c.record.Name == name | 29 | return c.record, c.record.Name == name | ||
| 30 | } | 30 | } | ||
| 31 | func (c *checkpointCatalogue) Create(domain.CatalogueRecord) error { return nil } | 31 | func (c *checkpointCatalogue) Create(domain.CatalogueRecord) error { return nil } | ||
| 32 | + | func (c *checkpointCatalogue) UpdateMetadata(domain.CatalogueMetadataUpdate) error { | |||
| 33 | + | return nil | |||
| 34 | + | } | |||
| 32 | func (c *checkpointCatalogue) Replace(name string, record domain.CatalogueRecord) error { | 35 | func (c *checkpointCatalogue) Replace(name string, record domain.CatalogueRecord) error { | ||
| 33 | c.mu.Lock() | 36 | c.mu.Lock() | ||
| 34 | defer c.mu.Unlock() | 37 | defer c.mu.Unlock() | ||