v0.10.0 d5d679d

security_review_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"errors"
6	"sync/atomic"
7	"testing"
8	"time"
9
10	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11	"github.com/bnema/neferwl/internal/ports"
12)
13
14func reviewReceive[T any](t *testing.T, ch <-chan T) T {
15	t.Helper()
16	select {
17	case v := <-ch:
18		return v
19	case <-time.After(time.Second):
20		t.Fatal("timed out")
21		var z T
22		return z
23	}
24}
25
26func TestInputOutputSwitchBackpressureKeepsAdmittedEpoch(t *testing.T) {
27	c, _, _ := hiddenCaptureCommands(t)
28	c.addScreen(ports.OutputInfo{Name: "B", Width: 200, Height: 100})
29	var state atomic.Value
30	state.Store(ports.SecurityState{})
31	gate := portsmocks.NewMockSessionSecurity(t)
32	var calls atomic.Int32
33	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState {
34		v := state.Load().(ports.SecurityState)
35		if calls.Add(1) == 2 {
36			// The epoch changes right after the check that admits the command.
37			state.Store(ports.SecurityState{Generation: 2})
38		}
39		return v
40	})
41	c.opts.Security = gate
42	c.sent.outputs = ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
43	commands := make(chan ports.ClientCommand, 8)
44	c.ch.Commands = commands
45	err := c.handleInput(context.Background(), ports.PointerMotion{X: 350, Y: 30})
46	cmd := reviewReceive(t, commands).(ports.SecurityCommand)
47	if cmd.State != (ports.SecurityState{}) {
48		t.Fatalf("old handler adopted new epoch: %+v", cmd)
49	}
50	if !errors.Is(err, errSecurityChanged) {
51		t.Fatalf("handler not aborted: %v", err)
52	}
53	if c.security != (ports.SecurityState{}) {
54		t.Fatal("publish adopted generation mid-input")
55	}
56	select {
57	case cmd := <-commands:
58		t.Fatalf("remaining old input command: %+v", cmd)
59	default:
60	}
61}
62
63func TestExactEpochAdmissionPreservesProducerModifierState(t *testing.T) {
64	c, _, _ := hiddenCaptureCommands(t)
65	state := ports.SecurityState{Generation: 2}
66	gate := portsmocks.NewMockSessionSecurity(t)
67	gate.EXPECT().Snapshot().Return(state)
68	c.opts.Security = gate
69	c.syncSecurity()
70	key := ports.KeyEvent{Keycode: 58, Keysym: "Control_L", Pressed: true, Mods: ports.ModCtrl, State: ports.ModState{Depressed: 1 << 19, Latched: 1 << 21, Locked: 1 << 23, Group: 2}}
71	ev, ok := c.admitInput(ports.SecurityInput{State: state, Event: key})
72	if !ok || ev.(ports.KeyEvent) != key {
73		t.Fatalf("producer state reinterpreted: %+v", ev)
74	}
75	if _, ok := c.admitInput(ports.SecurityInput{State: ports.SecurityState{Generation: 1}, Event: key}); ok {
76		t.Fatal("stale producer epoch admitted")
77	}
78}
79
80func TestEpochChangeSuppressesSpawnAndDefersSlots(t *testing.T) {
81	c, _, _ := hiddenCaptureCommands(t)
82	key := slotKey{workspace: "dev", index: 1}
83	c.slots[key] = &slotState{argv: []string{"terminal"}}
84	c.toSpawn = []slotKey{key}
85	var state atomic.Value
86	state.Store(ports.SecurityState{})
87	gate := portsmocks.NewMockSessionSecurity(t)
88	var calls atomic.Int32
89	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState {
90		v := state.Load().(ports.SecurityState)
91		if calls.Add(1) == 2 {
92			// The gate locks right after the check that admits the slot.
93			state.Store(ports.SecurityState{Generation: 1, Protected: true})
94		}
95		return v
96	})
97	c.opts.Security = gate
98	commands := make(chan ports.ClientCommand, 8)
99	c.ch.Commands = commands
100	spawn := make(chan ports.SpawnRequest, 2)
101	c.ch.Spawn = spawn
102	err := c.spawnSlots(context.Background(), false)
103	if err != nil {
104		t.Fatal(err)
105	}
106	if len(spawn) != 0 || len(c.toSpawn) != 1 || c.placement.slotPending(key) {
107		t.Fatal("spawn leaked or deferred slot lost")
108	}
109	// Retry after unlock, with a real buffered command sink.
110	c.ch.Commands = make(chan ports.ClientCommand, 8)
111	state.Store(ports.SecurityState{Generation: 2})
112	c.syncSecurity()
113	if err := c.spawnSlots(context.Background(), false); err != nil {
114		t.Fatal(err)
115	}
116	if len(spawn) != 1 || len(c.toSpawn) != 0 {
117		t.Fatal("deferred slot did not retry")
118	}
119}
120
121func TestTerminalEpochChangeSuppressesSpawnAndRestoresRetry(t *testing.T) {
122	c, _, _ := hiddenCaptureCommands(t)
123	c.cur().mon.RemoveWindow(1)
124	c.cur().mon.RemoveWindow(2)
125	c.opts.Terminal = true
126	c.cfg.Terminal.AutoOpen = "always"
127	c.cfg.Terminal.Command = []string{"terminal"}
128	var state atomic.Value
129	state.Store(ports.SecurityState{})
130	gate := portsmocks.NewMockSessionSecurity(t)
131	var calls atomic.Int32
132	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState {
133		v := state.Load().(ports.SecurityState)
134		if calls.Add(1) == 1 {
135			// The gate locks right after the check that admits the spawn.
136			state.Store(ports.SecurityState{Generation: 1, Protected: true})
137		}
138		return v
139	})
140	c.opts.Security = gate
141	commands := make(chan ports.ClientCommand, 8)
142	c.ch.Commands = commands
143	spawn := make(chan ports.SpawnRequest, 1)
144	c.ch.Spawn = spawn
145	err := c.spawnEmpty(context.Background())
146	if err != nil {
147		t.Fatal(err)
148	}
149	if len(spawn) != 0 || c.placement.anyPending() {
150		t.Fatal("terminal spawn/claim crossed lock")
151	}
152}
153
154// The pre-existing slot test can observe a startup placeholder snapshot: a
155// scene send happens before the real-output workspace publication. Reproduce
156// the empty snapshot contract without intentionally panicking a test runner.
157func TestStartupWorkspaceSnapshotHasNoOutputBeforeOutputAdded(t *testing.T) {
158	var cfg ports.Config
159	cfg.Keyboard.CmdKey = "super"
160	cfg.Layout.MaxColumns = 2
161	workspaces := make(chan ports.Workspaces, 1)
162	c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: workspaces}, Options{})
163	if err != nil {
164		t.Fatal(err)
165	}
166	c.publishWorkspaces()
167	initial := <-workspaces
168	if len(initial.Outputs) != 0 {
169		t.Fatal(initial)
170	}
171	c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80})
172	c.publishWorkspaces()
173	if next := <-workspaces; len(next.Outputs) != 1 {
174		t.Fatal(next)
175	}
176}

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 23

Tags 10

↑↓ moveenter openesc close