@@ -583,6 +583,149 @@ describe("plan-agent no-tool-generation detection", () => {
583583 expect ( result . event ) . toBeNull ( )
584584 }
585585 } )
586+
587+ test ( "event contains all required fields with correct types" , ( ) => {
588+ const result = simulateFinishStep ( {
589+ ...baseOpts ,
590+ agent : "plan" ,
591+ finishReason : "stop" ,
592+ sessionToolCallsMade : 0 ,
593+ planNoToolWarningEmitted : false ,
594+ } )
595+ const ev = result . event !
596+ expect ( ev . type ) . toBe ( "plan_no_tool_generation" )
597+ expect ( typeof ev . timestamp ) . toBe ( "number" )
598+ expect ( typeof ev . session_id ) . toBe ( "string" )
599+ expect ( typeof ev . message_id ) . toBe ( "string" )
600+ expect ( typeof ev . model_id ) . toBe ( "string" )
601+ expect ( typeof ev . provider_id ) . toBe ( "string" )
602+ expect ( typeof ev . finish_reason ) . toBe ( "string" )
603+ expect ( typeof ev . tokens_output ) . toBe ( "number" )
604+ } )
605+
606+ test ( "exactly one tool call suppresses the warning (boundary: not only >1)" , ( ) => {
607+ const result = simulateFinishStep ( {
608+ ...baseOpts ,
609+ agent : "plan" ,
610+ finishReason : "stop" ,
611+ sessionToolCallsMade : 1 ,
612+ planNoToolWarningEmitted : false ,
613+ } )
614+ expect ( result . event ) . toBeNull ( )
615+ expect ( result . warningEmitted ) . toBe ( false )
616+ } )
617+
618+ test ( "fires with tokens_output=0 (model stopped immediately, produced nothing)" , ( ) => {
619+ const result = simulateFinishStep ( {
620+ ...baseOpts ,
621+ sessionID : "sess-plan-zero" ,
622+ messageID : "msg-plan-zero" ,
623+ agent : "plan" ,
624+ finishReason : "stop" ,
625+ sessionToolCallsMade : 0 ,
626+ planNoToolWarningEmitted : false ,
627+ tokensOutput : 0 ,
628+ } )
629+ expect ( result . event ) . not . toBeNull ( )
630+ expect ( result . event ?. tokens_output ) . toBe ( 0 )
631+ } )
632+
633+ test ( "fires with very large tokens_output (model wrote long text plan, still no tools)" , ( ) => {
634+ const result = simulateFinishStep ( {
635+ ...baseOpts ,
636+ sessionID : "sess-plan-verbose" ,
637+ messageID : "msg-plan-verbose" ,
638+ agent : "plan" ,
639+ finishReason : "stop" ,
640+ sessionToolCallsMade : 0 ,
641+ planNoToolWarningEmitted : false ,
642+ tokensOutput : 9999 ,
643+ } )
644+ expect ( result . event ) . not . toBeNull ( )
645+ expect ( result . event ?. tokens_output ) . toBe ( 9999 )
646+ } )
647+
648+ test ( "session IDs and model metadata are forwarded correctly to the event" , ( ) => {
649+ const result = simulateFinishStep ( {
650+ agent : "plan" ,
651+ finishReason : "stop" ,
652+ sessionToolCallsMade : 0 ,
653+ planNoToolWarningEmitted : false ,
654+ sessionID : "custom-session-abc" ,
655+ messageID : "custom-message-xyz" ,
656+ modelID : "gpt-5.4-turbo" ,
657+ providerID : "openai-proxy" ,
658+ tokensOutput : 42 ,
659+ } )
660+ expect ( result . event ?. session_id ) . toBe ( "custom-session-abc" )
661+ expect ( result . event ?. message_id ) . toBe ( "custom-message-xyz" )
662+ expect ( result . event ?. model_id ) . toBe ( "gpt-5.4-turbo" )
663+ expect ( result . event ?. provider_id ) . toBe ( "openai-proxy" )
664+ expect ( result . event ?. tokens_output ) . toBe ( 42 )
665+ } )
666+
667+ test ( "two-step simulation: tool call in step 1 suppresses warning in step 2" , ( ) => {
668+ // Simulates a session where step 1 produces a tool call (sessionToolCallsMade goes
669+ // from 0->1) and step 2 ends with finish_reason=stop. The warning should NOT fire
670+ // because the session has already made a tool call.
671+ let sessionToolCallsMade = 0
672+ let planNoToolWarningEmitted = false
673+
674+ // Step 1: a tool call occurs
675+ sessionToolCallsMade ++
676+
677+ // Step 2: finish-step fires with stop but no new tool calls
678+ const result = simulateFinishStep ( {
679+ ...baseOpts ,
680+ agent : "plan" ,
681+ finishReason : "stop" ,
682+ sessionToolCallsMade,
683+ planNoToolWarningEmitted,
684+ } )
685+
686+ expect ( result . event ) . toBeNull ( )
687+ expect ( sessionToolCallsMade ) . toBe ( 1 )
688+ } )
689+
690+ test ( "warning flag transitions from false to true only on first emission" , ( ) => {
691+ let planNoToolWarningEmitted = false
692+
693+ // First emission — should fire and flip the flag
694+ const first = simulateFinishStep ( {
695+ ...baseOpts ,
696+ agent : "plan" ,
697+ finishReason : "stop" ,
698+ sessionToolCallsMade : 0 ,
699+ planNoToolWarningEmitted,
700+ } )
701+ expect ( first . event ) . not . toBeNull ( )
702+ expect ( first . warningEmitted ) . toBe ( true )
703+
704+ // Propagate the new flag state
705+ planNoToolWarningEmitted = first . warningEmitted
706+
707+ // Second call — flag is now true, should be suppressed
708+ const second = simulateFinishStep ( {
709+ ...baseOpts ,
710+ agent : "plan" ,
711+ finishReason : "stop" ,
712+ sessionToolCallsMade : 0 ,
713+ planNoToolWarningEmitted,
714+ } )
715+ expect ( second . event ) . toBeNull ( )
716+ expect ( second . warningEmitted ) . toBe ( true )
717+ } )
718+
719+ test ( "empty-string agent is not treated as plan agent" , ( ) => {
720+ const result = simulateFinishStep ( {
721+ ...baseOpts ,
722+ agent : "" ,
723+ finishReason : "stop" ,
724+ sessionToolCallsMade : 0 ,
725+ planNoToolWarningEmitted : false ,
726+ } )
727+ expect ( result . event ) . toBeNull ( )
728+ } )
586729} )
587730
588731// ---------------------------------------------------------------------------
@@ -857,4 +1000,4 @@ describe("processor state tracking", () => {
8571000 expect ( retryStartTime ) . toBe ( firstRetryStart )
8581001 expect ( attempt ) . toBe ( 2 )
8591002 } )
860- } )
1003+ } )
0 commit comments