forked from bots-garden/ori
| ✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS | 1 | import { beforeEach, describe, expect, it } from "vitest"; |
| 2 | import { act, fireEvent, render, screen } from "@testing-library/react"; | |
| 3 | import { App } from "./App"; | |
| 4 | import { dispatch, resetStore } from "./store"; | |
| 5 | import type { OriSocket } from "./ws"; | |
| 6 | import type { ClientMessage } from "./protocol"; | |
| 7 | ||
| 8 | function fakeSocket() { | |
| 9 | const sent: ClientMessage[] = []; | |
| 10 | const socket: OriSocket = { | |
| 11 | send: (message) => sent.push(message), | |
| 12 | close: () => {}, | |
| 13 | }; | |
| 14 | return { socket, sent }; | |
| 15 | } | |
| 16 | ||
| 17 | describe("App", () => { | |
| 18 | beforeEach(() => { | |
| 19 | resetStore(); | |
| 20 | }); | |
| 21 | ||
| 22 | it("sends a prompt typed in the composer", () => { | |
| 23 | const { socket, sent } = fakeSocket(); | |
| 24 | render(<App socket={socket} />); | |
| 25 | ||
| 26 | fireEvent.change(screen.getByLabelText("prompt"), { | |
| 27 | target: { value: "hello agent" }, | |
| 28 | }); | |
| 29 | fireEvent.click(screen.getByRole("button", { name: "Send" })); | |
| 30 | ||
| 31 | expect(sent).toEqual([{ type: "prompt", text: "hello agent" }]); | |
| 32 | }); | |
| 33 | ||
| 34 | it("shows the streamed conversation from the store", () => { | |
| 35 | const { socket } = fakeSocket(); | |
| 36 | render(<App socket={socket} />); | |
| 37 | ||
| 38 | act(() => { | |
| 39 | dispatch({ | |
| 40 | type: "server", | |
| 41 | message: { type: "user_message", text: "my question" }, | |
| 42 | }); | |
| 43 | dispatch({ | |
| 44 | type: "server", | |
| 45 | message: { | |
| 46 | type: "session_update", | |
| 47 | update: { | |
| 48 | sessionUpdate: "agent_message_chunk", | |
| 49 | content: { type: "text", text: "my **answer**" }, | |
| 50 | }, | |
| 51 | }, | |
| 52 | }); | |
| 53 | }); | |
| 54 | ||
| 55 | expect(screen.getByText("my question")).toBeDefined(); | |
| 56 | expect(screen.getByText("answer")).toBeDefined(); // bold inner text | |
| 57 | }); | |
| 58 | ||
| 59 | it("offers Stop instead of Send during a turn, and Stop cancels", () => { | |
| 60 | const { socket, sent } = fakeSocket(); | |
| 61 | render(<App socket={socket} />); | |
| 62 | ||
| 63 | act(() => { | |
| 64 | dispatch({ type: "server", message: { type: "turn_started" } }); | |
| 65 | }); | |
| 66 | ||
| 67 | expect(screen.queryByRole("button", { name: "Send" })).toBeNull(); | |
| 68 | fireEvent.click(screen.getByRole("button", { name: "Stop" })); | |
| 69 | expect(sent).toEqual([{ type: "cancel" }]); | |
| 70 | }); | |
| 71 | ||
| 72 | it("shows the connection status and session id from hello", () => { | |
| 73 | const { socket } = fakeSocket(); | |
| 74 | render(<App socket={socket} />); | |
| 75 | ||
| 76 | act(() => { | |
| 77 | dispatch({ | |
| 78 | type: "server", | |
| 79 | message: { type: "hello", sessionId: "sess-42" }, | |
| 80 | }); | |
| 81 | }); | |
| 82 | ||
| 83 | expect(screen.getByText(/online/)).toBeDefined(); | |
| 84 | expect(screen.getByText(/sess-42/)).toBeDefined(); | |
| 85 | }); | |
| 86 | ||
| 87 | it("renders tool calls, plan and thoughts from updates", () => { | |
| 88 | const { socket } = fakeSocket(); | |
| 89 | render(<App socket={socket} />); | |
| 90 | ||
| 91 | act(() => { | |
| 92 | dispatch({ | |
| 93 | type: "server", | |
| 94 | message: { | |
| 95 | type: "session_update", | |
| 96 | update: { | |
| 97 | sessionUpdate: "tool_call", | |
| 98 | toolCallId: "c1", | |
| 99 | title: "Running tests", | |
| 100 | kind: "execute", | |
| 101 | status: "in_progress", | |
| 102 | }, | |
| 103 | }, | |
| 104 | }); | |
| 105 | dispatch({ | |
| 106 | type: "server", | |
| 107 | message: { | |
| 108 | type: "session_update", | |
| 109 | update: { | |
| 110 | sessionUpdate: "plan", | |
| 111 | entries: [ | |
| 112 | { | |
| 113 | content: "write the fix", | |
| 114 | priority: "high", | |
| 115 | status: "in_progress", | |
| 116 | }, | |
| 117 | ], | |
| 118 | }, | |
| 119 | }, | |
| 120 | }); | |
| 121 | dispatch({ | |
| 122 | type: "server", | |
| 123 | message: { | |
| 124 | type: "session_update", | |
| 125 | update: { | |
| 126 | sessionUpdate: "agent_thought_chunk", | |
| 127 | content: { type: "text", text: "let me think" }, | |
| 128 | }, | |
| 129 | }, | |
| 130 | }); | |
| 131 | }); | |
| 132 | ||
| 133 | expect(screen.getByText("Running tests")).toBeDefined(); | |
| 134 | expect(screen.getByText(/write the fix/)).toBeDefined(); | |
| 135 | expect(screen.getByText("let me think")).toBeDefined(); | |
| 136 | }); | |
| 137 | ||
| 138 | it("answers a permission request through the socket", () => { | |
| 139 | const { socket, sent } = fakeSocket(); | |
| 140 | render(<App socket={socket} />); | |
| 141 | ||
| 142 | act(() => { | |
| 143 | dispatch({ | |
| 144 | type: "server", | |
| 145 | message: { | |
| 146 | type: "permission_request", | |
| 147 | requestId: "perm-9", | |
| 148 | request: { | |
| 149 | toolCall: { title: "Write main.go" }, | |
| 150 | options: [ | |
| 151 | { | |
| 152 | optionId: "allow-once", | |
| 153 | name: "Allow once", | |
| 154 | kind: "allow_once", | |
| 155 | }, | |
| 156 | { optionId: "reject-once", name: "Reject", kind: "reject_once" }, | |
| 157 | ], | |
| 158 | }, | |
| 159 | }, | |
| 160 | }); | |
| 161 | }); | |
| 162 | ||
| 163 | expect(screen.getByText(/Write main\.go/)).toBeDefined(); | |
| 164 | fireEvent.click(screen.getByRole("button", { name: "Allow once" })); | |
| 165 | ||
| 166 | expect(sent).toEqual([ | |
| 167 | { | |
| 168 | type: "permission_response", | |
| 169 | requestId: "perm-9", | |
| 170 | optionId: "allow-once", | |
| 171 | }, | |
| 172 | ]); | |
| 173 | }); | |
| 174 | ||
| 175 | it("does not send empty prompts", () => { | |
| 176 | const { socket, sent } = fakeSocket(); | |
| 177 | render(<App socket={socket} />); | |
| 178 | ||
| 179 | fireEvent.change(screen.getByLabelText("prompt"), { | |
| 180 | target: { value: " " }, | |
| 181 | }); | |
| 182 | fireEvent.keyDown(screen.getByLabelText("prompt"), { key: "Enter" }); | |
| 183 | ||
| 184 | expect(sent).toEqual([]); | |
| 185 | }); | |
| 186 | }); |