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)
///