@@ -339,3 +339,115 @@ test("Will keep state order even if not awaited", async ({
339339
340340 pass ( ) ;
341341} ) ;
342+
343+ test ( "Will observe an in-flight write from an unrelated concurrent getState" , async ( {
344+ pass,
345+ fail,
346+ } ) => {
347+ const TEST_COMPLETION = addCompletion ( {
348+ completionName : "test_completion" ,
349+ getCompletion : async ( { agentName, messages } ) => {
350+ const [ { content } ] = messages . slice ( - 1 ) ;
351+ return { agentName, content, role : "assistant" } ;
352+ } ,
353+ } ) ;
354+
355+ const TEST_STATE = addState ( {
356+ stateName : "test_state" ,
357+ getDefaultState : ( ) => "foo" ,
358+ } ) ;
359+
360+ const TEST_AGENT = addAgent ( {
361+ agentName : "test_agent" ,
362+ completion : TEST_COMPLETION ,
363+ prompt : "x" ,
364+ states : [ TEST_STATE ] ,
365+ } ) ;
366+
367+ const TEST_SWARM = addSwarm ( {
368+ swarmName : "test_swarm" ,
369+ agentList : [ TEST_AGENT ] ,
370+ defaultAgent : TEST_AGENT ,
371+ } ) ;
372+
373+ const CLIENT_ID = randomString ( ) ;
374+ session ( CLIENT_ID , TEST_SWARM ) ;
375+ const ref = { clientId : CLIENT_ID , agentName : TEST_AGENT , stateName : TEST_STATE } ;
376+
377+ await State . setState ( ( ) => "foo" , ref ) ;
378+
379+ // A slow write, not awaited: it holds the dispatch queue for 200ms.
380+ const writePromise = State . setState ( async ( ) => {
381+ await sleep ( 200 ) ;
382+ return "WRITTEN" ;
383+ } , ref ) ;
384+
385+ // Let the write enter the dispatch, then issue an UNRELATED getState.
386+ // It must queue behind the write and observe "WRITTEN", not the stale "foo".
387+ await sleep ( 20 ) ;
388+ const readDuringWrite = await State . getState ( ref ) ;
389+ await writePromise ;
390+
391+ if ( readDuringWrite !== "WRITTEN" ) {
392+ fail ( `concurrent getState bypassed the dispatch queue: got ${ readDuringWrite } ` ) ;
393+ }
394+
395+ pass ( ) ;
396+ } ) ;
397+
398+ test ( "Will serve a reentrant getState from inside a setState without deadlock" , async ( {
399+ pass,
400+ fail,
401+ } ) => {
402+ const TEST_COMPLETION = addCompletion ( {
403+ completionName : "test_completion" ,
404+ getCompletion : async ( { agentName, messages } ) => {
405+ const [ { content } ] = messages . slice ( - 1 ) ;
406+ return { agentName, content, role : "assistant" } ;
407+ } ,
408+ } ) ;
409+
410+ const TEST_STATE = addState ( {
411+ stateName : "test_state" ,
412+ getDefaultState : ( ) => "init" ,
413+ } ) ;
414+
415+ const TEST_AGENT = addAgent ( {
416+ agentName : "test_agent" ,
417+ completion : TEST_COMPLETION ,
418+ prompt : "x" ,
419+ states : [ TEST_STATE ] ,
420+ } ) ;
421+
422+ const TEST_SWARM = addSwarm ( {
423+ swarmName : "test_swarm" ,
424+ agentList : [ TEST_AGENT ] ,
425+ defaultAgent : TEST_AGENT ,
426+ } ) ;
427+
428+ const CLIENT_ID = randomString ( ) ;
429+ session ( CLIENT_ID , TEST_SWARM ) ;
430+ const ref = { clientId : CLIENT_ID , agentName : TEST_AGENT , stateName : TEST_STATE } ;
431+
432+ let reentrantValue = "NOT_RUN" ;
433+ const writePromise = State . setState ( async ( ) => {
434+ // getState called from INSIDE the dispatchFn must not deadlock behind
435+ // this very write; it reads the current value directly.
436+ reentrantValue = await State . getState ( ref ) ;
437+ return "after-reentrant" ;
438+ } , ref ) ;
439+
440+ const settled = await Promise . race ( [
441+ writePromise . then ( ( ) => "SETTLED" ) ,
442+ sleep ( 2_000 ) . then ( ( ) => "DEADLOCK" ) ,
443+ ] ) ;
444+
445+ if ( settled !== "SETTLED" ) {
446+ fail ( "reentrant getState deadlocked behind its enclosing setState" ) ;
447+ }
448+ if ( reentrantValue !== "init" ) {
449+ fail ( `reentrant getState returned the wrong value: ${ reentrantValue } ` ) ;
450+ }
451+
452+ pass ( ) ;
453+ } ) ;
0 commit comments