Forge
csharpdeeb25a2
1using System.Text.Json;
2using CascadeIDE.Features.WebAiPortal.Application;
3using CascadeIDE.Services;
4using Xunit;
5
6namespace CascadeIDE.Tests;
7
8public sealed class WebAiPortalChatMixInFormatterTests
9{
10 [Fact]
11 public void BuildForComposer_keeps_short_response()
12 {
13 const string body = """{"hits":[]}""";
14 var r = WebAiPortalChatMixInFormatter.BuildForComposer(true, 1200, IdeCommands.CodebaseIndexSearch, body);
15 Assert.False(r.UsedCompactMixer);
16 Assert.Equal(body, r.TextForComposer);
17 }
18
19 [Fact]
20 public void BuildForComposer_compacts_large_get_editor_state_with_hci_cascade_hints()
21 {
22 const string previewLeakMarker = "<<<PREVIEW_BLOB>>>";
23 var dto = new EditorStateDto
24 {
25 FilePath = @"D:\repo\Prog.cs",
26 CaretLine = 100,
27 CaretColumn = 5,
28 SelectionStart = 0,
29 SelectionLength = 0,
30 SelectionText = "",
31 ContentLength = 50_000,
32 IsEmpty = false,
33 ContentPreview = previewLeakMarker.PadRight(15_000, '_'),
34 };
35 var huge = JsonSerializer.Serialize(dto);
36 Assert.True(huge.Length > 1200);
37
38 var r = WebAiPortalChatMixInFormatter.BuildForComposer(
39 preferCompact: true,
40 maxChatCharacters: 1200,
41 executedCommandId: IdeCommands.GetEditorState,
42 fullResponseText: huge);
43
44 Assert.True(r.UsedCompactMixer);
45 Assert.True(r.TextForComposer.Length < huge.Length / 10, "компакт сильно короче сырья");
46 Assert.Contains("codebase_index_search", r.TextForComposer, StringComparison.Ordinal);
47 Assert.Contains("codebase_index_status", r.TextForComposer, StringComparison.Ordinal);
48 Assert.Contains("get_editor_content_range", r.TextForComposer, StringComparison.Ordinal);
49 Assert.DoesNotContain(previewLeakMarker, r.TextForComposer, StringComparison.Ordinal);
50 Assert.DoesNotContain("```json-cascade", r.TextForComposer, StringComparison.Ordinal);
51 Assert.StartsWith("▸ ", r.TextForComposer.Split('\n', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(static l => l.Contains("codebase_index_status", StringComparison.Ordinal)), StringComparison.Ordinal);
52 }
53
54 [Fact]
55 public void BuildForComposer_generic_oversized_mentions_explain_when_command_unknown_shape()
56 {
57 var slab = new string('{', 3_000);
58 var r = WebAiPortalChatMixInFormatter.BuildForComposer(true, 800, "codebase_index_search", slab);
59 Assert.True(r.UsedCompactMixer);
60 Assert.Contains("codebase_index_explain", r.TextForComposer, StringComparison.Ordinal);
61 Assert.True(r.TextForComposer.Length < slab.Length, "компакт короче бессмысленного сырья");
62 Assert.DoesNotContain("```json-cascade", r.TextForComposer, StringComparison.Ordinal);
63 }
64
65 [Fact]
66 public void BuildForComposer_compacts_oversized_search_into_hit_table_not_raw_json()
67 {
68 const int hugeSnippet = 800;
69 object hit(int id) => new
70 {
71 hitId = (long)id,
72 path = $"src/Chunk{id}.cs",
73 extension = ".cs",
74 hitKind = "text_fts",
75 rankScore = 0.9 - id * 0.01,
76 ftsScore = (double?)0.5,
77 vecScore = (double?)null,
78 snippet = new string('x', hugeSnippet),
79 lineStart = id,
80 lineEnd = id + 20,
81 chunkCharCount = 400,
82 lastWriteUtcIso = (string?)null,
83 };
84 var hits = Enumerable.Range(1, 10).Select(hit).ToArray();
85 var body = JsonSerializer.Serialize(
86 new
87 {
88 err = (string?)null,
89 indexFormatVersion = 2,
90 query = "class Program",
91 databasePath = "D:\\db\\hc.sqlite",
92 hits,
93 },
94 new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
95 Assert.True(body.Length > WebAiPortalChatMixInFormatter.DefaultMaxChatCharacters);
96
97 var r = WebAiPortalChatMixInFormatter.BuildForComposer(true, 1200, IdeCommands.CodebaseIndexSearch, body);
98 Assert.True(r.UsedCompactMixer);
99 Assert.Contains("hit_id=1", r.TextForComposer, StringComparison.Ordinal);
100 Assert.Contains("codebase_index_explain", r.TextForComposer, StringComparison.Ordinal);
101 Assert.Contains("▸ ", r.TextForComposer, StringComparison.Ordinal);
102 Assert.DoesNotContain(new string('x', 150), r.TextForComposer, StringComparison.Ordinal);
103 Assert.DoesNotContain("```json-cascade", r.TextForComposer, StringComparison.Ordinal);
104 Assert.True(r.TextForComposer.Length < body.Length / 3);
105 }
106
107 [Fact]
108 public void BuildForComposer_compacts_oversized_status_to_few_lines()
109 {
110 var exts = Enumerable.Repeat(".cs", 600).Select((_, i) => $".t{i}").ToArray();
111 var body = JsonSerializer.Serialize(
112 new
113 {
114 indexFormatVersion = 2,
115 databasePath = new string('z', 2200),
116 databaseExists = true,
117 documentCount = 2627,
118 documentCountMayBeStale = false,
119 indexedAtIso = "2026-05-10T08:00:00Z",
120 workspaceRoot = @"D:\ws\repo",
121 lastReindexError = (string?)null,
122 lastReindexErrorAtIso = (string?)null,
123 settingsSource = "embedded",
124 settingsParseError = (string?)null,
125 effectiveSettings = new { includeCsInFts = true, extraIncludeRoots = Array.Empty<string>(), excludeRoots = Array.Empty<string>(), effectiveExtensions = exts, excludePathSegments = Array.Empty<string>(), ignoreFiles = Array.Empty<string>(), maxIndexedFileBytes = 524288, chunkLines = 110, chunkOverlapLines = 15, binaryProbeBytes = 8192 },
126 reindexState = "idle",
127 reindexStartedAtIso = (string?)null,
128 },
129 new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
130 Assert.True(body.Length > WebAiPortalChatMixInFormatter.DefaultMaxChatCharacters);
131
132 var r = WebAiPortalChatMixInFormatter.BuildForComposer(true, 1200, IdeCommands.CodebaseIndexStatus, body);
133 Assert.True(r.UsedCompactMixer);
134 Assert.Contains("2627", r.TextForComposer, StringComparison.Ordinal);
135 Assert.Contains("расширений FTS", r.TextForComposer, StringComparison.Ordinal);
136 Assert.True(r.TextForComposer.Length < body.Length / 8);
137 }
138
139 [Fact]
140 public void BuildForComposer_compacts_oversized_editor_content_range()
141 {
142 var content = new string('q', 2600);
143 var body = JsonSerializer.Serialize(
144 new Dictionary<string, object?> { ["file_path"] = @"D:\a\b\Program.cs", ["start_line"] = 1, ["end_line"] = 80, ["content"] = content });
145 Assert.True(body.Length > WebAiPortalChatMixInFormatter.DefaultMaxChatCharacters);
146
147 var r = WebAiPortalChatMixInFormatter.BuildForComposer(true, 1200, IdeCommands.GetEditorContentRange, body);
148 Assert.True(r.UsedCompactMixer);
149 Assert.Contains("get_editor_content_range", r.TextForComposer, StringComparison.Ordinal);
150 Assert.DoesNotContain("```json-cascade", r.TextForComposer, StringComparison.Ordinal);
151 Assert.DoesNotContain(content, r.TextForComposer, StringComparison.Ordinal);
152 Assert.True(r.TextForComposer.Length < body.Length / 2);
153 }
154}
155
View only · write via MCP/CIDE