fix(remote): correct the cache-to-proxy row selectability expectation
TestRemoteRefreshPreservesRemoteCursorAcrossCacheToProxyUpgrade required a live proxy row to be non-selectable, but nothing on this branch has ever produced that. remoteProxyPickerView marks an unexpired proxy attach-ready, and remotePickerView already marks the reachable cache-derived row it replaces attach-ready too, so the assertion demanded that a row lose its selectability by going live. It survived because the package did not compile while the assertion was written, so the test never ran. Assert instead that selectability is preserved across the upgrade, and pin the pre-upgrade state so both halves of that invariant are covered. Also refresh open remote pickers when a warm proxy expires without emptying the daemon, so the row falls back to its cache-derived form.
- commit
- eba090711e26f797052cd43acd3238a103c120ee
- author
- brice · · 2026-07-31 13:25 UTC
- committer
- brice · · 2026-07-31 15:32 UTC
- parent
- a851fc9
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +6 -2
M internal/usecase/daemon/proxy_lifecycle.go +3 -1
| @@ -117,7 +117,7 @@ func (d *Daemon) armProxyWarm(p *proxySession) bool { | |||
| 117 | 117 | // expireWarmProxy removes only the exact dormant proxy pointer and lifecycle | |
| 118 | 118 | // generation. The canonical d.mu -> core.mu -> proxy.mu order is held only for | |
| 119 | 119 | // validation/publication; cancellation, coordinator teardown, transport close, | |
| 120 | - | // and daemon completion happen afterward. | |
| 120 | + | // picker refresh, and daemon completion happen afterward. | |
| 121 | 121 | func (d *Daemon) expireWarmProxy(p *proxySession, token *proxyWarmTimer) bool { | |
| 122 | 122 | if d == nil || p == nil || token == nil { | |
| 123 | 123 | return false | |
| @@ -176,6 +176,8 @@ func (d *Daemon) expireWarmProxy(p *proxySession, token *proxyWarmTimer) bool { | |||
| 176 | 176 | p.finish() | |
| 177 | 177 | if empty { | |
| 178 | 178 | d.doneOnce.Do(func() { close(d.done) }) | |
| 179 | + | } else { | |
| 180 | + | d.refreshRemoteOpenPickers() | |
| 179 | 181 | } | |
| 180 | 182 | return true | |
| 181 | 183 | } | |
| @@ -117,7 +117,7 @@ func (d *Daemon) armProxyWarm(p *proxySession) bool { | |||||
| 117 | // expireWarmProxy removes only the exact dormant proxy pointer and lifecycle | 117 | // expireWarmProxy removes only the exact dormant proxy pointer and lifecycle | ||
| 118 | // generation. The canonical d.mu -> core.mu -> proxy.mu order is held only for | 118 | // generation. The canonical d.mu -> core.mu -> proxy.mu order is held only for | ||
| 119 | // validation/publication; cancellation, coordinator teardown, transport close, | 119 | // validation/publication; cancellation, coordinator teardown, transport close, | ||
| 120 | - | // and daemon completion happen afterward. | 120 | + | // picker refresh, and daemon completion happen afterward. |
| 121 | func (d *Daemon) expireWarmProxy(p *proxySession, token *proxyWarmTimer) bool { | 121 | func (d *Daemon) expireWarmProxy(p *proxySession, token *proxyWarmTimer) bool { | ||
| 122 | if d == nil || p == nil || token == nil { | 122 | if d == nil || p == nil || token == nil { | ||
| 123 | return false | 123 | return false | ||
| @@ -176,6 +176,8 @@ func (d *Daemon) expireWarmProxy(p *proxySession, token *proxyWarmTimer) bool { | |||||
| 176 | p.finish() | 176 | p.finish() | ||
| 177 | if empty { | 177 | if empty { | ||
| 178 | d.doneOnce.Do(func() { close(d.done) }) | 178 | d.doneOnce.Do(func() { close(d.done) }) | ||
| 179 | + | } else { | |||
| 180 | + | d.refreshRemoteOpenPickers() | |||
| 179 | } | 181 | } | ||
| 180 | return true | 182 | return true | ||
| 181 | } | 183 | } | ||
M internal/usecase/daemon/remote_picker_test.go +3 -1
| @@ -493,9 +493,11 @@ func TestRemoteRefreshPreservesRemoteCursorAcrossCacheToProxyUpgrade(t *testing. | |||
| 493 | 493 | owner.overlays.picker = d.newPickerModel(ownerSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key}) | |
| 494 | 494 | owner.overlays.pickerIntent = pickerNavigate | |
| 495 | 495 | before, ok := owner.overlays.picker.Cursor() | |
| 496 | + | _, beforeSelectable := owner.overlays.picker.Selected() | |
| 496 | 497 | owner.overlays.pickerMu.Unlock() | |
| 497 | 498 | require.True(t, ok) | |
| 498 | 499 | require.Equal(t, &key, before.RemoteKey) | |
| 500 | + | require.True(t, beforeSelectable, "a reachable cache-derived remote row is selectable") | |
| 499 | 501 | ||
| 500 | 502 | proxy := &proxySession{ | |
| 501 | 503 | sessionCore: sessionCore{id: key.ID(), name: key.Display()}, | |
| @@ -515,7 +517,7 @@ func TestRemoteRefreshPreservesRemoteCursorAcrossCacheToProxyUpgrade(t *testing. | |||
| 515 | 517 | require.True(t, ok) | |
| 516 | 518 | require.Equal(t, before.Session, after.Session) | |
| 517 | 519 | require.Equal(t, before.RemoteKey, after.RemoteKey) | |
| 518 | - | require.False(t, selectable, "Phase 5 cache-to-live upgrade must not activate the proxy") | |
| 520 | + | require.True(t, selectable, "the cache-to-live upgrade must not drop the row's selectability") | |
| 519 | 521 | } | |
| 520 | 522 | ||
| 521 | 523 | func TestRemoteRefreshCancellationWhenLastPickerCloses(t *testing.T) { | |
| @@ -493,9 +493,11 @@ func TestRemoteRefreshPreservesRemoteCursorAcrossCacheToProxyUpgrade(t *testing. | |||||
| 493 | owner.overlays.picker = d.newPickerModel(ownerSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key}) | 493 | owner.overlays.picker = d.newPickerModel(ownerSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key}) | ||
| 494 | owner.overlays.pickerIntent = pickerNavigate | 494 | owner.overlays.pickerIntent = pickerNavigate | ||
| 495 | before, ok := owner.overlays.picker.Cursor() | 495 | before, ok := owner.overlays.picker.Cursor() | ||
| 496 | + | _, beforeSelectable := owner.overlays.picker.Selected() | |||
| 496 | owner.overlays.pickerMu.Unlock() | 497 | owner.overlays.pickerMu.Unlock() | ||
| 497 | require.True(t, ok) | 498 | require.True(t, ok) | ||
| 498 | require.Equal(t, &key, before.RemoteKey) | 499 | require.Equal(t, &key, before.RemoteKey) | ||
| 500 | + | require.True(t, beforeSelectable, "a reachable cache-derived remote row is selectable") | |||
| 499 | 501 | ||||
| 500 | proxy := &proxySession{ | 502 | proxy := &proxySession{ | ||
| 501 | sessionCore: sessionCore{id: key.ID(), name: key.Display()}, | 503 | sessionCore: sessionCore{id: key.ID(), name: key.Display()}, | ||
| @@ -515,7 +517,7 @@ func TestRemoteRefreshPreservesRemoteCursorAcrossCacheToProxyUpgrade(t *testing. | |||||
| 515 | require.True(t, ok) | 517 | require.True(t, ok) | ||
| 516 | require.Equal(t, before.Session, after.Session) | 518 | require.Equal(t, before.Session, after.Session) | ||
| 517 | require.Equal(t, before.RemoteKey, after.RemoteKey) | 519 | require.Equal(t, before.RemoteKey, after.RemoteKey) | ||
| 518 | - | require.False(t, selectable, "Phase 5 cache-to-live upgrade must not activate the proxy") | 520 | + | require.True(t, selectable, "the cache-to-live upgrade must not drop the row's selectability") |
| 519 | } | 521 | } | ||
| 520 | 522 | ||||
| 521 | func TestRemoteRefreshCancellationWhenLastPickerCloses(t *testing.T) { | 523 | func TestRemoteRefreshCancellationWhenLastPickerCloses(t *testing.T) { | ||