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}