v0.2.0 13c6a90

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	entered := make(chan struct{}, 1)
33	var calls atomic.Int32
34	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState {
35		v := state.Load().(ports.SecurityState)
36		if calls.Add(1) == 2 {
37			entered <- struct{}{}
38		}
39		return v
40	})
41	c.ch.Security = gate
42	c.sentOutputs = ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
43	commands := make(chan ports.ClientCommand)
44	c.ch.Commands = commands
45	done := make(chan error, 1)
46	go func() { done <- c.handleInput(context.Background(), ports.PointerMotion{X: 350, Y: 30}) }()
47	reviewReceive(t, entered)
48	state.Store(ports.SecurityState{Generation: 2})
49	cmd := reviewReceive(t, commands).(ports.SecurityCommand)
50	if cmd.State != (ports.SecurityState{}) {
51		t.Fatalf("old handler adopted new epoch: %+v", cmd)
52	}
53	if err := reviewReceive(t, done); !errors.Is(err, errSecurityChanged) {
54		t.Fatalf("handler not aborted: %v", err)
55	}
56	if c.security != (ports.SecurityState{}) {
57		t.Fatal("publish adopted generation mid-input")
58	}
59	select {
60	case cmd := <-commands:
61		t.Fatalf("remaining old input command: %+v", cmd)
62	default:
63	}
64}
65
66func TestExactEpochAdmissionPreservesProducerModifierState(t *testing.T) {
67	c, _, _ := hiddenCaptureCommands(t)
68	state := ports.SecurityState{Generation: 2}
69	gate := portsmocks.NewMockSessionSecurity(t)
70	gate.EXPECT().Snapshot().Return(state)
71	c.ch.Security = gate
72	c.syncSecurity()
73	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}}
74	ev, ok := c.admitInput(ports.SecurityInput{State: state, Event: key})
75	if !ok || ev.(ports.KeyEvent) != key {
76		t.Fatalf("producer state reinterpreted: %+v", ev)
77	}
78	if _, ok := c.admitInput(ports.SecurityInput{State: ports.SecurityState{Generation: 1}, Event: key}); ok {
79		t.Fatal("stale producer epoch admitted")
80	}
81}
82
83func TestSlotsPendingBackpressureSuppressesSpawnAndDefersSlots(t *testing.T) {
84	c, _, _ := hiddenCaptureCommands(t)
85	key := slotKey{workspace: "dev", index: 1}
86	c.slots[key] = &slotState{argv: []string{"terminal"}}
87	c.toSpawn = []slotKey{key}
88	var state atomic.Value
89	state.Store(ports.SecurityState{})
90	gate := portsmocks.NewMockSessionSecurity(t)
91	checked := make(chan struct{}, 1)
92	var calls atomic.Int32
93	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState {
94		v := state.Load().(ports.SecurityState)
95		if calls.Add(1) == 2 {
96			checked <- struct{}{}
97		}
98		return v
99	})
100	c.ch.Security = gate
101	commands := make(chan ports.ClientCommand)
102	c.ch.Commands = commands
103	spawn := make(chan ports.SpawnRequest, 2)
104	c.ch.Spawn = spawn
105	done := make(chan error, 1)
106	go func() { done <- c.spawnSlots(context.Background(), false) }()
107	reviewReceive(t, checked)
108	state.Store(ports.SecurityState{Generation: 1, Protected: true})
109	cmd := reviewReceive(t, commands).(ports.SecurityCommand)
110	if _, ok := cmd.Command.(ports.SlotsPending); !ok {
111		t.Fatal(cmd)
112	}
113	if err := reviewReceive(t, done); err != nil {
114		t.Fatal(err)
115	}
116	if len(spawn) != 0 || len(c.toSpawn) != 1 || c.placement.slotPending(key) {
117		t.Fatal("spawn leaked or deferred slot lost")
118	}
119	// Retry after unlock, with a real buffered command sink.
120	c.ch.Commands = make(chan ports.ClientCommand, 8)
121	state.Store(ports.SecurityState{Generation: 2})
122	c.syncSecurity()
123	if err := c.spawnSlots(context.Background(), false); err != nil {
124		t.Fatal(err)
125	}
126	if len(spawn) != 1 || len(c.toSpawn) != 0 {
127		t.Fatal("deferred slot did not retry")
128	}
129}
130
131func TestTerminalPendingBackpressureSuppressesSpawnAndRestoresRetry(t *testing.T) {
132	c, _, _ := hiddenCaptureCommands(t)
133	c.cur().mon.RemoveWindow(1)
134	c.cur().mon.RemoveWindow(2)
135	c.ch.Terminal = true
136	c.cfg.Terminal.AutoOpen = "always"
137	c.cfg.Terminal.Command = []string{"terminal"}
138	var state atomic.Value
139	state.Store(ports.SecurityState{})
140	gate := portsmocks.NewMockSessionSecurity(t)
141	checked := make(chan struct{}, 1)
142	var calls atomic.Int32
143	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState {
144		v := state.Load().(ports.SecurityState)
145		if calls.Add(1) == 1 {
146			checked <- struct{}{}
147		}
148		return v
149	})
150	c.ch.Security = gate
151	commands := make(chan ports.ClientCommand)
152	c.ch.Commands = commands
153	spawn := make(chan ports.SpawnRequest, 1)
154	c.ch.Spawn = spawn
155	done := make(chan error, 1)
156	go func() { done <- c.spawnEmpty(context.Background()) }()
157	reviewReceive(t, checked)
158	state.Store(ports.SecurityState{Generation: 1, Protected: true})
159	reviewReceive(t, commands)
160	if err := reviewReceive(t, done); err != nil {
161		t.Fatal(err)
162	}
163	if len(spawn) != 0 || c.placement.anyPending() {
164		t.Fatal("terminal spawn/claim crossed lock")
165	}
166}
167
168// The pre-existing slot test can observe a startup placeholder snapshot: a
169// scene send happens before the real-output workspace publication. Reproduce
170// the empty snapshot contract without intentionally panicking a test runner.
171func TestStartupWorkspaceSnapshotHasNoOutputBeforeOutputAdded(t *testing.T) {
172	var cfg ports.Config
173	cfg.Keyboard.CmdKey = "super"
174	cfg.Layout.MaxColumns = 2
175	workspaces := make(chan ports.Workspaces, 1)
176	c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: workspaces})
177	if err != nil {
178		t.Fatal(err)
179	}
180	c.publishWorkspaces()
181	initial := <-workspaces
182	if len(initial.Outputs) != 0 {
183		t.Fatal(initial)
184	}
185	c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80})
186	c.publishWorkspaces()
187	if next := <-workspaces; len(next.Outputs) != 1 {
188		t.Fatal(next)
189	}
190}

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 25

Tags 10

↑↓ moveenter openesc close