detached eba0907

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
117117 // expireWarmProxy removes only the exact dormant proxy pointer and lifecycle
118118 // generation. The canonical d.mu -> core.mu -> proxy.mu order is held only for
119119 // validation/publication; cancellation, coordinator teardown, transport close,
120-// and daemon completion happen afterward.
120+// picker refresh, and daemon completion happen afterward.
121121 func (d *Daemon) expireWarmProxy(p *proxySession, token *proxyWarmTimer) bool {
122122 if d == nil || p == nil || token == nil {
123123 return false
@@ -176,6 +176,8 @@ func (d *Daemon) expireWarmProxy(p *proxySession, token *proxyWarmTimer) bool {
176176 p.finish()
177177 if empty {
178178 d.doneOnce.Do(func() { close(d.done) })
179+ } else {
180+ d.refreshRemoteOpenPickers()
179181 }
180182 return true
181183 }
@@ -117,7 +117,7 @@ func (d *Daemon) armProxyWarm(p *proxySession) bool {
117 // expireWarmProxy removes only the exact dormant proxy pointer and lifecycle117 // expireWarmProxy removes only the exact dormant proxy pointer and lifecycle
118 // generation. The canonical d.mu -> core.mu -> proxy.mu order is held only for118 // 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 false123 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 true182 return true
181 }183 }
M internal/usecase/daemon/remote_picker_test.go +3 -1
@@ -493,9 +493,11 @@ func TestRemoteRefreshPreservesRemoteCursorAcrossCacheToProxyUpgrade(t *testing.
493493 owner.overlays.picker = d.newPickerModel(ownerSession, pickerNavigate, moveSourceLocator{}, picker.SourceFilter{Session: key.ID(), RemoteKey: &key})
494494 owner.overlays.pickerIntent = pickerNavigate
495495 before, ok := owner.overlays.picker.Cursor()
496+ _, beforeSelectable := owner.overlays.picker.Selected()
496497 owner.overlays.pickerMu.Unlock()
497498 require.True(t, ok)
498499 require.Equal(t, &key, before.RemoteKey)
500+ require.True(t, beforeSelectable, "a reachable cache-derived remote row is selectable")
499501
500502 proxy := &proxySession{
501503 sessionCore: sessionCore{id: key.ID(), name: key.Display()},
@@ -515,7 +517,7 @@ func TestRemoteRefreshPreservesRemoteCursorAcrossCacheToProxyUpgrade(t *testing.
515517 require.True(t, ok)
516518 require.Equal(t, before.Session, after.Session)
517519 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")
519521 }
520522
521523 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 = pickerNavigate494 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) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close