| 1 | using CascadeIDE.Services; |
| 2 | using Xunit; |
| 3 | |
| 4 | namespace CascadeIDE.Tests; |
| 5 | |
| 6 | public sealed class IntentMelodyAliasesTests |
| 7 | { |
| 8 | [Theory] |
| 9 | [InlineData("c:gs", "gs")] |
| 10 | [InlineData("c: GS", "gs")] |
| 11 | [InlineData(" c:br ", "br")] |
| 12 | [InlineData("c:", "")] |
| 13 | public void TryGetTail_ParsesPrefix(string raw, string expectedTail) => |
| 14 | Assert.True(IntentMelodyAliases.TryGetTail(raw, out var tail) && tail == expectedTail); |
| 15 | |
| 16 | [Fact] |
| 17 | public void TryGetTail_RejectsNonMelody() => |
| 18 | Assert.False(IntentMelodyAliases.TryGetTail("f:foo", out _)); |
| 19 | |
| 20 | [Fact] |
| 21 | public void TryResolveExact_gs_IsGitStatus() => |
| 22 | Assert.Equal(IdeCommands.GitStatus, IntentMelodyAliases.TryResolveExactCommandId("gs")); |
| 23 | |
| 24 | [Fact] |
| 25 | public void TryResolveExact_dl_IsDebugLaunch() => |
| 26 | Assert.Equal(IdeCommands.DebugLaunch, IntentMelodyAliases.TryResolveExactCommandId("dl")); |
| 27 | |
| 28 | [Fact] |
| 29 | public void TryResolveExact_debug_step_melodies_map_to_dap_commands() |
| 30 | { |
| 31 | Assert.Equal(IdeCommands.DebugStepOver, IntentMelodyAliases.TryResolveExactCommandId("dn")); |
| 32 | Assert.Equal(IdeCommands.DebugStepInto, IntentMelodyAliases.TryResolveExactCommandId("di")); |
| 33 | Assert.Equal(IdeCommands.DebugStepOut, IntentMelodyAliases.TryResolveExactCommandId("df")); |
| 34 | Assert.Equal(IdeCommands.DebugStop, IntentMelodyAliases.TryResolveExactCommandId("dx")); |
| 35 | } |
| 36 | |
| 37 | [Fact] |
| 38 | public void TryResolveExact_so_IsOpenSolutionDialog() => |
| 39 | Assert.Equal(IdeCommands.OpenSolutionDialog, IntentMelodyAliases.TryResolveExactCommandId("so")); |
| 40 | |
| 41 | [Fact] |
| 42 | public void TryResolveExact_tol_IsToggleWorkspaceSplittersLock() => |
| 43 | Assert.Equal(IdeCommands.ToggleWorkspaceSplittersLock, IntentMelodyAliases.TryResolveExactCommandId("tol")); |
| 44 | |
| 45 | [Fact] |
| 46 | public void TryResolveExact_chat_and_agent_melodies_map_to_commands() |
| 47 | { |
| 48 | Assert.Equal(IdeCommands.ShowChatPage, IntentMelodyAliases.TryResolveExactCommandId("cps")); |
| 49 | Assert.Equal(IdeCommands.SendChat, IntentMelodyAliases.TryResolveExactCommandId("cs")); |
| 50 | Assert.Equal(IdeCommands.ChatExportReadable, IntentMelodyAliases.TryResolveExactCommandId("cex")); |
| 51 | Assert.Equal(IdeCommands.ForkChatThread, IntentMelodyAliases.TryResolveExactCommandId("ctf")); |
| 52 | Assert.Equal(IdeCommands.ChatSelectPrevMessage, IntentMelodyAliases.TryResolveExactCommandId("amp")); |
| 53 | Assert.Equal(IdeCommands.ChatSelectNextMessage, IntentMelodyAliases.TryResolveExactCommandId("amn")); |
| 54 | Assert.Equal(IdeCommands.ChatToggleSelectedThinking, IntentMelodyAliases.TryResolveExactCommandId("amt")); |
| 55 | Assert.Equal(IdeCommands.ChatToggleShowThinkingInHistory, IntentMelodyAliases.TryResolveExactCommandId("amh")); |
| 56 | Assert.Equal(IdeCommands.ChatSelectPrevThread, IntentMelodyAliases.TryResolveExactCommandId("atp")); |
| 57 | Assert.Equal(IdeCommands.ChatSelectNextThread, IntentMelodyAliases.TryResolveExactCommandId("atn")); |
| 58 | Assert.Equal(IdeCommands.ChatOpenSelectedThread, IntentMelodyAliases.TryResolveExactCommandId("ato")); |
| 59 | Assert.Equal(IdeCommands.ChatShowThreadOverview, IntentMelodyAliases.TryResolveExactCommandId("atb")); |
| 60 | } |
| 61 | |
| 62 | [Fact] |
| 63 | public void TryResolveExact_environment_and_terminal_surface_melodies() |
| 64 | { |
| 65 | Assert.Equal(IdeCommands.BuildSolutionUi, IntentMelodyAliases.TryResolveExactCommandId("br")); |
| 66 | Assert.Equal(IdeCommands.BuildStructured, IntentMelodyAliases.TryResolveExactCommandId("bs")); |
| 67 | Assert.Equal(IdeCommands.ShowEnvironmentReadinessPage, IntentMelodyAliases.TryResolveExactCommandId("ers")); |
| 68 | Assert.Equal(IdeCommands.ShowHybridIndexPage, IntentMelodyAliases.TryResolveExactCommandId("his")); |
| 69 | Assert.Equal(IdeCommands.ShowWebAiPortalPage, IntentMelodyAliases.TryResolveExactCommandId("wai")); |
| 70 | Assert.Equal(IdeCommands.ShowTerminalPanel, IntentMelodyAliases.TryResolveExactCommandId("ts")); |
| 71 | Assert.Equal(IdeCommands.Select, IntentMelodyAliases.TryResolveExactCommandId("els")); |
| 72 | Assert.Equal(IdeCommands.ApplyEdit, IntentMelodyAliases.TryResolveExactCommandId("eld")); |
| 73 | } |
| 74 | |
| 75 | [Fact] |
| 76 | public void HasStrictLongerAliasPrefix_er_true_ers_extends() => |
| 77 | Assert.True(IntentMelodyAliases.HasStrictLongerAliasPrefix("er")); |
| 78 | |
| 79 | [Fact] |
| 80 | public void Bundled_intent_melody_toml_is_readable_like_runtime() |
| 81 | { |
| 82 | Assert.True(BundledAppContent.TryReadDiskThenEmbedded(IntentMelodyAliases.BundledRelativePath, out var text)); |
| 83 | Assert.Contains("intent_catalog_schema_version = 2", text, StringComparison.Ordinal); |
| 84 | Assert.Contains("[[tail_wire_class]]", text, StringComparison.Ordinal); |
| 85 | Assert.Contains("[[command]]", text, StringComparison.Ordinal); |
| 86 | Assert.Contains("[[command.slash]]", text, StringComparison.Ordinal); |
| 87 | Assert.Contains("path = \"/terminal show\"", text, StringComparison.Ordinal); |
| 88 | Assert.Contains("melody_slug = \"ers\"", text, StringComparison.Ordinal); |
| 89 | Assert.Contains("command_id = \"show_environment_readiness_page\"", text, StringComparison.Ordinal); |
| 90 | Assert.Contains("melody_slug = \"his\"", text, StringComparison.Ordinal); |
| 91 | Assert.Contains("command_id = \"show_hybrid_index_page\"", text, StringComparison.Ordinal); |
| 92 | Assert.Contains("melody_slug = \"wai\"", text, StringComparison.Ordinal); |
| 93 | Assert.Contains("melody_palette_hint_slug = \"wai-url\"", text, StringComparison.Ordinal); |
| 94 | Assert.Contains("command_id = \"show_web_ai_portal_page\"", text, StringComparison.Ordinal); |
| 95 | Assert.Contains("melody_slug = \"ts\"", text, StringComparison.Ordinal); |
| 96 | Assert.Contains("command_id = \"show_terminal_panel\"", text, StringComparison.Ordinal); |
| 97 | Assert.Contains("melody_slug = \"dl\"", text, StringComparison.Ordinal); |
| 98 | Assert.Contains("command_id = \"debug_launch\"", text, StringComparison.Ordinal); |
| 99 | Assert.Contains("melody_slug = \"dn\"", text, StringComparison.Ordinal); |
| 100 | Assert.Contains("command_id = \"debug_step_over\"", text, StringComparison.Ordinal); |
| 101 | Assert.Contains("melody_slug = \"df\"", text, StringComparison.Ordinal); |
| 102 | Assert.Contains("command_id = \"debug_step_out\"", text, StringComparison.Ordinal); |
| 103 | Assert.Contains("melody_slug = \"els\"", text, StringComparison.Ordinal); |
| 104 | Assert.Contains("command_id = \"select\"", text, StringComparison.Ordinal); |
| 105 | Assert.Contains("melody_slug = \"eld\"", text, StringComparison.Ordinal); |
| 106 | Assert.Contains("command_id = \"apply_edit\"", text, StringComparison.Ordinal); |
| 107 | Assert.Contains("melody_slug = \"tol\"", text, StringComparison.Ordinal); |
| 108 | Assert.Contains("command_id = \"toggle_workspace_splitters_lock\"", text, StringComparison.Ordinal); |
| 109 | Assert.Contains("slash_group = \"Панели\"", text, StringComparison.Ordinal); |
| 110 | Assert.Contains("path = \"/mfd toggle\"", text, StringComparison.Ordinal); |
| 111 | } |
| 112 | |
| 113 | [Fact] |
| 114 | public void ParseBundle_command_model_flat_melody_and_form_slash() |
| 115 | { |
| 116 | const string toml = |
| 117 | """ |
| 118 | intent_catalog_schema_version = 1 |
| 119 | [[command]] |
| 120 | command_id = "git_status" |
| 121 | melody_slug = "gs" |
| 122 | melody_shape = "simple" |
| 123 | [[command.form.slash]] |
| 124 | path = "/git status" |
| 125 | help = "git status" |
| 126 | """; |
| 127 | |
| 128 | var bundle = IntentMelodyAliases.ParseBundleForTests(toml.Trim()); |
| 129 | |
| 130 | Assert.Equal(IdeCommands.GitStatus, IntentMelodyAliases.TryResolveExactCommandId("gs")); |
| 131 | Assert.True(bundle.Catalog.SlashRoutes.TryGetValue("/git status", out var slash)); |
| 132 | Assert.Equal(IdeCommands.GitStatus, slash.CommandId); |
| 133 | } |
| 134 | |
| 135 | [Fact] |
| 136 | public void Tomlyn_deserializes_between_slots_two_single_char_literals() |
| 137 | { |
| 138 | const string minimal = |
| 139 | """ |
| 140 | intent_catalog_schema_version = 1 |
| 141 | |
| 142 | [[tail_wire_class]] |
| 143 | id = "int_chain_colon_space" |
| 144 | kind = "delimited_slots" |
| 145 | between_slots_any_of = [":", " "] |
| 146 | |
| 147 | [[command]] |
| 148 | command_id = "git_status" |
| 149 | [command.melody] |
| 150 | slug = "z" |
| 151 | shape = "simple" |
| 152 | """; |
| 153 | |
| 154 | var parsed = CascadeTomlSerializer.Deserialize<IntentMelodyAliases.IntentMelodyTomlRoot>(minimal.Trim()); |
| 155 | Assert.NotNull(parsed?.TailWireClass); |
| 156 | Assert.Single(parsed.TailWireClass); |
| 157 | var row = parsed.TailWireClass[0]; |
| 158 | Assert.NotNull(row.BetweenSlotsAnyOf); |
| 159 | Assert.Equal(2, row.BetweenSlotsAnyOf!.Length); |
| 160 | Assert.Equal(":", row.BetweenSlotsAnyOf![0]); |
| 161 | Assert.Single(row.BetweenSlotsAnyOf![1]!); |
| 162 | Assert.Equal(' ', row.BetweenSlotsAnyOf![1]![0]); |
| 163 | |
| 164 | IntentMelodyAliases.ParseBundleForTests(minimal.Trim()); |
| 165 | } |
| 166 | |
| 167 | [Fact] |
| 168 | public void ParseBundle_command_model_melody_and_slash_forms() |
| 169 | { |
| 170 | const string toml = |
| 171 | """ |
| 172 | intent_catalog_schema_version = 1 |
| 173 | [[command]] |
| 174 | command_id = "git_status" |
| 175 | [command.melody] |
| 176 | slug = "gs" |
| 177 | shape = "simple" |
| 178 | [[command.slash]] |
| 179 | path = "/git status" |
| 180 | help = "git status" |
| 181 | """; |
| 182 | |
| 183 | var bundle = IntentMelodyAliases.ParseBundleForTests(toml.Trim()); |
| 184 | |
| 185 | Assert.Equal(IdeCommands.GitStatus, IntentMelodyAliases.TryResolveExactCommandId("gs")); |
| 186 | Assert.True(bundle.Catalog.SlashRoutes.TryGetValue("/git status", out var slash)); |
| 187 | Assert.Equal(IdeCommands.GitStatus, slash.CommandId); |
| 188 | } |
| 189 | |
| 190 | [Fact] |
| 191 | public void ParseBundle_infers_show_usage_hint_for_two_int_parametric_without_explicit_flag() |
| 192 | { |
| 193 | const string toml = |
| 194 | """ |
| 195 | intent_catalog_schema_version = 1 |
| 196 | |
| 197 | [[tail_wire_class]] |
| 198 | id = "int_chain_colon_space" |
| 199 | kind = "delimited_slots" |
| 200 | between_slots_any_of = [":", " "] |
| 201 | |
| 202 | [[command]] |
| 203 | command_id = "select" |
| 204 | [command.melody] |
| 205 | slug = "zz" |
| 206 | shape = "parametric" |
| 207 | tail_signature = "<start:ln>:<end:ln>" |
| 208 | wire_class = "int_chain_colon_space" |
| 209 | chord_commit = "enter" |
| 210 | """; |
| 211 | |
| 212 | var bundle = IntentMelodyAliases.ParseBundleForTests(toml.Trim()); |
| 213 | |
| 214 | Assert.True(bundle.Catalog.Roots.TryGetValue("zz", out var z)); |
| 215 | Assert.True(z.ShowUsageHintIfBareSlug); |
| 216 | } |
| 217 | |
| 218 | /// <summary>При локальном <c>dotnet test</c> полный проход <see cref="IntentMelodyAliases.Build"/> по embedded TOML (без дискового оверлея в <c>bin/</c>).</summary> |
| 219 | [Fact] |
| 220 | public void Embedded_intent_melody_ParseBundle_Build_succeeds() |
| 221 | { |
| 222 | Assert.True(BundledAppContent.TryReadEmbeddedText(IntentMelodyAliases.BundledRelativePath, out var text)); |
| 223 | var bundle = IntentMelodyAliases.ParseBundleForTests(text.Trim()); |
| 224 | |
| 225 | Assert.NotEmpty(bundle.AliasToCommandId); |
| 226 | Assert.NotEmpty(bundle.Catalog.Roots); |
| 227 | Assert.NotEmpty(bundle.Catalog.TailWireClasses); |
| 228 | Assert.NotEmpty(bundle.Catalog.SlashRoutes); |
| 229 | Assert.True(bundle.Catalog.SlashRoutes.TryGetValue("/terminal show", out var term)); |
| 230 | Assert.Equal(IdeCommands.SetMfdShellPage, term.CommandId); |
| 231 | Assert.Equal("Terminal", term.MfdPage); |
| 232 | Assert.Contains(bundle.Catalog.Roots.Values, static e => e.Shape == IntentMelodyShape.Parametric); |
| 233 | Assert.True(bundle.Catalog.Roots["els"].ShowUsageHintIfBareSlug, "els — два int-слота, подсказка по умолчанию"); |
| 234 | Assert.False(bundle.Catalog.Roots["wai"].ShowUsageHintIfBareSlug, "wai — явно false в TOML"); |
| 235 | } |
| 236 | |
| 237 | [Fact] |
| 238 | public void HasStrictLongerAliasPrefix_ce_true_cex_extends() => |
| 239 | Assert.True(IntentMelodyAliases.HasStrictLongerAliasPrefix("ce")); |
| 240 | |
| 241 | [Fact] |
| 242 | public void FilterByTailPrefix_c_Matches_chat_core_melodies() => |
| 243 | Assert.Equal(6, IntentMelodyAliases.FilterByTailPrefix("c").Count); |
| 244 | |
| 245 | [Fact] |
| 246 | public void FilterByTailPrefix_a_Matches_agent_navigation_melodies() => |
| 247 | Assert.Equal(8, IntentMelodyAliases.FilterByTailPrefix("a").Count); |
| 248 | |
| 249 | [Fact] |
| 250 | public void FilterByTailPrefix_g_Matches_gs_gc_gp_gsu() => |
| 251 | Assert.Equal(4, IntentMelodyAliases.FilterByTailPrefix("g").Count); |
| 252 | |
| 253 | [Fact] |
| 254 | public void HasStrictLongerAliasPrefix_gs_true_gsu_extends() => |
| 255 | Assert.True(IntentMelodyAliases.HasStrictLongerAliasPrefix("gs")); |
| 256 | |
| 257 | [Fact] |
| 258 | public void HasStrictLongerAliasPrefix_so_false() => |
| 259 | Assert.False(IntentMelodyAliases.HasStrictLongerAliasPrefix("so")); |
| 260 | |
| 261 | [Fact] |
| 262 | public void SampleAliasesForFooter_lists_aliases() |
| 263 | { |
| 264 | var s = IntentMelodyAliases.SampleAliasesForFooter(4); |
| 265 | Assert.False(string.IsNullOrWhiteSpace(s)); |
| 266 | Assert.Contains(",", s, StringComparison.Ordinal); |
| 267 | } |
| 268 | } |
| 269 | |