spawn_security_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"sync/atomic"
6	"testing"
7
8	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12func TestFullSpawnQueueSlotsTerminalsExplicitRemainTransitionSafe(t *testing.T) {
13	for _, kind := range []string{"slot", "terminal", "explicit", "startup"} {
14		t.Run(kind, func(t *testing.T) {
15			c, _, commands := hiddenCaptureCommands(t)
16			c.configures.cw.reset()
17			c.cur().mon.RemoveWindow(1)
18			c.cur().mon.RemoveWindow(2)
19			var epoch atomic.Value
20			epoch.Store(ports.SecurityState{Generation: 2})
21			gate := portsmocks.NewMockSessionSecurity(t)
22			gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) })
23			c.ch.Security = gate
24			c.syncSecurity()
25			spawn := make(chan ports.SpawnRequest, 1)
26			c.ch.Spawn = spawn
27			sentinel := ports.SpawnRequest{Argv: []string{"occupied"}}
28			spawn <- sentinel
29			key := slotKey{workspace: "dev", index: 1}
30			c.cfg.Terminal.Command = []string{"terminal"}
31			c.cfg.Terminal.AutoOpen = "always"
32			c.ch.Terminal = kind == "terminal"
33			if kind == "slot" {
34				c.slots[key] = &slotState{argv: []string{"slot"}}
35				c.toSpawn = []slotKey{key}
36			}
37			if kind == "startup" {
38				c.startup = [][]string{{"startup"}}
39			}
40			c.binds = map[binding]Action{{key: "t"}: ActionSpawnTerminal}
41			attempt := func() error {
42				switch kind {
43				case "slot":
44					return c.spawnSlots(context.Background(), false)
45				case "terminal":
46					return c.spawnEmpty(context.Background())
47				case "explicit":
48					return c.handleInput(context.Background(), ports.KeyEvent{Keysym: "t", Pressed: true})
49				default:
50					c.spawnStartup(context.Background())
51					return nil
52				}
53			}
54			done := make(chan error, 1)
55			go func() { done <- attempt() }()
56			if err := reviewReceive(t, done); err != nil {
57				t.Fatal(err)
58			}
59			if len(spawn) != 1 {
60				t.Fatal("full queue changed")
61			}
62			if c.placement.anyPending() {
63				t.Fatal("unsent spawn kept claim")
64			}
65			if kind == "slot" && len(c.toSpawn) != 1 {
66				t.Fatal("deferred slot lost")
67			}
68			if kind == "startup" && len(c.startup) != 1 {
69				t.Fatal("deferred startup lost")
70			}
71			// A full queue never prevents the owner from adopting protection. Empty
72			// it only after transition; no path may enqueue in the protected epoch.
73			epoch.Store(ports.SecurityState{Generation: 3, Protected: true})
74			c.syncSecurity()
75			<-spawn
76			if err := attempt(); err != nil {
77				t.Fatal(err)
78			}
79			if len(spawn) != 0 {
80				t.Fatal("protected spawn enqueued")
81			}
82			epoch.Store(ports.SecurityState{Generation: 4})
83			c.syncSecurity()
84			if err := attempt(); err != nil {
85				t.Fatal(err)
86			}
87			req := reviewReceive(t, spawn)
88			if req.Security != (ports.SecurityState{Generation: 4}) {
89				t.Fatalf("missing owner stamp: %+v", req)
90			}
91			// Queued requests retain admission generation, not a later gate value.
92			epoch.Store(ports.SecurityState{Generation: 5, Protected: true})
93			if req.Security.Generation != 4 {
94				t.Fatal("queued request relabeled")
95			}
96			for len(commands) > 0 {
97				<-commands
98			}
99		})
100	}
101}
102
103func TestRunStartupDrainsWithoutFurtherOwnerEvents(t *testing.T) {
104	c, _, _ := hiddenCaptureCommands(t)
105	c.cfg.Startup = [][]string{{"one"}, {"two"}, {"three"}}
106	spawn := make(chan ports.SpawnRequest, 1)
107	spawn <- ports.SpawnRequest{Argv: []string{"occupied"}}
108	c.ch.Spawn = spawn
109	ctx, cancel := context.WithCancel(context.Background())
110	done := make(chan error, 1)
111	go func() { done <- c.Run(ctx) }()
112	if got := reviewReceive(t, spawn); got.Argv[0] != "occupied" {
113		t.Fatal(got)
114	}
115	for _, want := range []string{"one", "two", "three"} {
116		if got := reviewReceive(t, spawn); got.Argv[0] != want {
117			t.Fatal(got)
118		}
119	}
120	cancel()
121	if err := reviewReceive(t, done); err != nil {
122		t.Fatal(err)
123	}
124}
125
126func TestRunStartupPendingDisabledWhileLockedResumesOnUnlockEvent(t *testing.T) {
127	c, _, _ := hiddenCaptureCommands(t)
128	c.cfg.Startup = [][]string{{"one"}, {"two"}, {"three"}}
129	var epoch atomic.Value
130	locked := ports.SecurityState{Generation: 1, Protected: true}
131	epoch.Store(locked)
132	gate := portsmocks.NewMockSessionSecurity(t)
133	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) })
134	c.ch.Security = gate
135	spawn := make(chan ports.SpawnRequest, 1)
136	c.ch.Spawn = spawn
137	client := make(chan ports.ClientEvent, 1)
138	c.ch.Client = client
139	ctx, cancel := context.WithCancel(context.Background())
140	done := make(chan error, 1)
141	go func() { done <- c.Run(ctx) }()
142	client <- ports.SessionLockChanged{State: locked}
143	reviewReceive(t, c.ch.Scenes) // locked event has completed, no startup sent
144	select {
145	case req := <-spawn:
146		t.Fatalf("locked startup sent: %+v", req)
147	default:
148	}
149	unlocked := ports.SecurityState{Generation: 2}
150	epoch.Store(unlocked)
151	client <- ports.SessionLockChanged{State: unlocked}
152	for _, want := range []string{"one", "two", "three"} {
153		got := reviewReceive(t, spawn)
154		if got.Argv[0] != want || got.Security != unlocked {
155			t.Fatal(got)
156		}
157	}
158	cancel()
159	if err := reviewReceive(t, done); err != nil {
160		t.Fatal(err)
161	}
162}
163
164func TestStartupSuccessfulHandoffConsumesDespiteImmediateTransition(t *testing.T) {
165	c, _, _ := hiddenCaptureCommands(t)
166	c.startup = [][]string{{"once"}}
167	spawn := make(chan ports.SpawnRequest, 1)
168	c.ch.Spawn = spawn
169	var epoch atomic.Value
170	epoch.Store(ports.SecurityState{})
171	gate := portsmocks.NewMockSessionSecurity(t)
172	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState {
173		if len(spawn) != 0 {
174			epoch.Store(ports.SecurityState{Generation: 1, Protected: true})
175		}
176		return epoch.Load().(ports.SecurityState)
177	})
178	c.ch.Security = gate
179	c.spawnStartup(context.Background())
180	if len(c.startup) != 0 {
181		t.Fatal("successful handoff kept pending startup")
182	}
183	if !gate.Snapshot().Protected {
184		t.Fatal("test did not engage gate after handoff")
185	}
186	epoch.Store(ports.SecurityState{Generation: 2})
187	req := <-spawn
188	if req.Argv[0] != "once" || req.Security != (ports.SecurityState{}) {
189		t.Fatal(req)
190	}
191	c.syncSecurity()
192	c.spawnStartup(context.Background())
193	if len(spawn) != 0 {
194		t.Fatal("successful startup duplicated after release")
195	}
196}
197
198func TestRunSelectableStartupConsumedOnceAcrossReceiverTransition(t *testing.T) {
199	c, _, _ := hiddenCaptureCommands(t)
200	c.cfg.Startup = [][]string{{"once"}}
201	var epoch atomic.Value
202	epoch.Store(ports.SecurityState{})
203	gate := portsmocks.NewMockSessionSecurity(t)
204	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) })
205	c.ch.Security = gate
206	spawn := make(chan ports.SpawnRequest)
207	c.ch.Spawn = spawn
208	client := make(chan ports.ClientEvent, 1)
209	c.ch.Client = client
210	ctx, cancel := context.WithCancel(context.Background())
211	done := make(chan error, 1)
212	go func() { done <- c.Run(ctx) }()
213	req := reviewReceive(t, spawn)
214	if req.Argv[0] != "once" {
215		t.Fatal(req)
216	}
217	locked := ports.SecurityState{Generation: 1, Protected: true}
218	epoch.Store(locked)
219	client <- ports.SessionLockChanged{State: locked}
220	reviewReceive(t, c.ch.Scenes)
221	unlocked := ports.SecurityState{Generation: 2}
222	epoch.Store(unlocked)
223	client <- ports.SessionLockChanged{State: unlocked}
224	for {
225		if s := reviewReceive(t, c.ch.Scenes); s[0].Security == unlocked {
226			break
227		}
228	}
229	select {
230	case duplicate := <-spawn:
231		t.Fatalf("startup replayed: %+v", duplicate)
232	default:
233	}
234	cancel()
235	if err := reviewReceive(t, done); err != nil {
236		t.Fatal(err)
237	}
238	if len(c.startup) != 0 {
239		t.Fatal("startup not consumed")
240	}
241}

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