1717 * under the License.
1818 */
1919
20- // Exercises the /agents/:id/react WebSocket protocol end to end: the snapshot
21- // sent on connect, the status lifecycle frames, the stop command, the prompt
22- // request (with a stubbed run), and the error paths. These drive the real
23- // socket via app.listen + a WebSocket client, since app.handle() does not
24- // perform WS upgrades.
20+ // Exercises the /agents/:id/react WebSocket protocol end to end: the init
21+ // snapshot sent on connect, the state lifecycle frames, the stop command, the
22+ // prompt request (with a stubbed run), and the error paths. Frames use the
23+ // discriminated-union message model (type: "init" | "step" | "state" |
24+ // "complete" | "error" for server frames; "prompt" | "command" for client
25+ // frames). These drive the real socket via app.listen + a WebSocket client,
26+ // since app.handle() does not perform WS upgrades.
2527
2628import { afterAll , afterEach , beforeAll , beforeEach , describe , expect , test } from "bun:test" ;
2729import { buildApp , _resetAgentStoreForTests , _getAgentForTests } from "./server" ;
@@ -150,74 +152,75 @@ afterEach(() => {
150152} ) ;
151153
152154describe ( `WS ${ API } /agents/:id/react` , ( ) => {
153- test ( "sends a results-free snapshot frame on connect" , async ( ) => {
155+ test ( "sends an init snapshot frame on connect" , async ( ) => {
154156 const id = await createAgent ( ) ;
155157 const { ws, messages } = connect ( id ) ;
156158 await waitOpen ( ws ) ;
157159
158- const snapshot = await messages . waitFor ( m => m . type === "WsServerSnapshotEvent " ) ;
160+ const snapshot = await messages . waitFor ( m => m . type === "init " ) ;
159161 expect ( snapshot . state ) . toBe ( "AVAILABLE" ) ;
160162 expect ( Array . isArray ( snapshot . steps ) ) . toBe ( true ) ;
161163 expect ( typeof snapshot . headId ) . toBe ( "string" ) ;
162- // Results are pulled on demand, never pushed on the snapshot.
163- expect ( "operatorResults" in snapshot ) . toBe ( false ) ;
164+ // The init frame carries the current per-operator execution summaries.
165+ expect ( "operatorResults" in snapshot ) . toBe ( true ) ;
166+ expect ( typeof snapshot . operatorResults ) . toBe ( "object" ) ;
164167 } ) ;
165168
166169 test ( "errors and closes when connecting to an unknown agent" , async ( ) => {
167170 const { messages } = connect ( "agent-does-not-exist" ) ;
168- const err = await messages . waitFor ( m => m . type === "WsServerErrorEvent " ) ;
171+ const err = await messages . waitFor ( m => m . type === "error " ) ;
169172 expect ( err . error ) . toBe ( "Agent not found" ) ;
170173 } ) ;
171174
172- test ( "a stop command broadcasts a STOPPING status frame" , async ( ) => {
175+ test ( "a stop command broadcasts a STOPPING state frame" , async ( ) => {
173176 const id = await createAgent ( ) ;
174177 const { ws, messages } = connect ( id ) ;
175178 await waitOpen ( ws ) ;
176- await messages . waitFor ( m => m . type === "WsServerSnapshotEvent " ) ;
179+ await messages . waitFor ( m => m . type === "init " ) ;
177180
178- ws . send ( JSON . stringify ( { type : "WsClientStopCommand " } ) ) ;
181+ ws . send ( JSON . stringify ( { type : "command" , commandType : "stop " } ) ) ;
179182
180- const status = await messages . waitFor ( m => m . type === "WsServerStatusEvent " ) ;
183+ const status = await messages . waitFor ( m => m . type === "state " ) ;
181184 expect ( status . state ) . toBe ( "STOPPING" ) ;
182185 } ) ;
183186
184187 test ( "a prompt with empty content yields an error frame" , async ( ) => {
185188 const id = await createAgent ( ) ;
186189 const { ws, messages } = connect ( id ) ;
187190 await waitOpen ( ws ) ;
188- await messages . waitFor ( m => m . type === "WsServerSnapshotEvent " ) ;
191+ await messages . waitFor ( m => m . type === "init " ) ;
189192
190- ws . send ( JSON . stringify ( { type : "WsClientPromptCommand " , content : "" } ) ) ;
193+ ws . send ( JSON . stringify ( { type : "prompt " , content : "" } ) ) ;
191194
192- const err = await messages . waitFor ( m => m . type === "WsServerErrorEvent " ) ;
195+ const err = await messages . waitFor ( m => m . type === "error " ) ;
193196 expect ( err . error ) . toBe ( "Message content is required" ) ;
194197 } ) ;
195198
196199 test ( "a malformed (non-JSON) frame yields an error frame" , async ( ) => {
197200 const id = await createAgent ( ) ;
198201 const { ws, messages } = connect ( id ) ;
199202 await waitOpen ( ws ) ;
200- await messages . waitFor ( m => m . type === "WsServerSnapshotEvent " ) ;
203+ await messages . waitFor ( m => m . type === "init " ) ;
201204
202205 ws . send ( "this is not json" ) ;
203206
204- const err = await messages . waitFor ( m => m . type === "WsServerErrorEvent " ) ;
207+ const err = await messages . waitFor ( m => m . type === "error " ) ;
205208 expect ( err . error ) . toBe ( "Invalid message format" ) ;
206209 } ) ;
207210
208211 test ( "an unknown message type yields an error frame" , async ( ) => {
209212 const id = await createAgent ( ) ;
210213 const { ws, messages } = connect ( id ) ;
211214 await waitOpen ( ws ) ;
212- await messages . waitFor ( m => m . type === "WsServerSnapshotEvent " ) ;
215+ await messages . waitFor ( m => m . type === "init " ) ;
213216
214217 ws . send ( JSON . stringify ( { type : "bogus" } ) ) ;
215218
216- const err = await messages . waitFor ( m => m . type === "WsServerErrorEvent " ) ;
219+ const err = await messages . waitFor ( m => m . type === "error " ) ;
217220 expect ( err . error ) . toBe ( "Unknown message type: bogus" ) ;
218221 } ) ;
219222
220- test ( "a prompt run streams GENERATING -> step -> resting status (no result frames) " , async ( ) => {
223+ test ( "a prompt run streams GENERATING -> step -> complete " , async ( ) => {
221224 const id = await createAgent ( ) ;
222225
223226 // Stub the agent's run so no live LLM is needed: emit one ending step via
@@ -259,22 +262,25 @@ describe(`WS ${API}/agents/:id/react`, () => {
259262
260263 const { ws, messages } = connect ( id ) ;
261264 await waitOpen ( ws ) ;
262- await messages . waitFor ( m => m . type === "WsServerSnapshotEvent " ) ;
265+ await messages . waitFor ( m => m . type === "init " ) ;
263266
264- ws . send ( JSON . stringify ( { type : "WsClientPromptCommand " , content : "hello" } ) ) ;
267+ ws . send ( JSON . stringify ( { type : "prompt " , content : "hello" } ) ) ;
265268
266- const generating = await messages . waitFor ( m => m . type === "WsServerStatusEvent " && m . state === "GENERATING" ) ;
269+ const generating = await messages . waitFor ( m => m . type === "state " && m . state === "GENERATING" ) ;
267270 expect ( generating . state ) . toBe ( "GENERATING" ) ;
268271
269- const step = await messages . waitFor ( m => m . type === "WsServerStepEvent " ) ;
272+ const step = await messages . waitFor ( m => m . type === "step " ) ;
270273 expect ( step . step . content ) . toBe ( "done" ) ;
274+ // A step with no tool calls carries no operatorResults.
271275 expect ( "operatorResults" in step ) . toBe ( false ) ;
272276
273- const resting = await messages . waitFor ( m => m . type === "WsServerStatusEvent" && m . state === "AVAILABLE" ) ;
274- expect ( resting . state ) . toBe ( "AVAILABLE" ) ;
277+ // The run ends with a terminal `complete` frame carrying the resting state.
278+ const complete = await messages . waitFor ( m => m . type === "complete" ) ;
279+ expect ( complete . state ) . toBe ( "AVAILABLE" ) ;
280+ expect ( "operatorResults" in complete ) . toBe ( true ) ;
275281 } ) ;
276282
277- test ( "a failed run emits an error frame and still returns to a resting status " , async ( ) => {
283+ test ( "a failed run emits an error frame" , async ( ) => {
278284 const id = await createAgent ( ) ;
279285
280286 const agent = _getAgentForTests ( id ) ! ;
@@ -284,40 +290,35 @@ describe(`WS ${API}/agents/:id/react`, () => {
284290
285291 const { ws, messages } = connect ( id ) ;
286292 await waitOpen ( ws ) ;
287- await messages . waitFor ( m => m . type === "WsServerSnapshotEvent " ) ;
293+ await messages . waitFor ( m => m . type === "init " ) ;
288294
289- ws . send ( JSON . stringify ( { type : "WsClientPromptCommand " , content : "hello" } ) ) ;
295+ ws . send ( JSON . stringify ( { type : "prompt " , content : "hello" } ) ) ;
290296
291- await messages . waitFor ( m => m . type === "WsServerStatusEvent " && m . state === "GENERATING" ) ;
297+ await messages . waitFor ( m => m . type === "state " && m . state === "GENERATING" ) ;
292298
293- const err = await messages . waitFor ( m => m . type === "WsServerErrorEvent " ) ;
299+ const err = await messages . waitFor ( m => m . type === "error " ) ;
294300 expect ( err . error ) . toBe ( "boom" ) ;
295-
296- // The end-of-run status frame must still fire after a failure, so the client
297- // is not left stuck on GENERATING.
298- const resting = await messages . waitFor ( m => m . type === "WsServerStatusEvent" && m . state === "AVAILABLE" ) ;
299- expect ( resting . state ) . toBe ( "AVAILABLE" ) ;
300301 } ) ;
301302
302303 test ( "a message for an agent that no longer exists yields an error frame" , async ( ) => {
303304 const id = await createAgent ( ) ;
304305 const { ws, messages } = connect ( id ) ;
305306 await waitOpen ( ws ) ;
306- await messages . waitFor ( m => m . type === "WsServerSnapshotEvent " ) ;
307+ await messages . waitFor ( m => m . type === "init " ) ;
307308
308309 // Drop the agent while the socket stays open; the message handler re-looks it up.
309310 _resetAgentStoreForTests ( ) ;
310- ws . send ( JSON . stringify ( { type : "WsClientPromptCommand " , content : "hello" } ) ) ;
311+ ws . send ( JSON . stringify ( { type : "prompt " , content : "hello" } ) ) ;
311312
312- const err = await messages . waitFor ( m => m . type === "WsServerErrorEvent " ) ;
313+ const err = await messages . waitFor ( m => m . type === "error " ) ;
313314 expect ( err . error ) . toBe ( "Agent not found" ) ;
314315 } ) ;
315316
316317 test ( "runs the close handler when the client disconnects" , async ( ) => {
317318 const id = await createAgent ( ) ;
318319 const { ws, messages } = connect ( id ) ;
319320 await waitOpen ( ws ) ;
320- await messages . waitFor ( m => m . type === "WsServerSnapshotEvent " ) ;
321+ await messages . waitFor ( m => m . type === "init " ) ;
321322
322323 const closed = new Promise < void > ( resolve => ws . addEventListener ( "close" , ( ) => resolve ( ) , { once : true } ) ) ;
323324 ws . close ( ) ;
0 commit comments