Skip to content

Commit 4c5fdb5

Browse files
committed
Remove duplicate ChatService flow tests and harden null-input test
- Remove 4 ChatServiceTests that duplicated existing tests covering identical code paths (structured-syntax success, classifier miss, explicit RequestProposal failure, actionable-but-parser-fails) - Change Classify_NullInput test to assert base Exception instead of NullReferenceException so it survives addition of a null guard
1 parent ab59b76 commit 4c5fdb5

2 files changed

Lines changed: 4 additions & 176 deletions

File tree

backend/tests/Taskdeck.Application.Tests/Services/ChatServiceTests.cs

Lines changed: 0 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

backend/tests/Taskdeck.Application.Tests/Services/LlmIntentClassifierTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,15 @@ public void Classify_NonActionable_ShouldReturnFalse(string message)
145145
#region Edge Cases — Input Extremes
146146

147147
[Fact]
148-
public void Classify_NullInput_ThrowsNullReferenceException()
148+
public void Classify_NullInput_Throws()
149149
{
150150
// The classifier calls message.ToLowerInvariant() without a null guard.
151151
// This documents that null input is not handled gracefully.
152+
// Using base Exception type so the test survives if a null guard
153+
// (ArgumentNullException) is added later.
152154
var act = () => LlmIntentClassifier.Classify(null!);
153155

154-
act.Should().Throw<NullReferenceException>();
156+
act.Should().Throw<Exception>();
155157
}
156158

157159
[Fact]

0 commit comments

Comments
 (0)