1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
package mockagent_test
import (
"context"
"io"
"strings"
"sync"
"testing"
"time"
acp "github.com/coder/acp-go-sdk"
"github.com/bots-garden/ori/internal/agent"
"github.com/bots-garden/ori/internal/mockagent"
)
// panelHandler records everything the mock streams, and answers the
// permission request with a fixed choice.
type panelHandler struct {
mu sync.Mutex
optionId string
texts []string
updateKinds []string
permissions int
}
func (h *panelHandler) HandleSessionUpdate(_ context.Context, n acp.SessionNotification) {
h.mu.Lock()
defer h.mu.Unlock()
switch {
case n.Update.AgentMessageChunk != nil:
h.updateKinds = append(h.updateKinds, "message")
if t := n.Update.AgentMessageChunk.Content.Text; t != nil {
h.texts = append(h.texts, t.Text)
}
case n.Update.AgentThoughtChunk != nil:
h.updateKinds = append(h.updateKinds, "thought")
case n.Update.ToolCall != nil:
h.updateKinds = append(h.updateKinds, "tool_call")
case n.Update.ToolCallUpdate != nil:
h.updateKinds = append(h.updateKinds, "tool_call_update")
case n.Update.Plan != nil:
h.updateKinds = append(h.updateKinds, "plan")
}
}
func (h *panelHandler) HandlePermissionRequest(_ context.Context, req acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.permissions++
_ = req
return acp.RequestPermissionResponse{Outcome: acp.NewRequestPermissionOutcomeSelected(acp.PermissionOptionId(h.optionId))}, nil
}
func (h *panelHandler) kinds() map[string]int {
h.mu.Lock()
defer h.mu.Unlock()
counts := map[string]int{}
for _, k := range h.updateKinds {
counts[k]++
}
return counts
}
// connectPanel wires the mock agent to ori's real client code over pipes.
func connectPanel(t *testing.T, handler agent.Handler) *agent.Session {
t.Helper()
clientToAgentR, clientToAgentW := io.Pipe()
agentToClientR, agentToClientW := io.Pipe()
mock := mockagent.New()
go func() { _ = mock.Run(clientToAgentR, agentToClientW) }()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
t.Cleanup(cancel)
session, err := agent.Connect(ctx, clientToAgentW, agentToClientR, agent.Options{
Cwd: t.TempDir(),
Handler: handler,
})
if err != nil {
t.Fatalf("Connect to mock agent failed: %v", err)
}
return session
}
func TestScenarioStreamsEveryPanelFeature(t *testing.T) {
handler := &panelHandler{optionId: "allow-once"}
session := connectPanel(t, handler)
stop, err := session.PromptText(context.Background(), "show me everything")
if err != nil {
t.Fatalf("PromptText returned an error: %v", err)
}
if stop != acp.StopReasonEndTurn {
t.Errorf("stop reason = %q, want end_turn", stop)
}
counts := handler.kinds()
for kind, want := range map[string]int{"thought": 1, "plan": 2, "tool_call": 2, "tool_call_update": 2} {
if counts[kind] < want {
t.Errorf("streamed %d %q updates, want at least %d (all: %v)", counts[kind], kind, want, counts)
}
}
if handler.permissions != 1 {
t.Errorf("permission requests = %d, want 1", handler.permissions)
}
if all := strings.Join(handler.texts, ""); !strings.Contains(all, "show me everything") {
t.Errorf("final message %q does not echo the prompt", all)
}
}
func TestScenarioStopsOnRejectedPermission(t *testing.T) {
handler := &panelHandler{optionId: "reject-once"}
session := connectPanel(t, handler)
stop, err := session.PromptText(context.Background(), "try to write")
if err != nil {
t.Fatalf("PromptText returned an error: %v", err)
}
if stop != acp.StopReasonRefusal {
t.Errorf("stop reason = %q, want refusal after a rejected permission", stop)
}
if counts := handler.kinds(); counts["tool_call"] != 1 {
t.Errorf("tool calls = %d, want only the read call (no edit after rejection)", counts["tool_call"])
}
}
|