@@ -760,180 +760,6 @@ public async Task GetProviderHealthAsync_ShouldUseProbeStatus_WhenRequested()
760760 _llmProviderMock . Verify ( p => p . GetHealthAsync ( default ) , Times . Never ) ;
761761 }
762762
763- #region Chat-to-Proposal Flow — Classifier → Parser Integration (#577)
764-
765- /// <summary>
766- /// Full flow: structured syntax hits the LLM classifier (IsActionable=true),
767- /// then the planner parses successfully, yielding a proposal reference.
768- /// </summary>
769- [ Fact ]
770- public async Task SendMessageAsync_StructuredSyntax_ClassifierHit_ParserSuccess_ProposalCreated ( )
771- {
772- var userId = Guid . NewGuid ( ) ;
773- var boardId = Guid . NewGuid ( ) ;
774- var proposalId = Guid . NewGuid ( ) ;
775- var session = new ChatSession ( userId , "Full flow session" , boardId ) ;
776-
777- _chatSessionRepoMock
778- . Setup ( r => r . GetByIdWithMessagesAsync ( session . Id , default ) )
779- . ReturnsAsync ( session ) ;
780- _llmProviderMock
781- . Setup ( p => p . CompleteAsync ( It . IsAny < ChatCompletionRequest > ( ) , default ) )
782- . ReturnsAsync ( new LlmCompletionResult (
783- "I'll create that card." , 15 , true , "card.create" ) ) ;
784- _plannerMock
785- . Setup ( p => p . ParseInstructionAsync (
786- It . IsAny < string > ( ) , userId , boardId ,
787- It . IsAny < CancellationToken > ( ) , ProposalSourceType . Chat ,
788- session . Id . ToString ( ) , It . IsAny < string ? > ( ) ) )
789- . ReturnsAsync ( Result . Success ( new ProposalDto (
790- proposalId , ProposalSourceType . Chat , null , boardId , userId ,
791- ProposalStatus . PendingReview , RiskLevel . Low ,
792- "create card 'Deploy script'" , null , null ,
793- DateTimeOffset . UtcNow , DateTimeOffset . UtcNow ,
794- DateTime . UtcNow . AddHours ( 1 ) , null , null , null , null ,
795- "corr" , new List < ProposalOperationDto > ( ) ) ) ) ;
796-
797- var result = await _service . SendMessageAsync (
798- session . Id ,
799- userId ,
800- new SendChatMessageDto ( "create card 'Deploy script'" ) ,
801- default ) ;
802-
803- result . IsSuccess . Should ( ) . BeTrue ( ) ;
804- result . Value . MessageType . Should ( ) . Be ( "proposal-reference" ) ;
805- result . Value . ProposalId . Should ( ) . Be ( proposalId ) ;
806- result . Value . Content . Should ( ) . Contain ( "Proposal created for review" ) ;
807- _plannerMock . Verify (
808- p => p . ParseInstructionAsync (
809- "create card 'Deploy script'" , userId , boardId ,
810- It . IsAny < CancellationToken > ( ) , ProposalSourceType . Chat ,
811- session . Id . ToString ( ) , It . IsAny < string ? > ( ) ) ,
812- Times . Once ) ;
813- }
814-
815- /// <summary>
816- /// Natural language misses classifier (IsActionable=false), no RequestProposal set,
817- /// so the planner is never called — current behavior documents the gap.
818- /// </summary>
819- [ Fact ]
820- public async Task SendMessageAsync_NaturalLanguage_ClassifierMiss_NoPlannerCall ( )
821- {
822- var userId = Guid . NewGuid ( ) ;
823- var boardId = Guid . NewGuid ( ) ;
824- var session = new ChatSession ( userId , "Classifier miss session" , boardId ) ;
825-
826- _chatSessionRepoMock
827- . Setup ( r => r . GetByIdWithMessagesAsync ( session . Id , default ) )
828- . ReturnsAsync ( session ) ;
829- _llmProviderMock
830- . Setup ( p => p . CompleteAsync ( It . IsAny < ChatCompletionRequest > ( ) , default ) )
831- . ReturnsAsync ( new LlmCompletionResult (
832- "Sure, I can help with that." , 10 , false , null ) ) ;
833-
834- var result = await _service . SendMessageAsync (
835- session . Id ,
836- userId ,
837- new SendChatMessageDto ( "set up some tasks for the sprint" ) ,
838- default ) ;
839-
840- result . IsSuccess . Should ( ) . BeTrue ( ) ;
841- result . Value . MessageType . Should ( ) . Be ( "text" ) ;
842- _plannerMock . Verify (
843- p => p . ParseInstructionAsync (
844- It . IsAny < string > ( ) , It . IsAny < Guid > ( ) , It . IsAny < Guid ? > ( ) ,
845- It . IsAny < CancellationToken > ( ) , It . IsAny < ProposalSourceType > ( ) ,
846- It . IsAny < string ? > ( ) , It . IsAny < string ? > ( ) ) ,
847- Times . Never ,
848- "planner should not be called when classifier reports non-actionable and RequestProposal is false" ) ;
849- }
850-
851- /// <summary>
852- /// Explicit RequestProposal with natural language — parser receives the raw
853- /// message and fails because it only understands structured syntax.
854- /// </summary>
855- [ Fact ]
856- public async Task SendMessageAsync_ExplicitRequestProposal_NaturalLanguage_ParserFailsGracefully ( )
857- {
858- var userId = Guid . NewGuid ( ) ;
859- var boardId = Guid . NewGuid ( ) ;
860- var session = new ChatSession ( userId , "Explicit NLP fail session" , boardId ) ;
861-
862- _chatSessionRepoMock
863- . Setup ( r => r . GetByIdWithMessagesAsync ( session . Id , default ) )
864- . ReturnsAsync ( session ) ;
865- _llmProviderMock
866- . Setup ( p => p . CompleteAsync ( It . IsAny < ChatCompletionRequest > ( ) , default ) )
867- . ReturnsAsync ( new LlmCompletionResult (
868- "I understand you want tasks." , 15 , false , null ) ) ;
869- _plannerMock
870- . Setup ( p => p . ParseInstructionAsync (
871- It . IsAny < string > ( ) , userId , boardId ,
872- It . IsAny < CancellationToken > ( ) , It . IsAny < ProposalSourceType > ( ) ,
873- It . IsAny < string ? > ( ) , It . IsAny < string ? > ( ) ) )
874- . ReturnsAsync ( Result . Failure < ProposalDto > (
875- ErrorCodes . ValidationError ,
876- "Could not parse instruction. Supported patterns: 'create card \" title\" '..." ) ) ;
877-
878- var result = await _service . SendMessageAsync (
879- session . Id ,
880- userId ,
881- new SendChatMessageDto (
882- "please create some tasks for the deployment checklist" ,
883- RequestProposal : true ) ,
884- default ) ;
885-
886- result . IsSuccess . Should ( ) . BeTrue ( ) ;
887- result . Value . MessageType . Should ( ) . Be ( "status" ) ;
888- result . Value . Content . Should ( ) . Contain ( "Could not create the requested proposal" ) ;
889- _plannerMock . Verify (
890- p => p . ParseInstructionAsync (
891- It . IsAny < string > ( ) , userId , boardId ,
892- It . IsAny < CancellationToken > ( ) , ProposalSourceType . Chat ,
893- session . Id . ToString ( ) , It . IsAny < string ? > ( ) ) ,
894- Times . Once ) ;
895- }
896-
897- /// <summary>
898- /// Classifier detects actionable intent but parser fails on the raw message
899- /// (e.g., message says "create card for testing" but lacks quoted title).
900- /// Verifies the hint message is shown to the user.
901- /// </summary>
902- [ Fact ]
903- public async Task SendMessageAsync_ActionableClassification_ParserFails_ShowsParseHint ( )
904- {
905- var userId = Guid . NewGuid ( ) ;
906- var boardId = Guid . NewGuid ( ) ;
907- var session = new ChatSession ( userId , "Actionable parse fail" , boardId ) ;
908-
909- _chatSessionRepoMock
910- . Setup ( r => r . GetByIdWithMessagesAsync ( session . Id , default ) )
911- . ReturnsAsync ( session ) ;
912- _llmProviderMock
913- . Setup ( p => p . CompleteAsync ( It . IsAny < ChatCompletionRequest > ( ) , default ) )
914- . ReturnsAsync ( new LlmCompletionResult (
915- "I'll help you create that." , 10 , true , "card.create" ) ) ;
916- _plannerMock
917- . Setup ( p => p . ParseInstructionAsync (
918- It . IsAny < string > ( ) , userId , boardId ,
919- It . IsAny < CancellationToken > ( ) , It . IsAny < ProposalSourceType > ( ) ,
920- It . IsAny < string ? > ( ) , It . IsAny < string ? > ( ) ) )
921- . ReturnsAsync ( Result . Failure < ProposalDto > (
922- ErrorCodes . ValidationError , "Could not parse instruction" ) ) ;
923-
924- var result = await _service . SendMessageAsync (
925- session . Id ,
926- userId ,
927- new SendChatMessageDto ( "create card for testing without quotes" ) ,
928- default ) ;
929-
930- result . IsSuccess . Should ( ) . BeTrue ( ) ;
931- result . Value . MessageType . Should ( ) . Be ( "status" ) ;
932- result . Value . Content . Should ( ) . Contain ( "detected a task request but could not parse it" ) ;
933- }
934-
935- #endregion
936-
937763 #region NLP Gap Tests — Documents #570 (Chat-to-Proposal NLP Gap)
938764
939765 /// <summary>
0 commit comments