Skip to content

Commit 128bc29

Browse files
bkudiessCopilot
andcommitted
test(chat): prove history ID collision rendering
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 6835178 commit 128bc29

6 files changed

Lines changed: 598 additions & 14 deletions

File tree

42.2 KB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
head=683517894bd13dfeeb9996d3934bdac5b4b4863b
2+
dirty=sha256:F10E5599298252AA5BA0941ABB4B159A6B9545C3BF0D546D05409914429A1BEC; files=4; base=4712e227d110746400e93107c60616544d68f7b4
3+
product-version=0.6.13-bkudiess-finalize-history-cache-correlation.1+159.Branch.bkudiess-finalize-history-cache-correlation.Sha.683517894bd13dfeeb9996d3934bdac5b4b4863b.683517894bd13dfeeb9996d3934bdac5b4b4863b
4+
proof-scope=production reducer, activity projection, and Reactor renderer; allocator=focused provider regression
5+
visual=two production tool cards plus the synthetic output text, composed without coordinate cropping
6+
UIA expanded="Activity: Ran 2 commands. 2 tools. Collapsed."
7+
UIA expanded="Tool call Exec. Interrupted."
8+
UIA expanded="Tool call Bash. Done."
9+
UIA tool-row-count=2
10+
UIA structured="automationId=ChatToolCall_e3; state=Interrupted; output=absent"
11+
UIA synthetic="automationId=ChatToolCall_e4; state=Done; output=flattened output owned by history-tool-1"
12+
screenshot=history-collision.png bytes=43204
13+
result=pass

src/OpenClaw.Tray.WinUI/Helpers/VisualTestCapture.cs

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using Microsoft.UI.Xaml;
2+
using Microsoft.UI.Xaml.Automation;
3+
using Microsoft.UI.Xaml.Automation.Peers;
24
using Microsoft.UI.Xaml.Controls;
5+
using Microsoft.UI.Xaml.Documents;
36
using Microsoft.UI.Xaml.Media;
47
using Microsoft.UI.Xaml.Media.Imaging;
58
using OpenClawTray.Services;
@@ -50,21 +53,38 @@ private static async Task CaptureWhenSignaledAsync(
5053
if (!File.Exists(signalPath))
5154
continue;
5255

53-
await CaptureAsync(root, surfaceName);
56+
var rootDir = GetVisualTestDirectory();
57+
if (rootDir is null)
58+
return;
59+
await CaptureToDirectoryAsync(
60+
root,
61+
Path.Combine(rootDir, SanitizePathSegment(surfaceName)),
62+
Environment.GetEnvironmentVariable(
63+
"OPENCLAW_VISUAL_TEST_ELEMENT_AUTOMATION_ID_PREFIX"),
64+
Environment.GetEnvironmentVariable(
65+
"OPENCLAW_VISUAL_TEST_TEXT"));
5466
return;
5567
}
5668

5769
Logger.Warn($"[VisualTest] Timed out waiting for capture signal for {surfaceName}.");
5870
}
5971

60-
private static async Task CaptureToDirectoryAsync(FrameworkElement root, string surfaceDir)
72+
private static async Task CaptureToDirectoryAsync(
73+
FrameworkElement root,
74+
string surfaceDir,
75+
string? automationIdPrefix = null,
76+
string? exactText = null)
6177
{
6278
try
6379
{
6480
Directory.CreateDirectory(surfaceDir);
6581
if (root.DispatcherQueue.HasThreadAccess)
6682
{
67-
await CaptureOnUiThreadAsync(root, surfaceDir);
83+
foreach (var captureRoot in ResolveCaptureRoots(
84+
root,
85+
automationIdPrefix,
86+
exactText))
87+
await CaptureOnUiThreadAsync(captureRoot, surfaceDir);
6888
return;
6989
}
7090

@@ -73,7 +93,11 @@ private static async Task CaptureToDirectoryAsync(FrameworkElement root, string
7393
{
7494
try
7595
{
76-
await CaptureOnUiThreadAsync(root, surfaceDir);
96+
foreach (var captureRoot in ResolveCaptureRoots(
97+
root,
98+
automationIdPrefix,
99+
exactText))
100+
await CaptureOnUiThreadAsync(captureRoot, surfaceDir);
77101
tcs.SetResult();
78102
}
79103
catch (Exception ex)
@@ -93,6 +117,96 @@ private static async Task CaptureToDirectoryAsync(FrameworkElement root, string
93117
}
94118
}
95119

120+
private static IReadOnlyList<FrameworkElement> ResolveCaptureRoots(
121+
FrameworkElement root,
122+
string? automationIdPrefix,
123+
string? exactText)
124+
{
125+
if (string.IsNullOrWhiteSpace(automationIdPrefix)
126+
&& string.IsNullOrWhiteSpace(exactText))
127+
return [root];
128+
129+
var matches = new List<FrameworkElement>();
130+
FindDescendants(root, automationIdPrefix, exactText, matches);
131+
if (matches.Count == 0)
132+
{
133+
throw new InvalidOperationException(
134+
"Could not find the requested visual elements for capture.");
135+
}
136+
137+
return matches;
138+
}
139+
140+
private static void FindDescendants(
141+
FrameworkElement root,
142+
string? automationIdPrefix,
143+
string? exactText,
144+
ICollection<FrameworkElement> matches)
145+
{
146+
if (!string.IsNullOrWhiteSpace(automationIdPrefix)
147+
&& AutomationProperties.GetAutomationId(root).StartsWith(
148+
automationIdPrefix,
149+
StringComparison.Ordinal))
150+
{
151+
matches.Add(root);
152+
}
153+
if (!string.IsNullOrWhiteSpace(exactText)
154+
&& MatchesText(root, exactText))
155+
{
156+
matches.Add(FindScrollViewer(root) ?? root);
157+
}
158+
159+
var childCount = VisualTreeHelper.GetChildrenCount(root);
160+
for (var index = 0; index < childCount; index++)
161+
{
162+
if (VisualTreeHelper.GetChild(root, index) is FrameworkElement child)
163+
FindDescendants(child, automationIdPrefix, exactText, matches);
164+
}
165+
}
166+
167+
private static FrameworkElement? FindScrollViewer(FrameworkElement element)
168+
{
169+
var current = VisualTreeHelper.GetParent(element);
170+
for (var depth = 0; current is not null && depth < 16; depth++)
171+
{
172+
if (current is ScrollViewer scrollViewer)
173+
return scrollViewer;
174+
current = VisualTreeHelper.GetParent(current);
175+
}
176+
177+
return null;
178+
}
179+
180+
private static bool MatchesText(FrameworkElement element, string exactText)
181+
{
182+
var text = ReadText(element)?.TrimEnd('\r', '\n');
183+
if (string.Equals(text, exactText, StringComparison.Ordinal))
184+
return true;
185+
186+
var peer = FrameworkElementAutomationPeer.FromElement(element)
187+
?? FrameworkElementAutomationPeer.CreatePeerForElement(element);
188+
return string.Equals(peer?.GetName(), exactText, StringComparison.Ordinal);
189+
}
190+
191+
private static string? ReadText(FrameworkElement element) => element switch
192+
{
193+
TextBlock textBlock => textBlock.Text,
194+
RichTextBlock richTextBlock => string.Concat(
195+
richTextBlock.Blocks
196+
.OfType<Paragraph>()
197+
.SelectMany(paragraph => paragraph.Inlines)
198+
.Select(ReadInline)),
199+
_ => null,
200+
};
201+
202+
private static string ReadInline(Inline inline) => inline switch
203+
{
204+
Run run => run.Text,
205+
Span span => string.Concat(span.Inlines.Select(ReadInline)),
206+
LineBreak => Environment.NewLine,
207+
_ => string.Empty,
208+
};
209+
96210
private static async Task CaptureOnUiThreadAsync(FrameworkElement root, string surfaceDir)
97211
{
98212
Action restoreBackground = () => { };

src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,41 @@ static ChatTimelineState CreateTimeline(string id)
425425
if (!string.Equals(id, DefaultThreadId, StringComparison.Ordinal))
426426
return timeline;
427427

428+
if (string.Equals(
429+
Environment.GetEnvironmentVariable("OPENCLAW_ACCESSIBILITY_TEST_CHAT_FIXTURE"),
430+
"history-collision",
431+
StringComparison.Ordinal))
432+
{
433+
timeline = ChatTimelineReducer.Apply(
434+
timeline,
435+
new ChatToolStartEvent(
436+
"Verified structured history call",
437+
"Exec",
438+
new JsonObject
439+
{
440+
["command"] = "verified structured id: history-tool-0",
441+
},
442+
ToolCallId: "history-tool-0",
443+
IdentityStrength: ChatToolIdentityStrength.Specific));
444+
timeline = ChatTimelineReducer.Apply(
445+
timeline,
446+
new ChatToolStartEvent(
447+
"Flattened history output",
448+
"Bash",
449+
new JsonObject
450+
{
451+
["command"] = "synthetic flattened id: history-tool-1",
452+
},
453+
ToolCallId: "history-tool-1",
454+
IdentityStrength: ChatToolIdentityStrength.Specific));
455+
timeline = ChatTimelineReducer.Apply(
456+
timeline,
457+
new ChatToolOutputEvent(
458+
"flattened output owned by history-tool-1",
459+
"history-tool-1"));
460+
return ChatTimelineReducer.Apply(timeline, new ChatTurnEndEvent());
461+
}
462+
428463
timeline = ChatTimelineReducer.Apply(
429464
timeline,
430465
new ChatToolStartEvent(

0 commit comments

Comments
 (0)