tool_gate_test.go RawHistoryFolder
1package modes
2
3import "testing"
4
5func newGateTestInteractive() *Interactive {
6 return &Interactive{
7 toolGate: map[string]int{},
8 dirty: make(chan struct{}, 1),
9 }
10}
11
12// While paced text is still draining, a tool that arrives mid-stream
13// must stay gated until the streaming buffer reaches the position
14// captured when the tool started.
15func TestToolGateHoldsToolUntilTextDrains(t *testing.T) {
16 i := newGateTestInteractive()
17
18 // Simulate a turn that streamed 10 runes already with 20 more
19 // queued in the pacer when the tool call arrives.
20 i.streamOn = true
21 i.streaming.WriteString("0123456789") // 10 painted
22 i.streamPending = []rune("01234567890123456789")
23
24 i.gateToolLocked("t1")
25 if got := i.toolGate["t1"]; got != 30 {
26 t.Fatalf("gate = %d, want 30 (10 painted + 20 pending)", got)
27 }
28
29 if i.toolGateOpenLocked("t1") {
30 t.Fatal("tool should be gated while only 10/30 runes painted")
31 }
32
33 // Drain halfway: still gated.
34 i.streaming.WriteString("0123456789") // 20 painted
35 if i.toolGateOpenLocked("t1") {
36 t.Fatal("tool should still be gated at 20/30")
37 }
38
39 // Reach the gate: now visible.
40 i.streaming.WriteString("0123456789") // 30 painted
41 if !i.toolGateOpenLocked("t1") {
42 t.Fatal("tool should be visible once streaming reaches the gate")
43 }
44}
45
46// A tool call on a turn with no active text stream shows immediately.
47func TestToolGateOpenWhenNoStream(t *testing.T) {
48 i := newGateTestInteractive()
49 i.streamOn = false
50
51 i.gateToolLocked("t1")
52 if got := i.toolGate["t1"]; got != 0 {
53 t.Fatalf("gate = %d, want 0 for non-streaming turn", got)
54 }
55 if !i.toolGateOpenLocked("t1") {
56 t.Fatal("tool should be visible immediately when no text is streaming")
57 }
58}
59
60// First registration wins: a later EvToolCall must not move an
61// existing gate (e.g. push it forward as more text arrives).
62func TestToolGateFirstRegistrationWins(t *testing.T) {
63 i := newGateTestInteractive()
64 i.streamOn = true
65 i.streaming.WriteString("01234") // 5
66 i.streamPending = []rune("01234")
67
68 i.gateToolLocked("t1") // gate 10
69 first := i.toolGate["t1"]
70
71 // More text queues up, then the same tool is re-registered.
72 i.streamPending = append(i.streamPending, []rune("567890")...)
73 i.gateToolLocked("t1")
74
75 if i.toolGate["t1"] != first {
76 t.Fatalf("gate moved from %d to %d; first registration must win", first, i.toolGate["t1"])
77 }
78}
79
80// Once streaming finalizes, the buffer resets to length 0; gates that
81// were already satisfied must not re-hide their tools.
82func TestOpenAllToolGatesSurvivesStreamReset(t *testing.T) {
83 i := newGateTestInteractive()
84 i.streamOn = true
85 i.streaming.WriteString("0123456789")
86 i.gateToolLocked("t1") // gate 10, satisfied
87
88 if !i.toolGateOpenLocked("t1") {
89 t.Fatal("precondition: tool should be open before reset")
90 }
91
92 // Finalize the stream (mirrors resetStreamingStateLocked / pacer
93 // flush): buffer resets to 0 but gates are opened first.
94 i.openAllToolGatesLocked()
95 i.streaming.Reset()
96 i.streamOn = false
97
98 if !i.toolGateOpenLocked("t1") {
99 t.Fatal("tool re-hidden after stream reset; gate should have been opened")
100 }
101}