1package daemon
2
3import (
4 "context"
5 "io"
6 "sync"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/mock"
11 "github.com/stretchr/testify/require"
12
13 renderer "github.com/bnema/vev-vt"
14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/domain/layout"
16 "github.com/bnema/vev/internal/ports"
17 "github.com/bnema/vev/internal/protocol"
18 "github.com/bnema/vev/internal/protocol/wire"
19 "github.com/bnema/vev/internal/usecase/keys"
20 "github.com/bnema/vev/internal/usecase/ui"
21)
22
23func TestNoteAttentionFlagsBackgroundTab(t *testing.T) {
24 d, sess, _, _, releases := newManualTabSession(t, 2)
25 defer releases[0]()
26 defer releases[1]()
27 selectTestAttachmentTab(sess, 0)
28
29 d.noteAttention(sess, sess.tabs[1])
30
31 sess.mu.Lock()
32 defer sess.mu.Unlock()
33 require.True(t, sess.tabs[1].attention)
34 require.False(t, sess.tabs[1].attentionAt.IsZero())
35 require.False(t, sess.tabs[0].attention)
36}
37
38func TestNoteAttentionFlagsVisibleTabUntilPainted(t *testing.T) {
39 d, sess, _, _, releases := newManualTabSession(t, 1)
40 defer releases[0]()
41 selectTestAttachmentTab(sess, 0)
42
43 d.noteAttention(sess, sess.tabs[0])
44
45 sess.mu.Lock()
46 defer sess.mu.Unlock()
47 require.True(t, sess.tabs[0].attention)
48 require.False(t, sess.tabs[0].attentionAt.IsZero())
49}
50
51func TestPaintDoesNotAckActiveAttentionOnBlankPulseFrame(t *testing.T) {
52 d, sess, ac, sends, releases := newManualTabSession(t, 1)
53 defer releases[0]()
54 selectTestAttachmentTab(sess, 0)
55 d.setAttentionFrame(0)
56 d.noteAttention(sess, sess.tabs[0])
57
58 d.paint(sess, ac, true, nil)
59 data := mustOutputData(t, sends)
60 require.NotContains(t, string(data), string(ui.AttentionGlyph))
61
62 sess.mu.Lock()
63 require.True(t, sess.tabs[0].attention)
64 sess.mu.Unlock()
65
66 d.setAttentionFrame(1)
67 d.paint(sess, ac, true, nil)
68 data = mustOutputData(t, sends)
69 require.Contains(t, string(data), string(ui.AttentionGlyph))
70
71 sess.mu.Lock()
72 require.False(t, sess.tabs[0].attention)
73 sess.mu.Unlock()
74}
75
76func TestPTYReaderActiveVisibleAgentNotificationRendersBellBeforeAck(t *testing.T) {
77 tests := []struct {
78 name string
79 data []byte
80 }{
81 {name: "bell", data: []byte("\a")},
82 {name: "osc notify", data: []byte("\x1b]777;notify;Claude;needs input\x1b\\")},
83 {name: "osc progress complete", data: []byte("\x1b]9;4;1;50\a\x1b]9;4;0;100\a")},
84 }
85
86 for _, tt := range tests {
87 t.Run(tt.name, func(t *testing.T) {
88 pty := newScriptPTY([][]byte{tt.data})
89 d, sess, ac, sends := newManualSessionWithPTYs(t, pty)
90 selectTestAttachmentTab(sess, 0)
91 d.setAttentionFrame(1)
92
93 d.sessWg.Add(1)
94 go d.ptyReader(sess, sess.tabs[0], sess.tabs[0].focusedPane())
95
96 require.Eventually(t, func() bool {
97 sess.mu.Lock()
98 defer sess.mu.Unlock()
99 return sess.tabs[0].attention && !sess.tabs[0].attentionAt.IsZero()
100 }, 2*time.Second, 5*time.Millisecond)
101
102 d.paint(sess, ac, true, nil)
103 data := mustOutputData(t, sends)
104 require.Contains(t, string(data), string(ui.AttentionGlyph))
105
106 sess.mu.Lock()
107 require.False(t, sess.tabs[0].attention)
108 require.True(t, sess.tabs[0].attentionAt.IsZero())
109 sess.mu.Unlock()
110
111 cleared := mustOutputData(t, sends)
112 require.NotContains(t, string(cleared), string(ui.AttentionGlyph))
113
114 _ = pty.Close()
115 d.sessWg.Wait()
116 })
117 }
118}
119
120func TestNoteAttentionFlagsDetachedActiveTab(t *testing.T) {
121 d, sess, ac, _, releases := newManualTabSession(t, 1)
122 defer releases[0]()
123 selectTestAttachmentTab(sess, 0)
124 require.True(t, sess.detachIfCurrent(ac))
125
126 d.noteAttention(sess, sess.tabs[0])
127
128 sess.mu.Lock()
129 defer sess.mu.Unlock()
130 require.True(t, sess.tabs[0].attention)
131 require.False(t, sess.tabs[0].attentionAt.IsZero())
132}
133
134func TestPTYReaderBellMarksBackgroundTab(t *testing.T) {
135 p1, releasePTY1 := newBlockingPTY(t)
136 p2 := newScriptPTY([][]byte{[]byte("\a")})
137 d, sess, _, _ := newManualSessionWithPTYs(t, p1, p2)
138 defer releasePTY1()
139 selectTestAttachmentTab(sess, 0)
140
141 d.sessWg.Add(1)
142 go d.ptyReader(sess, sess.tabs[1], sess.tabs[1].focusedPane())
143
144 require.Eventually(t, func() bool {
145 sess.mu.Lock()
146 defer sess.mu.Unlock()
147 return sess.tabs[1].attention && !sess.tabs[1].attentionAt.IsZero()
148 }, 2*time.Second, 5*time.Millisecond)
149
150 _ = p2.Close()
151 d.sessWg.Wait()
152}
153
154// TestNoteAttentionDoesNotBlockOnWedgedOtherClient is the regression test for
155// the reader-blocks-on-a-slow-client bug: noteAttention runs on the PTY
156// reader goroutine (see ptyReader in render.go), so it must never repaint
157// synchronously — paint ends in Transport.Send, which has no deadline, and a
158// stalled client's socket would otherwise stall the reader of an unrelated
159// session. A second session's client whose Send never returns must not stop
160// noteAttention from returning promptly.
161func TestNoteAttentionDoesNotBlockOnWedgedOtherClient(t *testing.T) {
162 d, sess, _, _, releases := newManualTabSession(t, 2)
163 defer releases[0]()
164 defer releases[1]()
165 selectTestAttachmentTab(sess, 0)
166
167 trW := newMockServerConnection(t)
168 block := make(chan struct{})
169 t.Cleanup(func() { close(block) })
170 trW.EXPECT().Send(mock.Anything).RunAndReturn(func(wire.Envelope) error {
171 <-block
172 return nil
173 }).Maybe()
174 trW.EXPECT().Close().Return(nil).Maybe()
175 acW := &attachedClient{tr: trW, output: newOutputStateStream(), geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}}
176 acW.initOverlays()
177 sctxW, cancelW := context.WithCancel(d.serveCtx)
178 t.Cleanup(cancelW)
179 tabW := newTestTabWithContext(newScriptPTY(nil), sctxW, cancelW)
180 sessW := &session{sessionCore: sessionCore{id: "wedged", name: "wedged", attachments: map[*attachedClient]struct{}{acW: {}}}, ctx: sctxW, cancel: cancelW, tabs: []*tab{tabW}}
181 acW.setSession(sessW)
182 acW.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: acW}, nil)
183 d.sessions[sessW.id] = sessW
184
185 done := make(chan struct{})
186 go func() {
187 d.noteAttention(sess, sess.tabs[1])
188 close(done)
189 }()
190
191 select {
192 case <-done:
193 case <-time.After(2 * time.Second):
194 t.Fatal("noteAttention blocked on a wedged other-session client's Send")
195 }
196
197 sess.mu.Lock()
198 defer sess.mu.Unlock()
199 require.True(t, sess.tabs[1].attention)
200}
201
202type scriptPTY struct {
203 mu sync.Mutex
204 reads [][]byte
205 closed chan struct{}
206 once sync.Once
207}
208
209func newScriptPTY(reads [][]byte) *scriptPTY {
210 return &scriptPTY{reads: reads, closed: make(chan struct{})}
211}
212
213func (p *scriptPTY) Read(b []byte) (int, error) {
214 p.mu.Lock()
215 if len(p.reads) > 0 {
216 next := p.reads[0]
217 p.reads = p.reads[1:]
218 p.mu.Unlock()
219 return copy(b, next), nil
220 }
221 p.mu.Unlock()
222 <-p.closed
223 return 0, io.EOF
224}
225
226func (p *scriptPTY) Write(b []byte) (int, error) { return len(b), nil }
227func (p *scriptPTY) Close() error {
228 p.once.Do(func() { close(p.closed) })
229 return nil
230}
231func (p *scriptPTY) Resize(domain.Geometry) error { return nil }
232func (p *scriptPTY) Pid() int { return 4242 }
233func (p *scriptPTY) ForegroundPgid() (int, error) { return 4242, nil }
234
235func TestAckAttentionClearsOnlyPaintedVisibleTab(t *testing.T) {
236 d, sess, ac, sends, releases := newManualTabSession(t, 2)
237 defer releases[0]()
238 defer releases[1]()
239 selectTestAttachmentTab(sess, 0)
240 sess.mu.Lock()
241 sess.tabs[0].attention = true
242 sess.tabs[0].attentionAt = time.Unix(1, 0)
243 sess.tabs[1].attention = true
244 sess.tabs[1].attentionAt = time.Unix(2, 0)
245 sess.mu.Unlock()
246
247 d.paint(sess, ac, true, nil)
248 _ = mustOutputData(t, sends)
249 // The deferred repaint (from ackAttention) targets all attached clients,
250 // including this one, but the bars it would draw are already exactly what
251 // the full paint above just sent: no second frame should follow.
252 select {
253 case f := <-sends:
254 t.Fatalf("unexpected extra frame after ack: %s", envelopeMessageName(t, f.Payload))
255 case <-time.After(50 * time.Millisecond):
256 }
257
258 sess.mu.Lock()
259 defer sess.mu.Unlock()
260 require.False(t, sess.tabs[0].attention)
261 require.True(t, sess.tabs[0].attentionAt.IsZero())
262 require.True(t, sess.tabs[1].attention)
263 require.False(t, sess.tabs[1].attentionAt.IsZero())
264}
265
266func TestPaintPreservesBackgroundAttention(t *testing.T) {
267 d, sess, ac, sends, releases := newManualTabSession(t, 2)
268 defer releases[0]()
269 defer releases[1]()
270 selectTestAttachmentTab(sess, 0)
271 sess.mu.Lock()
272 sess.tabs[1].attention = true
273 sess.tabs[1].attentionAt = time.Unix(2, 0)
274 sess.mu.Unlock()
275 d.setAttentionFrame(1)
276
277 d.paint(sess, ac, true, nil)
278 data := mustOutputData(t, sends)
279
280 require.Contains(t, string(data), "")
281 sess.mu.Lock()
282 defer sess.mu.Unlock()
283 require.True(t, sess.tabs[1].attention)
284}
285
286func TestSwitchTabClearsAttentionEndToEnd(t *testing.T) {
287 d, sess, ac, sends, releases := newManualTabSession(t, 2)
288 defer releases[0]()
289 defer releases[1]()
290 selectTestAttachmentTab(sess, 0)
291 sess.mu.Lock()
292 sess.tabs[1].attention = true
293 sess.tabs[1].attentionAt = time.Unix(2, 0)
294 sess.mu.Unlock()
295
296 require.True(t, selectTestAttachmentTab(sess, 1))
297 d.paint(sess, ac, true, nil)
298 data := mustOutputData(t, sends)
299 position, ok := decodeServerMessage(t, awaitFrame(t, sends, "RoutePosition")).(protocol.RoutePosition)
300 require.True(t, ok)
301 require.Equal(t, domain.TabStableID(sess.tabs[1].stableID), position.ActiveTabID)
302 // The tab transition publishes its position, but the deferred repaint has
303 // no additional terminal bytes.
304 select {
305 case f := <-sends:
306 t.Fatalf("unexpected extra frame after ack: %s", envelopeMessageName(t, f.Payload))
307 case <-time.After(50 * time.Millisecond):
308 }
309
310 require.NotContains(t, string(data), "")
311 sess.mu.Lock()
312 defer sess.mu.Unlock()
313 require.False(t, sess.tabs[1].attention)
314 require.True(t, sess.tabs[1].attentionAt.IsZero())
315}
316
317func TestCloseRingingTabClearsClientBar(t *testing.T) {
318 d, sess, ac, sends, releases := newManualTabSession(t, 2)
319 defer releases[0]()
320 defer releases[1]()
321 selectTestAttachmentTab(sess, 0)
322 sess.mu.Lock()
323 ringing := sess.tabs[1]
324 ringing.attention = true
325 ringing.attentionAt = time.Unix(2, 0)
326 sess.mu.Unlock()
327 d.setAttentionFrame(1)
328
329 d.paint(sess, ac, true, nil)
330 before := mustOutputData(t, sends)
331 require.Contains(t, string(before), "")
332
333 require.NoError(t, d.closeTab(sess, ringing, true))
334 after := mustOutputData(t, sends)
335 require.NotContains(t, string(after), "")
336}
337
338// TestAnimationRepaintConfinedToBarRows is the regression test for the pulse
339// animator: repaintAttachedClients must diff against the warm barCache
340// (reset=false), so a repaint with nothing new to say sends no frame at all,
341// and a repaint following only a pulse-frame advance touches the bar rows
342// without re-emitting screen content.
343func TestAnimationRepaintConfinedToBarRows(t *testing.T) {
344 d, sess, ac, sends, releases := newManualTabSession(t, 2)
345 defer releases[0]()
346 defer releases[1]()
347 selectTestAttachmentTab(sess, 0)
348 sess.tabs[0].focusedPane().screen.Write([]byte("MIDSCREENMARKER"))
349 sess.mu.Lock()
350 sess.tabs[1].attention = true
351 sess.tabs[1].attentionAt = time.Unix(1, 0)
352 sess.mu.Unlock()
353
354 d.paint(sess, ac, true, nil)
355 full := mustOutputData(t, sends)
356 require.Contains(t, string(full), "MIDSCREENMARKER")
357
358 // Nothing changed: a repaint against the warm cache must send nothing.
359 d.repaintAttachedClients(sess)
360 select {
361 case f := <-sends:
362 t.Fatalf("unexpected frame on unchanged repaint: %s", envelopeMessageName(t, f.Payload))
363 case <-time.After(50 * time.Millisecond):
364 }
365
366 // Advance the pulse frame (only the bells' rendered style changes) and
367 // repaint: the bytes sent must not repaint screen content.
368 d.advanceAttentionFrame()
369 d.repaintAttachedClients(sess)
370 data := mustOutputData(t, sends)
371 require.NotContains(t, string(data), "MIDSCREENMARKER")
372}
373
374// TestComposeFrameAttentionFrameChangeDamagesOnlyBars pins the same invariant
375// through the production composition pipeline: with a warm cache and no pane
376// damage, changing only bars.attentionFrame damages only the chrome rows.
377func TestComposeFrameAttentionFrameChangeDamagesOnlyBars(t *testing.T) {
378 d, sess, _, _, releases := newManualTabSession(t, 2)
379 defer releases[0]()
380 defer releases[1]()
381 selectTestAttachmentTab(sess, 0)
382 sess.mu.Lock()
383 sess.tabs[1].attention = true
384 sess.tabs[1].attentionAt = time.Unix(1, 0)
385 sess.mu.Unlock()
386
387 tb := sess.tabs[0]
388 pane := tb.focusedPane()
389 pane.screen.Write([]byte("ATTENTION-PANE-CONTENT"))
390 bars := d.barStateFor(sess, "")
391 area := domain.Rect{Width: pane.screen.Columns(), Height: pane.screen.Rows()}
392 placement := layout.Placement{ID: pane.id, Content: area}
393 state := capturedRenderState{
394 reset: true,
395 layout: capturedTabLayout{area: area, focus: pane.id, placements: []layout.Placement{placement}, valid: true},
396 panes: []capturedPaneRenderState{{
397 id: pane.id, frame: captureTestFrame(pane.screen), placement: placement, focused: true,
398 damage: []renderer.Damage{renderer.FullRedraw()},
399 }},
400 bars: bars,
401 }
402 out := composeFrame(state, composeCacheInput{})
403 require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, out.damage)
404 require.Len(t, state.panes, 1)
405 require.Contains(t, rowText(out.frame.Row(1)), "ATTENTION-PANE-CONTENT")
406 firstContent := rowText(out.frame.Row(1))
407
408 // The pane was included in the warmed cache; it has no new damage when the
409 // attention pulse advances, so the subsequent compose must retain it.
410 bars.attentionFrame++
411 state.reset, state.bars = false, bars
412 state.panes[0].damage = nil
413 out = composeFrame(state, out.cache)
414 damage := out.damage
415 require.Len(t, state.panes, 1, "pane state must remain present when it has no damage")
416 require.Empty(t, state.panes[0].damage)
417 require.Equal(t, firstContent, rowText(out.frame.Row(1)), "unchanged pane content must remain in the composed frame")
418 require.Contains(t, rowText(out.frame.Row(1)), "ATTENTION-PANE-CONTENT")
419 require.NotEmpty(t, damage)
420 lastRow := pane.screen.Rows() + 1
421 for _, dmg := range damage {
422 require.Contains(t, []int{0, lastRow}, dmg.Y, "expected damage confined to bar rows, got y=%d", dmg.Y)
423 }
424}
425
426func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {
427 d, sess, ac, sends, releases := newManualTabSession(t, 3)
428 sess.mu.Lock()
429 sess.registerAttachmentLocked(ac)
430 sess.mu.Unlock()
431 d.ptys = newBlockingOpenFactory(t, d)
432 defer releases[0]()
433 defer releases[1]()
434 defer releases[2]()
435 selectTestAttachmentTab(sess, 0)
436 sess.mu.Lock()
437 sess.tabs[1].attention = true
438 sess.tabs[1].attentionAt = time.Unix(20, 0)
439 sess.tabs[2].attention = true
440 sess.tabs[2].attentionAt = time.Unix(10, 0)
441 sess.mu.Unlock()
442
443 d.handleInput(sess, ac, []byte("\x1ba"))
444
445 require.Equal(t, 2, testAttachmentTabIndex(sess))
446 requireFloatingInitialized(t, sess)
447 _ = mustOutputData(t, sends)
448}
449
450func TestJumpAttentionCrossesSessionsWhenNoLocalBells(t *testing.T) {
451 p1, release1 := newBlockingPTY(t)
452 p2, release2 := newBlockingPTY(t)
453 p3, release3 := newBlockingPTY(t)
454 defer release1()
455 defer release2()
456 defer release3()
457 d, sess1, ac, sends := newManualSessionWithPTYs(t, p1)
458 sctx2, cancel2 := context.WithCancel(d.serveCtx)
459 defer cancel2()
460 tab2a := newTestTabWithContext(p2, sctx2, cancel2)
461 tab2b := newTestTabWithContext(p3, sctx2, cancel2)
462 tab2a.attention = true
463 tab2a.attentionAt = time.Unix(9, 0)
464 tab2b.attention = true
465 tab2b.attentionAt = time.Unix(5, 0)
466 sess2 := &session{sessionCore: sessionCore{id: "other",
467 name: "other", incarnation: newTestLifecycle(t)}, ctx: sctx2,
468 cancel: cancel2,
469 tabs: []*tab{tab2a, tab2b},
470 }
471 d.sessions[sess2.id] = sess2
472
473 require.NoError(t, d.jumpAttention(sess1, ac))
474
475 require.Same(t, sess2, ac.currentSession())
476 require.Contains(t, sess2.snapshotAttachments(), ac)
477 require.Empty(t, sess1.snapshotAttachments())
478 require.Equal(t, 1, testAttachmentTabIndex(sess2))
479 _ = mustOutputData(t, sends)
480}
481
482func TestJumpAttentionNoopsWithNoBells(t *testing.T) {
483 d, sess, ac, sends, releases := newManualTabSession(t, 2)
484 defer releases[0]()
485 defer releases[1]()
486 selectTestAttachmentTab(sess, 0)
487
488 require.NoError(t, d.jumpAttention(sess, ac), "no target to jump to is not a failure")
489
490 require.Same(t, sess, ac.currentSession())
491 require.Equal(t, 0, testAttachmentTabIndex(sess))
492 select {
493 case f := <-sends:
494 t.Fatalf("unexpected frame on no-op jump: %s", envelopeMessageName(t, f.Payload))
495 case <-time.After(20 * time.Millisecond):
496 }
497}
498
499// TestJumpAttentionSwitchFailureReportsNotice drives jumpAttention's
500// cross-session path once a target has already been chosen, then makes the
501// commit itself fail (the origin session's client no longer matches ac —
502// simulating a client that detached between target selection and commit).
503// Unlike the no-target case, a target existed and the switch to it failed:
504// that is a genuine error, not a benign no-op, and must reach the user.
505func TestJumpAttentionSwitchFailureReportsNotice(t *testing.T) {
506 p1, release1 := newBlockingPTY(t)
507 p2, release2 := newBlockingPTY(t)
508 defer release1()
509 defer release2()
510 d, sess1, ac, _ := newManualSessionWithPTYs(t, p1)
511 sctx2, cancel2 := context.WithCancel(d.serveCtx)
512 defer cancel2()
513 tab2 := newTestTabWithContext(p2, sctx2, cancel2)
514 tab2.attention = true
515 tab2.attentionAt = time.Unix(9, 0)
516 sess2 := &session{sessionCore: sessionCore{id: "other", name: "other"}, ctx: sctx2, cancel: cancel2, tabs: []*tab{tab2}}
517 d.sessions[sess2.id] = sess2
518
519 sess1.mu.Lock()
520 clearAttachmentsForTestLocked(sess1)
521 sess1.mu.Unlock()
522
523 err := d.jumpAttention(sess1, ac)
524
525 require.Error(t, err)
526 require.Same(t, sess1, ac.currentSession(), "a failed switch must leave the client on its origin session")
527
528 d.reportError(sess1, err)
529 history := d.notices.history()
530 require.Len(t, history, 1)
531 require.Equal(t, domain.NoticeSessionUnavailable, history[0].Code)
532}
533
534func TestAttentionAnimatorParksAdvancesAndResets(t *testing.T) {
535 d, sess, _, sends, releases := newManualTabSession(t, 2)
536 defer releases[0]()
537 defer releases[1]()
538 clk := newManualAttentionClock()
539 d.clock = clk
540 selectTestAttachmentTab(sess, 0)
541
542 ctx, cancel := context.WithCancel(context.Background())
543 done := make(chan struct{})
544 go func() {
545 d.attentionAnimator(ctx)
546 close(done)
547 }()
548
549 select {
550 case <-clk.timers:
551 t.Fatal("parked animator requested timer with no attention")
552 case <-time.After(20 * time.Millisecond):
553 }
554
555 sess.mu.Lock()
556 sess.tabs[1].attention = true
557 sess.tabs[1].attentionAt = time.Unix(1, 0)
558 sess.mu.Unlock()
559 d.pokeAttentionTicker()
560
561 timer := clk.nextTimer(t)
562 timer.fire()
563 _ = mustOutputData(t, sends)
564 require.Equal(t, 1, d.attentionFrame())
565
566 sess.mu.Lock()
567 sess.tabs[1].attention = false
568 sess.tabs[1].attentionAt = time.Time{}
569 sess.mu.Unlock()
570 d.pokeAttentionTicker()
571
572 require.Eventually(t, func() bool { return d.attentionFrame() == 0 }, time.Second, time.Millisecond)
573 _ = mustOutputData(t, sends)
574 select {
575 case <-clk.timers:
576 t.Fatal("animator requested new timer after attention cleared")
577 case <-time.After(20 * time.Millisecond):
578 }
579
580 cancel()
581 require.Eventually(t, func() bool {
582 select {
583 case <-done:
584 return true
585 default:
586 return false
587 }
588 }, time.Second, time.Millisecond)
589}
590
591func TestPokeAttentionTickerIsNonBlockingCapOne(t *testing.T) {
592 d := New(nil, stubClock{}, nil)
593 require.Equal(t, 1, cap(d.animWake))
594 d.pokeAttentionTicker()
595 d.pokeAttentionTicker()
596 require.Len(t, d.animWake, 1)
597}
598
599type manualAttentionClock struct {
600 timers chan *manualAttentionTimer
601}
602
603func newManualAttentionClock() *manualAttentionClock {
604 return &manualAttentionClock{timers: make(chan *manualAttentionTimer, 8)}
605}
606
607func (c *manualAttentionClock) Now() time.Time { return time.Unix(100, 0) }
608func (c *manualAttentionClock) NewTimer(time.Duration) ports.Timer {
609 t := &manualAttentionTimer{ch: make(chan time.Time, 1)}
610 c.timers <- t
611 return t
612}
613func (c *manualAttentionClock) nextTimer(t *testing.T) *manualAttentionTimer {
614 t.Helper()
615 select {
616 case timer := <-c.timers:
617 return timer
618 case <-time.After(time.Second):
619 t.Fatal("timed out waiting for attention timer")
620 return nil
621 }
622}
623
624type manualAttentionTimer struct {
625 ch chan time.Time
626}
627
628func (t *manualAttentionTimer) C() <-chan time.Time { return t.ch }
629func (t *manualAttentionTimer) Reset(time.Duration) bool { return true }
630func (t *manualAttentionTimer) Stop() bool { return true }
631func (t *manualAttentionTimer) fire() { t.ch <- time.Now() }