package mockagent_test import ( "context" "io" "strings" "sync" "testing" "time" acp "github.com/coder/acp-go-sdk" "rickub.com/bots-garden/ori/internal/agent" "rickub.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 commands []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") case n.Update.AvailableCommandsUpdate != nil: h.updateKinds = append(h.updateKinds, "available_commands") for _, c := range n.Update.AvailableCommandsUpdate.AvailableCommands { h.commands = append(h.commands, c.Name) } } } 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() return h.kindsLocked() } // kindsLocked is kinds for callers already holding h.mu. func (h *panelHandler) kindsLocked() map[string]int { 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 TestSessionStartAnnouncesAvailableCommands(t *testing.T) { handler := &panelHandler{optionId: "allow-once"} connectPanel(t, handler) handler.mu.Lock() defer handler.mu.Unlock() if handler.kindsLocked()["available_commands"] != 1 { t.Fatalf("available_commands updates = %d, want 1 (all: %v)", handler.kindsLocked()["available_commands"], handler.kindsLocked()) } if len(handler.commands) != 2 || handler.commands[0] != "review" || handler.commands[1] != "compact" { t.Errorf("commands = %v, want [review compact]", handler.commands) } } func TestPromptEchoesAttachedResourceLinks(t *testing.T) { handler := &panelHandler{optionId: "allow-once"} session := connectPanel(t, handler) _, err := session.Prompt(context.Background(), []acp.ContentBlock{ acp.TextBlock("look at @src/main.go"), acp.ResourceLinkBlock("src/main.go", "file:///work/src/main.go"), }) if err != nil { t.Fatalf("Prompt returned an error: %v", err) } if all := strings.Join(handler.texts, ""); !strings.Contains(all, "[attached: src/main.go]") { t.Errorf("final message %q does not name the attachment", 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"]) } }