detached db16c31

refactor(daemon): drop test-only graphics namespace release and cwd override inspector

commit
db16c3170dc0cd799ead274af92c3280575ee886
author
brice · · 2026-10-08 08:59 UTC
committer
brice · · 2026-10-08 09:31 UTC
parent
0f5321a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +20 -209
M internal/usecase/daemon/bar_refresh_test.go +2 -2
@@ -96,10 +96,10 @@ func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) {
9696 active := sess.tabs[0].focusedPane()
9797 active.stableID = "pane-default"
9898 active.pty = newScriptPTY(nil)
99- WithCwdReader(func(pid int) (string, error) {
99+ WithProcessInspector(&processInspectorFake{cwd: func(pid int) (string, error) {
100100 require.Equal(t, 4242, pid)
101101 return "/pane-repo", nil
102- })(d)
102+ }})(d)
103103
104104 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
105105 waitBarRefreshIdle(t, d)
@@ -96,10 +96,10 @@ func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) {
96 active := sess.tabs[0].focusedPane()96 active := sess.tabs[0].focusedPane()
97 active.stableID = "pane-default"97 active.stableID = "pane-default"
98 active.pty = newScriptPTY(nil)98 active.pty = newScriptPTY(nil)
99- WithCwdReader(func(pid int) (string, error) {99+ WithProcessInspector(&processInspectorFake{cwd: func(pid int) (string, error) {
100 require.Equal(t, 4242, pid)100 require.Equal(t, 4242, pid)
101 return "/pane-repo", nil101 return "/pane-repo", nil
102- })(d)102+ }})(d)
103 103
104 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))104 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
105 waitBarRefreshIdle(t, d)105 waitBarRefreshIdle(t, d)
M internal/usecase/daemon/daemon.go +1 -12
@@ -187,7 +187,7 @@ type Daemon struct {
187187 restoreDone chan struct{}
188188 restoreOnce sync.Once
189189 // proc inspects pane processes (cwd, foreground command, argv). Nil means
190- // no inspection is available; see WithProcessInspector and WithCwdReader.
190+ // no inspection is available; see WithProcessInspector.
191191 proc ports.ProcessInspector
192192 dirOrHome func(string) string
193193 // bindings is read by keys.Router through its own atomic pointer, so it is
@@ -527,17 +527,6 @@ func WithSnapshotGarbageCollection() Option {
527527 return func(d *Daemon) { d.snapshotGarbageCollection = true }
528528 }
529529
530-// WithCwdReader overrides the process cwd reader used for persistence tests.
531-// Every other inspection keeps using the installed process inspector, or stays
532-// unavailable when none is installed.
533-func WithCwdReader(fn func(int) (string, error)) Option {
534- return func(d *Daemon) {
535- if fn != nil {
536- d.proc = cwdOverrideInspector{base: unwrapCwdOverride(d.proc), cwd: fn}
537- }
538- }
539-}
540-
541530 // WithProcessInspector installs the platform process-inspection implementation.
542531 func WithProcessInspector(ins ports.ProcessInspector) Option {
543532 return func(d *Daemon) {
@@ -187,7 +187,7 @@ type Daemon struct {
187 restoreDone chan struct{}187 restoreDone chan struct{}
188 restoreOnce sync.Once188 restoreOnce sync.Once
189 // proc inspects pane processes (cwd, foreground command, argv). Nil means189 // proc inspects pane processes (cwd, foreground command, argv). Nil means
190- // no inspection is available; see WithProcessInspector and WithCwdReader.190+ // no inspection is available; see WithProcessInspector.
191 proc ports.ProcessInspector191 proc ports.ProcessInspector
192 dirOrHome func(string) string192 dirOrHome func(string) string
193 // bindings is read by keys.Router through its own atomic pointer, so it is193 // bindings is read by keys.Router through its own atomic pointer, so it is
@@ -527,17 +527,6 @@ func WithSnapshotGarbageCollection() Option {
527 return func(d *Daemon) { d.snapshotGarbageCollection = true }527 return func(d *Daemon) { d.snapshotGarbageCollection = true }
528 }528 }
529 529
530-// WithCwdReader overrides the process cwd reader used for persistence tests.
531-// Every other inspection keeps using the installed process inspector, or stays
532-// unavailable when none is installed.
533-func WithCwdReader(fn func(int) (string, error)) Option {
534- return func(d *Daemon) {
535- if fn != nil {
536- d.proc = cwdOverrideInspector{base: unwrapCwdOverride(d.proc), cwd: fn}
537- }
538- }
539-}
540-
541 // WithProcessInspector installs the platform process-inspection implementation.530 // WithProcessInspector installs the platform process-inspection implementation.
542 func WithProcessInspector(ins ports.ProcessInspector) Option {531 func WithProcessInspector(ins ports.ProcessInspector) Option {
543 return func(d *Daemon) {532 return func(d *Daemon) {
M internal/usecase/daemon/floating_spare_test.go +1 -1
@@ -124,7 +124,7 @@ func TestFloatingSpareIsSharedPerSession(t *testing.T) {
124124 d := newTestDaemon(t, factory, stubClock{})
125125 d.ApplyConfig(domain.Config{Floating: cfg})
126126 cwds := map[int]string{1: "/work", 2: tt.claimCwd}
127- WithCwdReader(func(pid int) (string, error) { return cwds[pid], nil })(d)
127+ WithProcessInspector(&processInspectorFake{cwd: func(pid int) (string, error) { return cwds[pid], nil }})(d)
128128 d.dirOrHome = func(cwd string) string { return cwd }
129129
130130 first := newTab(&pidPTY{pid: 1}, domain.Size{Cols: 120, Rows: 40})
@@ -124,7 +124,7 @@ func TestFloatingSpareIsSharedPerSession(t *testing.T) {
124 d := newTestDaemon(t, factory, stubClock{})124 d := newTestDaemon(t, factory, stubClock{})
125 d.ApplyConfig(domain.Config{Floating: cfg})125 d.ApplyConfig(domain.Config{Floating: cfg})
126 cwds := map[int]string{1: "/work", 2: tt.claimCwd}126 cwds := map[int]string{1: "/work", 2: tt.claimCwd}
127- WithCwdReader(func(pid int) (string, error) { return cwds[pid], nil })(d)127+ WithProcessInspector(&processInspectorFake{cwd: func(pid int) (string, error) { return cwds[pid], nil }})(d)
128 d.dirOrHome = func(cwd string) string { return cwd }128 d.dirOrHome = func(cwd string) string { return cwd }
129 129
130 first := newTab(&pidPTY{pid: 1}, domain.Size{Cols: 120, Rows: 40})130 first := newTab(&pidPTY{pid: 1}, domain.Size{Cols: 120, Rows: 40})
M internal/usecase/daemon/floating_test.go +1 -1
@@ -627,7 +627,7 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) {
627627 return cwd
628628 }
629629 if tt.liveCwd != "" {
630- WithCwdReader(func(int) (string, error) { return tt.liveCwd, nil })(d)
630+ WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return tt.liveCwd, nil }})(d)
631631 }
632632 tb := newFloatingTestTab(t)
633633 sess := &session{sessionCore: sessionCore{name: "work"}, cwd: tt.sessionCwd, tabs: []*tab{tb}, ctx: t.Context()}
@@ -627,7 +627,7 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) {
627 return cwd627 return cwd
628 }628 }
629 if tt.liveCwd != "" {629 if tt.liveCwd != "" {
630- WithCwdReader(func(int) (string, error) { return tt.liveCwd, nil })(d)630+ WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return tt.liveCwd, nil }})(d)
631 }631 }
632 tb := newFloatingTestTab(t)632 tb := newFloatingTestTab(t)
633 sess := &session{sessionCore: sessionCore{name: "work"}, cwd: tt.sessionCwd, tabs: []*tab{tb}, ctx: t.Context()}633 sess := &session{sessionCore: sessionCore{name: "work"}, cwd: tt.sessionCwd, tabs: []*tab{tb}, ctx: t.Context()}
M internal/usecase/daemon/graphics_namespace_pool.go +0 -14
@@ -70,20 +70,6 @@ func (p *graphicsNamespacePool) reserveLease(key string) (uint64, uint64) {
7070 return 0, 0
7171 }
7272
73-// releaseBase is the legacy base-only release. A quarantined block is never
74-// released by it.
75-func (p *graphicsNamespacePool) releaseBase(base uint64) {
76- if base == 0 || base%graphicsIDNamespaceSize != 1 {
77- return
78- }
79- block := (base - 1) / graphicsIDNamespaceSize
80- if _, quarantined := p.quarantines[block]; quarantined {
81- return
82- }
83- delete(p.reserved, block)
84- delete(p.fences, block)
85-}
86-
8773 func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) {
8874 if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {
8975 return
@@ -70,20 +70,6 @@ func (p *graphicsNamespacePool) reserveLease(key string) (uint64, uint64) {
70 return 0, 070 return 0, 0
71 }71 }
72 72
73-// releaseBase is the legacy base-only release. A quarantined block is never
74-// released by it.
75-func (p *graphicsNamespacePool) releaseBase(base uint64) {
76- if base == 0 || base%graphicsIDNamespaceSize != 1 {
77- return
78- }
79- block := (base - 1) / graphicsIDNamespaceSize
80- if _, quarantined := p.quarantines[block]; quarantined {
81- return
82- }
83- delete(p.reserved, block)
84- delete(p.fences, block)
85-}
86-
87 func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) {73 func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) {
88 if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {74 if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {
89 return75 return
M internal/usecase/daemon/graphics_output.go +4 -27
@@ -149,14 +149,10 @@ func graphicsNamespaceKey(sess *session, clientID [16]byte) string {
149149 return fmt.Sprintf("session:%s:%x:%x", sess.id, sess.incarnation, clientID)
150150 }
151151
152-// reserveGraphicsNamespaceLocked chooses a deterministic preferred block and
153-// linearly probes the bounded namespace table for a free block. Caller holds
154-// d.mu; probing makes equal session/client identities collision-safe too.
155-func (d *Daemon) reserveGraphicsNamespaceLocked(key string) uint64 {
156- base, _ := d.reserveGraphicsNamespaceLeaseLocked(key)
157- return base
158-}
159-
152+// reserveGraphicsNamespaceLeaseLocked chooses a deterministic preferred block
153+// and linearly probes the bounded namespace table for a free block, returning
154+// its base and fence. Caller holds d.mu; probing makes equal session/client
155+// identities collision-safe too.
160156 func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64) {
161157 if d == nil {
162158 return 0, 0
@@ -164,25 +160,6 @@ func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64
164160 return d.graphics.reserveLease(key)
165161 }
166162
167-func (d *Daemon) releaseGraphicsNamespace(base uint64) {
168- if d == nil || base == 0 || base%graphicsIDNamespaceSize != 1 {
169- return
170- }
171- d.mu.Lock()
172- d.releaseGraphicsNamespaceLocked(base)
173- d.mu.Unlock()
174-}
175-
176-// releaseGraphicsNamespaceLocked is used by resume publication while d.mu is
177-// already held. A quarantined block is intentionally not released by this
178-// legacy base-only helper; only its fenced cleanup lifecycle may retire it.
179-func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) {
180- if d == nil {
181- return
182- }
183- d.graphics.releaseBase(base)
184-}
185-
186163 func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) {
187164 if d == nil {
188165 return
@@ -149,14 +149,10 @@ func graphicsNamespaceKey(sess *session, clientID [16]byte) string {
149 return fmt.Sprintf("session:%s:%x:%x", sess.id, sess.incarnation, clientID)149 return fmt.Sprintf("session:%s:%x:%x", sess.id, sess.incarnation, clientID)
150 }150 }
151 151
152-// reserveGraphicsNamespaceLocked chooses a deterministic preferred block and152+// reserveGraphicsNamespaceLeaseLocked chooses a deterministic preferred block
153-// linearly probes the bounded namespace table for a free block. Caller holds153+// and linearly probes the bounded namespace table for a free block, returning
154-// d.mu; probing makes equal session/client identities collision-safe too.154+// its base and fence. Caller holds d.mu; probing makes equal session/client
155-func (d *Daemon) reserveGraphicsNamespaceLocked(key string) uint64 {155+// identities collision-safe too.
156- base, _ := d.reserveGraphicsNamespaceLeaseLocked(key)
157- return base
158-}
159-
160 func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64) {156 func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64) {
161 if d == nil {157 if d == nil {
162 return 0, 0158 return 0, 0
@@ -164,25 +160,6 @@ func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64
164 return d.graphics.reserveLease(key)160 return d.graphics.reserveLease(key)
165 }161 }
166 162
167-func (d *Daemon) releaseGraphicsNamespace(base uint64) {
168- if d == nil || base == 0 || base%graphicsIDNamespaceSize != 1 {
169- return
170- }
171- d.mu.Lock()
172- d.releaseGraphicsNamespaceLocked(base)
173- d.mu.Unlock()
174-}
175-
176-// releaseGraphicsNamespaceLocked is used by resume publication while d.mu is
177-// already held. A quarantined block is intentionally not released by this
178-// legacy base-only helper; only its fenced cleanup lifecycle may retire it.
179-func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) {
180- if d == nil {
181- return
182- }
183- d.graphics.releaseBase(base)
184-}
185-
186 func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) {163 func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) {
187 if d == nil {164 if d == nil {
188 return165 return
M internal/usecase/daemon/graphics_output_test.go +5 -7
@@ -353,16 +353,14 @@ func TestUnsupportedGraphicsWarningIsBoundedForLateSceneCreation(t *testing.T) {
353353 func TestGraphicsNamespacesAreDeterministicAndCollisionSafe(t *testing.T) {
354354 d := &Daemon{}
355355 d.mu.Lock()
356- first := d.reserveGraphicsNamespaceLocked("session:work:attachment")
357- second := d.reserveGraphicsNamespaceLocked("session:work:attachment")
358- d.mu.Unlock()
356+ defer d.mu.Unlock()
357+ first, firstFence := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment")
358+ second, _ := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment")
359359 require.NotZero(t, first)
360360 require.NotZero(t, second)
361361 require.NotEqual(t, first, second, "equal attachment/session keys still need a collision-safe namespace")
362- d.releaseGraphicsNamespace(first)
363- d.mu.Lock()
364- reused := d.reserveGraphicsNamespaceLocked("session:work:attachment")
365- d.mu.Unlock()
362+ d.releaseGraphicsNamespaceLeaseLocked(&graphicsOutputState{namespaceBase: first, namespaceFence: firstFence})
363+ reused, _ := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment")
366364 require.Equal(t, first, reused, "released namespaces should deterministically return to their preferred block")
367365 }
368366
@@ -353,16 +353,14 @@ func TestUnsupportedGraphicsWarningIsBoundedForLateSceneCreation(t *testing.T) {
353 func TestGraphicsNamespacesAreDeterministicAndCollisionSafe(t *testing.T) {353 func TestGraphicsNamespacesAreDeterministicAndCollisionSafe(t *testing.T) {
354 d := &Daemon{}354 d := &Daemon{}
355 d.mu.Lock()355 d.mu.Lock()
356- first := d.reserveGraphicsNamespaceLocked("session:work:attachment")356+ defer d.mu.Unlock()
357- second := d.reserveGraphicsNamespaceLocked("session:work:attachment")357+ first, firstFence := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment")
358- d.mu.Unlock()358+ second, _ := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment")
359 require.NotZero(t, first)359 require.NotZero(t, first)
360 require.NotZero(t, second)360 require.NotZero(t, second)
361 require.NotEqual(t, first, second, "equal attachment/session keys still need a collision-safe namespace")361 require.NotEqual(t, first, second, "equal attachment/session keys still need a collision-safe namespace")
362- d.releaseGraphicsNamespace(first)362+ d.releaseGraphicsNamespaceLeaseLocked(&graphicsOutputState{namespaceBase: first, namespaceFence: firstFence})
363- d.mu.Lock()363+ reused, _ := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment")
364- reused := d.reserveGraphicsNamespaceLocked("session:work:attachment")
365- d.mu.Unlock()
366 require.Equal(t, first, reused, "released namespaces should deterministically return to their preferred block")364 require.Equal(t, first, reused, "released namespaces should deterministically return to their preferred block")
367 }365 }
368 366
M internal/usecase/daemon/pane_titles.go +1 -1
@@ -187,7 +187,7 @@ func (d *Daemon) refreshPaneDisplayTitle(_ *session, p *pane, force bool) string
187187 // distinct from the display fallback: an OSC title must then stand alone,
188188 // and future floating panes can supply their command fallback at format time.
189189 processName := ""
190- if d.hasProcessDetails() && p.pty != nil {
190+ if d.proc != nil && p.pty != nil {
191191 if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 {
192192 if comm, err := d.proc.Comm(pgid); err == nil && strings.TrimSpace(comm) != "" {
193193 processName = strings.TrimSpace(comm)
@@ -187,7 +187,7 @@ func (d *Daemon) refreshPaneDisplayTitle(_ *session, p *pane, force bool) string
187 // distinct from the display fallback: an OSC title must then stand alone,187 // distinct from the display fallback: an OSC title must then stand alone,
188 // and future floating panes can supply their command fallback at format time.188 // and future floating panes can supply their command fallback at format time.
189 processName := ""189 processName := ""
190- if d.hasProcessDetails() && p.pty != nil {190+ if d.proc != nil && p.pty != nil {
191 if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 {191 if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 {
192 if comm, err := d.proc.Comm(pgid); err == nil && strings.TrimSpace(comm) != "" {192 if comm, err := d.proc.Comm(pgid); err == nil && strings.TrimSpace(comm) != "" {
193 processName = strings.TrimSpace(comm)193 processName = strings.TrimSpace(comm)
M internal/usecase/daemon/pane_titles_test.go +2 -0
@@ -205,6 +205,8 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) {
205205 require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback")
206206 }
207207
208+var errProcessInspectionUnavailable = errors.New("process inspection unavailable")
209+
208210 // processInspectorFake is a hand-written ports.ProcessInspector whose
209211 // behavior is set per test through func fields; an unset func reports an
210212 // error, the same as an inspector that cannot read the process.
@@ -205,6 +205,8 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) {
205 require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback")205 require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback")
206 }206 }
207 207
208+var errProcessInspectionUnavailable = errors.New("process inspection unavailable")
209+
208 // processInspectorFake is a hand-written ports.ProcessInspector whose210 // processInspectorFake is a hand-written ports.ProcessInspector whose
209 // behavior is set per test through func fields; an unset func reports an211 // behavior is set per test through func fields; an unset func reports an
210 // error, the same as an inspector that cannot read the process.212 // error, the same as an inspector that cannot read the process.
D internal/usecase/daemon/process_inspector.go +0 -58
@@ -1,58 +0,0 @@
1-package daemon
2-
3-import (
4- "errors"
5-
6- "github.com/bnema/vev/internal/ports"
7-)
8-
9-var errProcessInspectionUnavailable = errors.New("process inspection unavailable")
10-
11-// cwdOverrideInspector replaces only Cwd on top of an optional base inspector.
12-// With a nil base, Comm, Argv, and GroupArgv report the inspection as
13-// unavailable, matching a daemon that was given only a cwd reader.
14-type cwdOverrideInspector struct {
15- base ports.ProcessInspector
16- cwd func(int) (string, error)
17-}
18-
19-func (i cwdOverrideInspector) Cwd(pid int) (string, error) { return i.cwd(pid) }
20-
21-func (i cwdOverrideInspector) Comm(pid int) (string, error) {
22- if i.base == nil {
23- return "", errProcessInspectionUnavailable
24- }
25- return i.base.Comm(pid)
26-}
27-
28-func (i cwdOverrideInspector) Argv(pid int) ([]string, error) {
29- if i.base == nil {
30- return nil, errProcessInspectionUnavailable
31- }
32- return i.base.Argv(pid)
33-}
34-
35-func (i cwdOverrideInspector) GroupArgv(pgid, shellPid int) ([]string, error) {
36- if i.base == nil {
37- return nil, errProcessInspectionUnavailable
38- }
39- return i.base.GroupArgv(pgid, shellPid)
40-}
41-
42-// unwrapCwdOverride returns the inspector beneath a previous cwd override so
43-// repeated overrides do not stack.
44-func unwrapCwdOverride(ins ports.ProcessInspector) ports.ProcessInspector {
45- if o, ok := ins.(cwdOverrideInspector); ok {
46- return o.base
47- }
48- return ins
49-}
50-
51-// hasProcessDetails reports whether Comm, Argv, and GroupArgv are backed by a
52-// real inspector rather than a cwd-only override.
53-func (d *Daemon) hasProcessDetails() bool {
54- if o, ok := d.proc.(cwdOverrideInspector); ok {
55- return o.base != nil
56- }
57- return d.proc != nil
58-}
@@ -1,58 +0,0 @@
1-package daemon
2-
3-import (
4- "errors"
5-
6- "github.com/bnema/vev/internal/ports"
7-)
8-
9-var errProcessInspectionUnavailable = errors.New("process inspection unavailable")
10-
11-// cwdOverrideInspector replaces only Cwd on top of an optional base inspector.
12-// With a nil base, Comm, Argv, and GroupArgv report the inspection as
13-// unavailable, matching a daemon that was given only a cwd reader.
14-type cwdOverrideInspector struct {
15- base ports.ProcessInspector
16- cwd func(int) (string, error)
17-}
18-
19-func (i cwdOverrideInspector) Cwd(pid int) (string, error) { return i.cwd(pid) }
20-
21-func (i cwdOverrideInspector) Comm(pid int) (string, error) {
22- if i.base == nil {
23- return "", errProcessInspectionUnavailable
24- }
25- return i.base.Comm(pid)
26-}
27-
28-func (i cwdOverrideInspector) Argv(pid int) ([]string, error) {
29- if i.base == nil {
30- return nil, errProcessInspectionUnavailable
31- }
32- return i.base.Argv(pid)
33-}
34-
35-func (i cwdOverrideInspector) GroupArgv(pgid, shellPid int) ([]string, error) {
36- if i.base == nil {
37- return nil, errProcessInspectionUnavailable
38- }
39- return i.base.GroupArgv(pgid, shellPid)
40-}
41-
42-// unwrapCwdOverride returns the inspector beneath a previous cwd override so
43-// repeated overrides do not stack.
44-func unwrapCwdOverride(ins ports.ProcessInspector) ports.ProcessInspector {
45- if o, ok := ins.(cwdOverrideInspector); ok {
46- return o.base
47- }
48- return ins
49-}
50-
51-// hasProcessDetails reports whether Comm, Argv, and GroupArgv are backed by a
52-// real inspector rather than a cwd-only override.
53-func (d *Daemon) hasProcessDetails() bool {
54- if o, ok := d.proc.(cwdOverrideInspector); ok {
55- return o.base != nil
56- }
57- return d.proc != nil
58-}
D internal/usecase/daemon/process_inspector_test.go +0 -83
@@ -1,83 +0,0 @@
1-package daemon
2-
3-import (
4- "testing"
5-
6- "github.com/stretchr/testify/require"
7-)
8-
9-func TestProcessInspectorOptionsComposition(t *testing.T) {
10- cwd := func(int) (string, error) { return "/override", nil }
11- base := &processInspectorFake{
12- cwd: func(int) (string, error) { return "/base", nil },
13- comm: func(int) (string, error) { return "vim", nil },
14- }
15-
16- tests := []struct {
17- name string
18- opts []Option
19- wantDetails bool
20- wantCwd string
21- wantComm string
22- wantCommErr error
23- }{
24- {name: "none", wantDetails: false},
25- {
26- name: "cwd reader only",
27- opts: []Option{WithCwdReader(cwd)},
28- wantDetails: false,
29- wantCwd: "/override",
30- wantCommErr: errProcessInspectionUnavailable,
31- },
32- {
33- name: "inspector only",
34- opts: []Option{WithProcessInspector(base)},
35- wantDetails: true,
36- wantCwd: "/base",
37- wantComm: "vim",
38- },
39- {
40- name: "inspector then cwd reader keeps base details",
41- opts: []Option{WithProcessInspector(base), WithCwdReader(cwd)},
42- wantDetails: true,
43- wantCwd: "/override",
44- wantComm: "vim",
45- },
46- {
47- name: "cwd reader then inspector replaces override",
48- opts: []Option{WithCwdReader(cwd), WithProcessInspector(base)},
49- wantDetails: true,
50- wantCwd: "/base",
51- wantComm: "vim",
52- },
53- {
54- name: "repeated cwd readers do not stack",
55- opts: []Option{WithProcessInspector(base), WithCwdReader(cwd), WithCwdReader(cwd)},
56- wantDetails: true,
57- wantCwd: "/override",
58- wantComm: "vim",
59- },
60- }
61- for _, tt := range tests {
62- t.Run(tt.name, func(t *testing.T) {
63- d := &Daemon{}
64- for _, opt := range tt.opts {
65- opt(d)
66- }
67- require.Equal(t, tt.wantDetails, d.hasProcessDetails())
68- if d.proc == nil {
69- return
70- }
71- got, err := d.proc.Cwd(1)
72- require.NoError(t, err)
73- require.Equal(t, tt.wantCwd, got)
74- comm, err := d.proc.Comm(1)
75- require.ErrorIs(t, err, tt.wantCommErr)
76- require.Equal(t, tt.wantComm, comm)
77- if o, ok := d.proc.(cwdOverrideInspector); ok {
78- _, nested := o.base.(cwdOverrideInspector)
79- require.False(t, nested)
80- }
81- })
82- }
83-}
@@ -1,83 +0,0 @@
1-package daemon
2-
3-import (
4- "testing"
5-
6- "github.com/stretchr/testify/require"
7-)
8-
9-func TestProcessInspectorOptionsComposition(t *testing.T) {
10- cwd := func(int) (string, error) { return "/override", nil }
11- base := &processInspectorFake{
12- cwd: func(int) (string, error) { return "/base", nil },
13- comm: func(int) (string, error) { return "vim", nil },
14- }
15-
16- tests := []struct {
17- name string
18- opts []Option
19- wantDetails bool
20- wantCwd string
21- wantComm string
22- wantCommErr error
23- }{
24- {name: "none", wantDetails: false},
25- {
26- name: "cwd reader only",
27- opts: []Option{WithCwdReader(cwd)},
28- wantDetails: false,
29- wantCwd: "/override",
30- wantCommErr: errProcessInspectionUnavailable,
31- },
32- {
33- name: "inspector only",
34- opts: []Option{WithProcessInspector(base)},
35- wantDetails: true,
36- wantCwd: "/base",
37- wantComm: "vim",
38- },
39- {
40- name: "inspector then cwd reader keeps base details",
41- opts: []Option{WithProcessInspector(base), WithCwdReader(cwd)},
42- wantDetails: true,
43- wantCwd: "/override",
44- wantComm: "vim",
45- },
46- {
47- name: "cwd reader then inspector replaces override",
48- opts: []Option{WithCwdReader(cwd), WithProcessInspector(base)},
49- wantDetails: true,
50- wantCwd: "/base",
51- wantComm: "vim",
52- },
53- {
54- name: "repeated cwd readers do not stack",
55- opts: []Option{WithProcessInspector(base), WithCwdReader(cwd), WithCwdReader(cwd)},
56- wantDetails: true,
57- wantCwd: "/override",
58- wantComm: "vim",
59- },
60- }
61- for _, tt := range tests {
62- t.Run(tt.name, func(t *testing.T) {
63- d := &Daemon{}
64- for _, opt := range tt.opts {
65- opt(d)
66- }
67- require.Equal(t, tt.wantDetails, d.hasProcessDetails())
68- if d.proc == nil {
69- return
70- }
71- got, err := d.proc.Cwd(1)
72- require.NoError(t, err)
73- require.Equal(t, tt.wantCwd, got)
74- comm, err := d.proc.Comm(1)
75- require.ErrorIs(t, err, tt.wantCommErr)
76- require.Equal(t, tt.wantComm, comm)
77- if o, ok := d.proc.(cwdOverrideInspector); ok {
78- _, nested := o.base.(cwdOverrideInspector)
79- require.False(t, nested)
80- }
81- })
82- }
83-}
M internal/usecase/daemon/session_test.go +2 -2
@@ -1336,7 +1336,7 @@ func TestRefreshSessionCwdTouchesOnlyOnChange(t *testing.T) {
13361336 d := newTestDaemon(t, newFactory(t, p), stubClock{})
13371337 WithStore(t, store)(d)
13381338 cwd := "/tmp/work"
1339- WithCwdReader(func(int) (string, error) { return cwd, nil })(d)
1339+ WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return cwd, nil }})(d)
13401340
13411341 sess, err := createSessionForTest(d, "work", false, "/tmp/work", sz, terminalEnv{}, d.baseEnv)
13421342 require.NoError(t, err)
@@ -1529,7 +1529,7 @@ func TestNaturalExitStoppedButExplicitKillPurges(t *testing.T) {
15291529 store, state := newMockStore(t)
15301530 d := newTestDaemon(t, newFactorySeq(t, p1, p2), stubClock{})
15311531 WithStore(t, store)(d)
1532- WithCwdReader(func(int) (string, error) { return "/tmp/latest", nil })(d)
1532+ WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return "/tmp/latest", nil }})(d)
15331533
15341534 natural, err := createSessionForTest(d, "natural", false, "/tmp/old", sz, terminalEnv{}, d.baseEnv)
15351535 require.NoError(t, err)
@@ -1336,7 +1336,7 @@ func TestRefreshSessionCwdTouchesOnlyOnChange(t *testing.T) {
1336 d := newTestDaemon(t, newFactory(t, p), stubClock{})1336 d := newTestDaemon(t, newFactory(t, p), stubClock{})
1337 WithStore(t, store)(d)1337 WithStore(t, store)(d)
1338 cwd := "/tmp/work"1338 cwd := "/tmp/work"
1339- WithCwdReader(func(int) (string, error) { return cwd, nil })(d)1339+ WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return cwd, nil }})(d)
1340 1340
1341 sess, err := createSessionForTest(d, "work", false, "/tmp/work", sz, terminalEnv{}, d.baseEnv)1341 sess, err := createSessionForTest(d, "work", false, "/tmp/work", sz, terminalEnv{}, d.baseEnv)
1342 require.NoError(t, err)1342 require.NoError(t, err)
@@ -1529,7 +1529,7 @@ func TestNaturalExitStoppedButExplicitKillPurges(t *testing.T) {
1529 store, state := newMockStore(t)1529 store, state := newMockStore(t)
1530 d := newTestDaemon(t, newFactorySeq(t, p1, p2), stubClock{})1530 d := newTestDaemon(t, newFactorySeq(t, p1, p2), stubClock{})
1531 WithStore(t, store)(d)1531 WithStore(t, store)(d)
1532- WithCwdReader(func(int) (string, error) { return "/tmp/latest", nil })(d)1532+ WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return "/tmp/latest", nil }})(d)
1533 1533
1534 natural, err := createSessionForTest(d, "natural", false, "/tmp/old", sz, terminalEnv{}, d.baseEnv)1534 natural, err := createSessionForTest(d, "natural", false, "/tmp/old", sz, terminalEnv{}, d.baseEnv)
1535 require.NoError(t, err)1535 require.NoError(t, err)
M internal/usecase/daemon/snapshot_capture.go +1 -1
@@ -82,7 +82,7 @@ func (d *Daemon) captureSession(sess *session) bool {
8282 }
8383
8484 func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process {
85- if d == nil || pty == nil || shellPid <= 0 || !d.hasProcessDetails() {
85+ if d == nil || pty == nil || shellPid <= 0 || d.proc == nil {
8686 return nil
8787 }
8888 pgid, err := pty.ForegroundPgid()
@@ -82,7 +82,7 @@ func (d *Daemon) captureSession(sess *session) bool {
82 }82 }
83 83
84 func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process {84 func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process {
85- if d == nil || pty == nil || shellPid <= 0 || !d.hasProcessDetails() {85+ if d == nil || pty == nil || shellPid <= 0 || d.proc == nil {
86 return nil86 return nil
87 }87 }
88 pgid, err := pty.ForegroundPgid()88 pgid, err := pty.ForegroundPgid()

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