diff --git a/backend/src/Taskdeck.Application/Services/LlmIntentClassifier.cs b/backend/src/Taskdeck.Application/Services/LlmIntentClassifier.cs index 29372d56b..4ff0e1c83 100644 --- a/backend/src/Taskdeck.Application/Services/LlmIntentClassifier.cs +++ b/backend/src/Taskdeck.Application/Services/LlmIntentClassifier.cs @@ -4,6 +4,9 @@ public static class LlmIntentClassifier { public static (bool IsActionable, string? ActionIntent) Classify(string message) { + if (string.IsNullOrWhiteSpace(message)) + return (false, null); + var lower = message.ToLowerInvariant(); // Card creation — explicit commands and natural language diff --git a/backend/tests/Taskdeck.Application.Tests/Services/LlmIntentClassifierTests.cs b/backend/tests/Taskdeck.Application.Tests/Services/LlmIntentClassifierTests.cs index d4011bce3..f4577031d 100644 --- a/backend/tests/Taskdeck.Application.Tests/Services/LlmIntentClassifierTests.cs +++ b/backend/tests/Taskdeck.Application.Tests/Services/LlmIntentClassifierTests.cs @@ -142,6 +142,84 @@ public void Classify_NonActionable_ShouldReturnFalse(string message) #endregion + #region Edge Cases — Input Extremes + + [Fact] + public void Classify_NullInput_ReturnsNotActionable() + { + var (isActionable, actionIntent) = LlmIntentClassifier.Classify(null!); + + isActionable.Should().BeFalse(); + actionIntent.Should().BeNull(); + } + + [Fact] + public void Classify_VeryLongString_ReturnsNotActionable() + { + var longMessage = new string('x', 50_000); + + var (isActionable, actionIntent) = LlmIntentClassifier.Classify(longMessage); + + isActionable.Should().BeFalse(); + actionIntent.Should().BeNull(); + } + + [Fact] + public void Classify_VeryLongStringContainingPattern_StillMatches() + { + var longMessage = new string('x', 25_000) + " create card for testing " + new string('x', 25_000); + + var (isActionable, actionIntent) = LlmIntentClassifier.Classify(longMessage); + + isActionable.Should().BeTrue(); + actionIntent.Should().Be("card.create"); + } + + [Theory] + [InlineData(" ")] + [InlineData("\t\t")] + [InlineData("\n\n\n")] + public void Classify_WhitespaceOnly_ReturnsNotActionable(string message) + { + var (isActionable, actionIntent) = LlmIntentClassifier.Classify(message); + + isActionable.Should().BeFalse(); + actionIntent.Should().BeNull(); + } + + [Theory] + [InlineData("Hello! @#$%^&*() special chars")] + [InlineData("Unicode: \u00e9\u00e8\u00ea\u00eb\u00fc\u00f6\u00e4")] + [InlineData("")] + [InlineData("SELECT * FROM cards; DROP TABLE boards;")] + public void Classify_SpecialCharacters_WithoutPattern_ReturnsNotActionable(string message) + { + var (isActionable, actionIntent) = LlmIntentClassifier.Classify(message); + + isActionable.Should().BeFalse(); + actionIntent.Should().BeNull(); + } + + [Fact] + public void Classify_PatternWithSpecialCharsSurrounding_StillMatches() + { + var (isActionable, actionIntent) = LlmIntentClassifier.Classify("!!! create card !!! @#$ testing"); + + isActionable.Should().BeTrue(); + actionIntent.Should().Be("card.create"); + } + + [Fact] + public void Classify_PatternWithNewlines_StillMatches() + { + var (isActionable, actionIntent) = LlmIntentClassifier.Classify("line 1\ncreate card for testing\nline 3"); + + isActionable.Should().BeTrue(); + actionIntent.Should().Be("card.create"); + } + + #endregion + #region Known Gaps — Natural Language Misses (Documents #570/#571) ///