| 1 | using CascadeIDE.Models.Editor; |
| 2 | using Xunit; |
| 3 | |
| 4 | namespace CascadeIDE.Tests; |
| 5 | |
| 6 | public sealed class ParametricIntentMelodyTests |
| 7 | { |
| 8 | [Theory] |
| 9 | [InlineData("wai:", true, "")] |
| 10 | [InlineData("wai:google.com", true, "google.com")] |
| 11 | [InlineData("wai:https://host/p", true, "https://host/p")] |
| 12 | [InlineData("wa", false, null)] |
| 13 | [InlineData("wai", true, "")] |
| 14 | public void TryParseWebAiPortalMelodyTail_PrefixAndPayload(string tail, bool expectedOk, string? expectedPayload) |
| 15 | { |
| 16 | var ok = ParametricIntentMelody.TryParseWebAiPortalMelodyTail(tail, out var slugOut, out var payload); |
| 17 | Assert.Equal(expectedOk, ok); |
| 18 | Assert.Equal(expectedPayload, ok ? payload : null); |
| 19 | if (ok) |
| 20 | Assert.Equal("wai", slugOut); |
| 21 | } |
| 22 | |
| 23 | [Fact] |
| 24 | public void TryParseLineRangeTail_Parses_EditorLineAliases() |
| 25 | { |
| 26 | Assert.True(ParametricIntentMelody.TryParseLineRangeTail("els:5:15", out var parsed)); |
| 27 | Assert.NotNull(parsed); |
| 28 | Assert.Equal("els", parsed!.Alias); |
| 29 | Assert.Equal(5, parsed.Lines.Start.Value); |
| 30 | Assert.Equal(15, parsed.Lines.End.Value); |
| 31 | } |
| 32 | |
| 33 | [Fact] |
| 34 | public void TryParseLineRangeTail_Normalizes_space_separated_three_tokens() |
| 35 | { |
| 36 | Assert.True(ParametricIntentMelody.TryParseLineRangeTail("els 5 15", out var parsed)); |
| 37 | Assert.NotNull(parsed); |
| 38 | Assert.Equal("els", parsed!.Alias); |
| 39 | Assert.Equal("els:5:15", parsed.DisplayTail); |
| 40 | Assert.Equal(5, parsed.Lines.Start.Value); |
| 41 | Assert.Equal(15, parsed.Lines.End.Value); |
| 42 | } |
| 43 | |
| 44 | [Fact] |
| 45 | public void TryBuildExecutionArgs_ForSelect_UsesFullLineColumns() |
| 46 | { |
| 47 | var parsed = ParsedLineRange("els", "els:2:3", 2, 3); |
| 48 | var text = "one\ntwo\nthree\nfour"; |
| 49 | |
| 50 | var ok = ParametricIntentMelody.TryBuildExecutionArgs( |
| 51 | parsed, |
| 52 | @"D:\repo\file.txt", |
| 53 | text, |
| 54 | out var commandId, |
| 55 | out var argsJson, |
| 56 | out var error); |
| 57 | |
| 58 | Assert.True(ok, error); |
| 59 | Assert.Equal(IdeCommands.Select, commandId); |
| 60 | Assert.Contains(@"""start_line"":2", argsJson, StringComparison.Ordinal); |
| 61 | Assert.Contains(@"""start_column"":1", argsJson, StringComparison.Ordinal); |
| 62 | Assert.Contains(@"""end_line"":3", argsJson, StringComparison.Ordinal); |
| 63 | Assert.Contains(@"""end_column"":6", argsJson, StringComparison.Ordinal); |
| 64 | } |
| 65 | |
| 66 | [Fact] |
| 67 | public void TryBuildExecutionArgs_ForDelete_RemovesWholeLines() |
| 68 | { |
| 69 | var parsed = ParsedLineRange("eld", "eld:2:3", 2, 3); |
| 70 | var text = "one\ntwo\nthree\nfour"; |
| 71 | |
| 72 | var ok = ParametricIntentMelody.TryBuildExecutionArgs( |
| 73 | parsed, |
| 74 | @"D:\repo\file.txt", |
| 75 | text, |
| 76 | out var commandId, |
| 77 | out var argsJson, |
| 78 | out var error); |
| 79 | |
| 80 | Assert.True(ok, error); |
| 81 | Assert.Equal(IdeCommands.ApplyEdit, commandId); |
| 82 | Assert.Contains(@"""start_line"":2", argsJson, StringComparison.Ordinal); |
| 83 | Assert.Contains(@"""start_column"":1", argsJson, StringComparison.Ordinal); |
| 84 | Assert.Contains(@"""end_line"":4", argsJson, StringComparison.Ordinal); |
| 85 | Assert.Contains(@"""end_column"":1", argsJson, StringComparison.Ordinal); |
| 86 | Assert.Contains(@"""new_text"":""""", argsJson, StringComparison.Ordinal); |
| 87 | } |
| 88 | |
| 89 | [Fact] |
| 90 | public void TryParseLineRangeTail_Canonicalizes_ReversedBounds_ToSameRange() |
| 91 | { |
| 92 | Assert.True(ParametricIntentMelody.TryParseLineRangeTail("els:7:3", out var reversed)); |
| 93 | Assert.True(ParametricIntentMelody.TryParseLineRangeTail("els:3:7", out var forward)); |
| 94 | Assert.NotNull(reversed); |
| 95 | Assert.NotNull(forward); |
| 96 | Assert.Equal(forward!.Lines, reversed!.Lines); |
| 97 | Assert.Equal(3, reversed.Lines.Start.Value); |
| 98 | Assert.Equal(7, reversed.Lines.End.Value); |
| 99 | } |
| 100 | |
| 101 | [Theory] |
| 102 | [InlineData("els;7")] |
| 103 | [InlineData("els:7")] |
| 104 | [InlineData("els 7")] |
| 105 | [InlineData("eld;7")] |
| 106 | [InlineData("eld:7")] |
| 107 | [InlineData("eld 7")] |
| 108 | public void TryParseLineRangeTail_SingleLineNumber_IsInclusiveOneLineRange(string tail) |
| 109 | { |
| 110 | Assert.True(ParametricIntentMelody.TryParseLineRangeTail(tail, out var parsed), tail); |
| 111 | Assert.NotNull(parsed); |
| 112 | Assert.Equal(7, parsed!.Lines.Start.Value); |
| 113 | Assert.Equal(7, parsed.Lines.End.Value); |
| 114 | } |
| 115 | |
| 116 | [Theory] |
| 117 | [InlineData("esc:", true, "")] |
| 118 | [InlineData("esc:[m:run]", true, "[m:run]")] |
| 119 | [InlineData("erc:[f:foo.cs; m:bar]", true, "[f:foo.cs; m:bar]")] |
| 120 | [InlineData("er", false, null)] |
| 121 | public void TryParseBracketCodeRefMelodyTail_PrefixAndPayload(string tail, bool expectedOk, string? expectedPayload) |
| 122 | { |
| 123 | var ok = ParametricIntentMelody.TryParseBracketCodeRefMelodyTail(tail, out var slugOut, out var payload); |
| 124 | Assert.Equal(expectedOk, ok); |
| 125 | Assert.Equal(expectedPayload, ok ? payload : null); |
| 126 | if (ok) |
| 127 | { |
| 128 | var expectedSlug = tail.StartsWith("erc", StringComparison.Ordinal) ? "erc" : "esc"; |
| 129 | Assert.Equal(expectedSlug, slugOut); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | [Fact] |
| 134 | public void TryResolveParametricExecution_BracketSelect_BuildsEditorSelectCodeArgs() |
| 135 | { |
| 136 | var ok = ParametricIntentMelody.TryResolveParametricExecution( |
| 137 | "esc:[M:Run]", |
| 138 | @"D:\repo\src\Foo.cs", |
| 139 | "", |
| 140 | out var commandId, |
| 141 | out var argsJson, |
| 142 | out _); |
| 143 | |
| 144 | Assert.True(ok); |
| 145 | Assert.Equal(IdeCommands.EditorSelectCode, commandId); |
| 146 | Assert.Contains(@"""code_ref"":""[M:Run]""", argsJson, StringComparison.Ordinal); |
| 147 | Assert.Contains("Foo.cs", argsJson!, StringComparison.Ordinal); |
| 148 | } |
| 149 | |
| 150 | [Theory] |
| 151 | [InlineData("e", true)] |
| 152 | [InlineData("es", true)] |
| 153 | [InlineData("esc", true)] |
| 154 | [InlineData("esc:", true)] |
| 155 | [InlineData("esc:[", true)] |
| 156 | [InlineData("erc:[m:run", true)] |
| 157 | [InlineData("ex", false)] |
| 158 | public void IsParametricChordTailPrefix_MatchesBracketMelodyTails(string tail, bool expected) => |
| 159 | Assert.Equal(expected, ParametricIntentMelody.IsParametricChordTailPrefix(tail)); |
| 160 | |
| 161 | [Theory] |
| 162 | [InlineData("w", true)] |
| 163 | [InlineData("wa", true)] |
| 164 | [InlineData("wai", true)] |
| 165 | [InlineData("wai:", true)] |
| 166 | [InlineData("wai:g", true)] |
| 167 | [InlineData("wag", false)] |
| 168 | [InlineData("el", true)] |
| 169 | [InlineData("els", true)] |
| 170 | [InlineData("els:", true)] |
| 171 | [InlineData("els;", true)] |
| 172 | [InlineData("els:5", true)] |
| 173 | [InlineData("els;5", true)] |
| 174 | [InlineData("els 7", true)] |
| 175 | [InlineData("els:5:", true)] |
| 176 | [InlineData("els;5;", true)] |
| 177 | [InlineData("els:5:10", true)] |
| 178 | [InlineData("els;5;10", true)] |
| 179 | [InlineData("els:5:x", false)] |
| 180 | public void IsParametricChordTailPrefix_MatchesChordBuildingTails(string tail, bool expected) => |
| 181 | Assert.Equal(expected, ParametricIntentMelody.IsParametricChordTailPrefix(tail)); |
| 182 | |
| 183 | [Theory] |
| 184 | [InlineData("wai", true)] |
| 185 | [InlineData("els", true)] |
| 186 | [InlineData("eld", true)] |
| 187 | [InlineData("so", false)] |
| 188 | [InlineData("gs", false)] |
| 189 | public void IsParametricMelodyBaseAlias_RecognizesParametricRoots(string alias, bool expected) => |
| 190 | Assert.Equal(expected, ParametricIntentMelody.IsParametricMelodyBaseAlias(alias)); |
| 191 | |
| 192 | [Fact] |
| 193 | public void BuildAliasUsageHint_reads_palette_usage_hint_from_catalog() |
| 194 | { |
| 195 | var h = ParametricIntentMelody.BuildAliasUsageHint("els"); |
| 196 | Assert.Contains("els", h, StringComparison.Ordinal); |
| 197 | Assert.Contains(";", h, StringComparison.Ordinal); |
| 198 | } |
| 199 | |
| 200 | [Fact] |
| 201 | public void BuildAliasUsageCategory_resolves_palette_hint_slug_to_root() |
| 202 | { |
| 203 | Assert.Equal("Web AI Portal", ParametricIntentMelody.BuildAliasUsageCategory("wai-url")); |
| 204 | Assert.Equal("Web AI Portal", ParametricIntentMelody.BuildAliasUsageCategory("wai")); |
| 205 | } |
| 206 | |
| 207 | [Fact] |
| 208 | public void BuildAliasUsageHint_unknown_slug_falls_back_to_template() => |
| 209 | Assert.Equal("c:zzz:<start>:<end>", ParametricIntentMelody.BuildAliasUsageHint("zzz")); |
| 210 | |
| 211 | private static ParametricIntentMelody.ParsedLineRange ParsedLineRange(string alias, string display, int start, int end) |
| 212 | { |
| 213 | Assert.True(LineNumber.TryCreate(start, out var s)); |
| 214 | Assert.True(LineNumber.TryCreate(end, out var e)); |
| 215 | Assert.True(LineRange.TryCreate(s, e, out var lines)); |
| 216 | return new ParametricIntentMelody.ParsedLineRange(alias, display, lines); |
| 217 | } |
| 218 | } |
| 219 | |