@@ -590,6 +590,103 @@ describe("channel-streaming", () => {
590590 expect ( onStart ) . toHaveBeenCalledTimes ( 1 ) ;
591591 } ) ;
592592
593+ it ( "does not report started when delayed progress startup rejects" , async ( ) => {
594+ vi . useFakeTimers ( ) ;
595+ const onStart = vi
596+ . fn < ( ) => Promise < void > > ( )
597+ . mockRejectedValueOnce ( new Error ( "draft unavailable" ) )
598+ . mockResolvedValueOnce ( undefined ) ;
599+ const gate = createChannelProgressDraftGate ( { onStart } ) ;
600+
601+ await expect ( gate . noteWork ( ) ) . resolves . toBe ( false ) ;
602+ await vi . advanceTimersByTimeAsync ( 5_000 ) ;
603+
604+ expect ( onStart ) . toHaveBeenCalledTimes ( 1 ) ;
605+ expect ( gate . hasStarted ) . toBe ( false ) ;
606+
607+ await expect ( gate . noteWork ( ) ) . resolves . toBe ( true ) ;
608+
609+ expect ( onStart ) . toHaveBeenCalledTimes ( 2 ) ;
610+ expect ( gate . hasStarted ) . toBe ( true ) ;
611+ } ) ;
612+
613+ it ( "keeps concurrent progress startup single-flight until onStart resolves" , async ( ) => {
614+ vi . useFakeTimers ( ) ;
615+ let resolveStart : ( ( ) => void ) | undefined ;
616+ const onStart = vi . fn (
617+ ( ) =>
618+ new Promise < void > ( ( resolve ) => {
619+ resolveStart = resolve ;
620+ } ) ,
621+ ) ;
622+ const gate = createChannelProgressDraftGate ( { onStart } ) ;
623+
624+ await gate . noteWork ( ) ;
625+ const firstStart = gate . noteWork ( ) ;
626+ const secondStart = gate . startNow ( ) ;
627+ await Promise . resolve ( ) ;
628+
629+ expect ( onStart ) . toHaveBeenCalledTimes ( 1 ) ;
630+ expect ( gate . hasStarted ) . toBe ( true ) ;
631+
632+ resolveStart ?.( ) ;
633+ await expect ( firstStart ) . resolves . toBe ( true ) ;
634+ await expect ( secondStart ) . resolves . toBeUndefined ( ) ;
635+
636+ expect ( onStart ) . toHaveBeenCalledTimes ( 1 ) ;
637+ expect ( gate . hasStarted ) . toBe ( true ) ;
638+ } ) ;
639+
640+ it ( "does not report active when cancel wins the startup race" , async ( ) => {
641+ vi . useFakeTimers ( ) ;
642+ let resolveStart : ( ( ) => void ) | undefined ;
643+ const onStart = vi . fn (
644+ ( ) =>
645+ new Promise < void > ( ( resolve ) => {
646+ resolveStart = resolve ;
647+ } ) ,
648+ ) ;
649+ const gate = createChannelProgressDraftGate ( { onStart } ) ;
650+
651+ await gate . noteWork ( ) ;
652+ const startResult = gate . noteWork ( ) ;
653+ await Promise . resolve ( ) ;
654+
655+ expect ( onStart ) . toHaveBeenCalledTimes ( 1 ) ;
656+ gate . cancel ( ) ;
657+
658+ resolveStart ?.( ) ;
659+
660+ await expect ( startResult ) . resolves . toBe ( false ) ;
661+ expect ( gate . hasStarted ) . toBe ( false ) ;
662+ } ) ;
663+
664+ it ( "joins explicit startup before applying the first-work delay" , async ( ) => {
665+ vi . useFakeTimers ( ) ;
666+ let resolveStart : ( ( ) => void ) | undefined ;
667+ const onStart = vi . fn (
668+ ( ) =>
669+ new Promise < void > ( ( resolve ) => {
670+ resolveStart = resolve ;
671+ } ) ,
672+ ) ;
673+ const gate = createChannelProgressDraftGate ( { onStart } ) ;
674+
675+ const explicitStart = gate . startNow ( ) ;
676+ await Promise . resolve ( ) ;
677+ const workDuringStart = gate . noteWork ( ) ;
678+
679+ expect ( onStart ) . toHaveBeenCalledTimes ( 1 ) ;
680+ expect ( gate . hasStarted ) . toBe ( true ) ;
681+
682+ resolveStart ?.( ) ;
683+
684+ await expect ( explicitStart ) . resolves . toBeUndefined ( ) ;
685+ await expect ( workDuringStart ) . resolves . toBe ( true ) ;
686+ expect ( onStart ) . toHaveBeenCalledTimes ( 1 ) ;
687+ expect ( gate . hasStarted ) . toBe ( true ) ;
688+ } ) ;
689+
593690 it ( "ignores message-like tools for progress draft work" , ( ) => {
594691 expect ( isChannelProgressDraftWorkToolName ( "message" ) ) . toBe ( false ) ;
595692 expect ( isChannelProgressDraftWorkToolName ( "react" ) ) . toBe ( false ) ;
0 commit comments