nandi/oripublic Fork 0
3d1f70c3b1e0d8c9a0b27adcc1bd1a9f053c050e
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

App.test.tsx · 301 lines · 8.0 KBTypeScript Blame HistoryRaw
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday1import {
2 act,
3 fireEvent,
4 render,
5 screen,
6 waitFor,
7} from "@testing-library/react";
8import { beforeEach, describe, expect, it, vi } from "vitest";
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g 2d ago9import { App } from "./App";
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday10import type { ClientMessage } from "./protocol";
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g 2d ago11import { dispatch, resetStore } from "./store";
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday12import { resetTheme } from "./theme";
13import { resetWorkspace } from "./workspace";
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g 2d ago14import type { OriSocket } from "./ws";
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday15
16vi.mock("./api", () => ({
17 listFiles: vi.fn().mockResolvedValue({ path: "/w", entries: [] }),
18 readFile: vi.fn().mockResolvedValue({ path: "/w/x", content: "" }),
19 writeFile: vi.fn().mockResolvedValue(undefined),
20 searchFiles: vi.fn().mockResolvedValue({ root: "/w", files: [] }),
21 listSkills: vi.fn().mockResolvedValue({ skills: [] }),
22 rawFileUrl: (path: string) => `/api/raw?path=${encodeURIComponent(path)}`,
23}));
24
25// The real terminal drags xterm and a WebSocket along; a stub is enough here.
26vi.mock("./components/TerminalPane", () => ({
27 TerminalPane: () => <div data-testid="terminal-pane" />,
28}));
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g 2d ago29
30function fakeSocket() {
31 const sent: ClientMessage[] = [];
32 const socket: OriSocket = {
33 send: (message) => sent.push(message),
34 close: () => {},
35 };
36 return { socket, sent };
37}
38
39describe("App", () => {
40 beforeEach(() => {
41 resetStore();
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday42 resetWorkspace();
43 resetTheme();
44 });
45
46 it("switches between the light and dark themes from the header", () => {
47 const { socket } = fakeSocket();
48 render(<App socket={socket} />);
49 expect(document.documentElement.dataset.theme).toBe("light");
50
51 fireEvent.click(
52 screen.getByRole("button", { name: "Switch to dark theme" }),
53 );
54 expect(document.documentElement.dataset.theme).toBe("dark");
55
56 fireEvent.click(
57 screen.getByRole("button", { name: "Switch to light theme" }),
58 );
59 expect(document.documentElement.dataset.theme).toBe("light");
60 });
61
62 it("toggles the workspace panel and switches its tabs", async () => {
63 const { socket } = fakeSocket();
64 render(<App socket={socket} />);
65
66 // The panel stays mounted (so its state survives) and hides via CSS.
67 const workspace = screen.getByRole("complementary", { name: "workspace" });
68 expect(workspace.className).toContain("workspace-hidden");
69
70 fireEvent.click(screen.getByRole("button", { name: "⊞ Workspace" }));
71 expect(workspace.className).not.toContain("workspace-hidden");
72
73 fireEvent.click(screen.getByRole("button", { name: "Terminal" }));
74 await waitFor(() =>
75 expect(screen.getByTestId("terminal-pane")).toBeDefined(),
76 );
77
78 fireEvent.click(screen.getByRole("button", { name: "⊞ Workspace" }));
79 expect(workspace.className).toContain("workspace-hidden");
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g 2d ago80 });
81
82 it("sends a prompt typed in the composer", () => {
83 const { socket, sent } = fakeSocket();
84 render(<App socket={socket} />);
85
86 fireEvent.change(screen.getByLabelText("prompt"), {
87 target: { value: "hello agent" },
88 });
89 fireEvent.click(screen.getByRole("button", { name: "Send" }));
90
91 expect(sent).toEqual([{ type: "prompt", text: "hello agent" }]);
92 });
93
94 it("shows the streamed conversation from the store", () => {
95 const { socket } = fakeSocket();
96 render(<App socket={socket} />);
97
98 act(() => {
99 dispatch({
100 type: "server",
101 message: { type: "user_message", text: "my question" },
102 });
103 dispatch({
104 type: "server",
105 message: {
106 type: "session_update",
107 update: {
108 sessionUpdate: "agent_message_chunk",
109 content: { type: "text", text: "my **answer**" },
110 },
111 },
112 });
113 });
114
115 expect(screen.getByText("my question")).toBeDefined();
116 expect(screen.getByText("answer")).toBeDefined(); // bold inner text
117 });
118
119 it("offers Stop instead of Send during a turn, and Stop cancels", () => {
120 const { socket, sent } = fakeSocket();
121 render(<App socket={socket} />);
122
123 act(() => {
124 dispatch({ type: "server", message: { type: "turn_started" } });
125 });
126
127 expect(screen.queryByRole("button", { name: "Send" })).toBeNull();
128 fireEvent.click(screen.getByRole("button", { name: "Stop" }));
129 expect(sent).toEqual([{ type: "cancel" }]);
130 });
131
132 it("shows the connection status and session id from hello", () => {
133 const { socket } = fakeSocket();
134 render(<App socket={socket} />);
135
136 act(() => {
137 dispatch({
138 type: "server",
139 message: { type: "hello", sessionId: "sess-42" },
140 });
141 });
142
143 expect(screen.getByText(/online/)).toBeDefined();
144 expect(screen.getByText(/sess-42/)).toBeDefined();
145 });
146
147 it("renders tool calls, plan and thoughts from updates", () => {
148 const { socket } = fakeSocket();
149 render(<App socket={socket} />);
150
151 act(() => {
152 dispatch({
153 type: "server",
154 message: {
155 type: "session_update",
156 update: {
157 sessionUpdate: "tool_call",
158 toolCallId: "c1",
159 title: "Running tests",
160 kind: "execute",
161 status: "in_progress",
162 },
163 },
164 });
165 dispatch({
166 type: "server",
167 message: {
168 type: "session_update",
169 update: {
170 sessionUpdate: "plan",
171 entries: [
172 {
173 content: "write the fix",
174 priority: "high",
175 status: "in_progress",
176 },
177 ],
178 },
179 },
180 });
181 dispatch({
182 type: "server",
183 message: {
184 type: "session_update",
185 update: {
186 sessionUpdate: "agent_thought_chunk",
187 content: { type: "text", text: "let me think" },
188 },
189 },
190 });
191 });
192
193 expect(screen.getByText("Running tests")).toBeDefined();
194 expect(screen.getByText(/write the fix/)).toBeDefined();
195 expect(screen.getByText("let me think")).toBeDefined();
196 });
197
198 it("answers a permission request through the socket", () => {
199 const { socket, sent } = fakeSocket();
200 render(<App socket={socket} />);
201
202 act(() => {
203 dispatch({
204 type: "server",
205 message: {
206 type: "permission_request",
207 requestId: "perm-9",
208 request: {
209 toolCall: { title: "Write main.go" },
210 options: [
211 {
212 optionId: "allow-once",
213 name: "Allow once",
214 kind: "allow_once",
215 },
216 { optionId: "reject-once", name: "Reject", kind: "reject_once" },
217 ],
218 },
219 },
220 });
221 });
222
223 expect(screen.getByText(/Write main\.go/)).toBeDefined();
224 fireEvent.click(screen.getByRole("button", { name: "Allow once" }));
225
226 expect(sent).toEqual([
227 {
228 type: "permission_response",
229 requestId: "perm-9",
230 optionId: "allow-once",
231 },
232 ]);
233 });
234
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday235 it("keeps a streaming thought open and collapses it when the turn ends", () => {
236 const { socket } = fakeSocket();
237 const { container } = render(<App socket={socket} />);
238
239 act(() => {
240 dispatch({
241 type: "server",
242 message: {
243 type: "session_update",
244 update: {
245 sessionUpdate: "agent_thought_chunk",
246 content: { type: "text", text: "streaming thought" },
247 },
248 },
249 });
250 });
251
252 // While streaming: the content is in a live (non-collapsible) block.
253 expect(container.querySelector(".thought-live")).not.toBeNull();
254 expect(screen.getByText("💭 Thinking…")).toBeDefined();
255 expect(screen.getByText("streaming thought")).toBeDefined();
256
257 act(() => {
258 dispatch({
259 type: "server",
260 message: { type: "turn_ended", stopReason: "end_turn" },
261 });
262 });
263
264 // After the turn: a collapsible details block replaces it.
265 expect(container.querySelector(".thought-live")).toBeNull();
266 expect(container.querySelector("details.thought")).not.toBeNull();
267 expect(screen.getByText("💭 Thought for a moment")).toBeDefined();
268 });
269
270 it("shows the spinner with a working phrase during a turn", () => {
271 const { socket } = fakeSocket();
272 const { container } = render(<App socket={socket} />);
273
274 act(() => {
275 dispatch({ type: "server", message: { type: "turn_started" } });
276 });
277
278 expect(container.querySelector(".spinner")).not.toBeNull();
279
280 act(() => {
281 dispatch({
282 type: "server",
283 message: { type: "turn_ended", stopReason: "end_turn" },
284 });
285 });
286
287 expect(container.querySelector(".spinner")).toBeNull();
288 });
289
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g 2d ago290 it("does not send empty prompts", () => {
291 const { socket, sent } = fakeSocket();
292 render(<App socket={socket} />);
293
294 fireEvent.change(screen.getByLabelText("prompt"), {
295 target: { value: " " },
296 });
297 fireEvent.keyDown(screen.getByLabelText("prompt"), { key: "Enter" });
298
299 expect(sent).toEqual([]);
300 });
301});