| 261 | } |
| 262 | |
| 263 | func TestCounterInt64_TryUpdate_LocalFallback(t *testing.T) { |
| 264 | tests := []struct { |
| 265 | name string |
| 266 | setupMocks func(*MockConnector, *MockLock) |
| 267 | initialValue int64 |
| 268 | updateValue int64 |
| 269 | expectedValue int64 |
| 270 | eventual bool |
| 271 | eventualValue int64 |
| 272 | expectedCalls int |
| 273 | expectedRemote bool |
| 274 | }{ |
| 275 | { |
| 276 | name: "lock failure falls back to local", |
| 277 | setupMocks: func(c *MockConnector, l *MockLock) { |
| 278 | c.On("Lock", mock.Anything, "test", mock.Anything). |
| 279 | Return(nil, errors.New("lock failed")) |
| 280 | // Background push now publishes the local snapshot best-effort even when lock acquisition fails. |
| 281 | c.On("PublishCounterInt64", mock.Anything, "test", mock.Anything).Return(nil) |
| 282 | }, |
| 283 | initialValue: 5, |
| 284 | updateValue: 10, |
| 285 | expectedValue: 10, |
| 286 | expectedCalls: 1, |
| 287 | expectedRemote: false, |
| 288 | }, |
| 289 | { |
| 290 | name: "get failure falls back to local", |
| 291 | setupMocks: func(c *MockConnector, l *MockLock) { |
| 292 | c.On("Lock", mock.Anything, "test", mock.Anything).Return(l, nil) |
| 293 | l.On("Unlock", mock.Anything).Return(nil) |
| 294 | c.On("Get", mock.Anything, ConnectorMainIndex, "test", "value", nil). |
| 295 | Return([]byte(""), errors.New("get failed")) |
| 296 | c.On("Set", mock.Anything, "test", "value", mock.Anything, mock.Anything). |
| 297 | Return(errors.New("set failed")) |
| 298 | // Best-effort publish happens before any remote reconciliation attempt. |
| 299 | c.On("PublishCounterInt64", mock.Anything, "test", mock.Anything).Return(nil) |
| 300 | }, |
| 301 | initialValue: 5, |
| 302 | updateValue: 10, |
| 303 | expectedValue: 10, |
| 304 | expectedCalls: 1, |
| 305 | expectedRemote: false, |
| 306 | }, |
| 307 | { |
| 308 | name: "remote value higher than update", |
| 309 | setupMocks: func(c *MockConnector, l *MockLock) { |
| 310 | c.On("Lock", mock.Anything, "test", mock.Anything).Return(l, nil) |
| 311 | l.On("Unlock", mock.Anything).Return(nil) |
| 312 | remoteUpdatedAt := time.Now().UnixMilli() + 60_000 |
| 313 | c.On("Get", mock.Anything, ConnectorMainIndex, "test", "value", nil). |
| 314 | Return([]byte(fmt.Sprintf(`{"v":15,"t":%d,"b":"test"}`, remoteUpdatedAt)), nil) |
| 315 | // Background publish is best-effort. |
| 316 | c.On("PublishCounterInt64", mock.Anything, "test", mock.Anything).Return(nil).Maybe() |
| 317 | }, |
| 318 | initialValue: 5, |
| 319 | updateValue: 10, |
| 320 | // Foreground is local-only; background reconcile will eventually adopt/push 15. |