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.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)
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.opts.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.opts.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.opts.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.opts.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}, Options{})
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}