fix: first native launch since refactor crashed — 3 stacked faults, all closed; RoundClip 'known failure' root-caused and green

User launch (2026-09-01 19:07) NRE'd in MainViewModel ctor:
1. SceneGraph (TASK 31) was 'null!'-declared, assigned mid-ctor, but Scenes is touched
   ~120 lines earlier — field-initialized now.
2. LeftPanel extraction (c9fd1bd) moved StaticResource users (EyeButton/EyeIconStyle)
   into a UserControl while the styles stayed window-scope — invisible at parse time;
   moved to Themes/Controls.xaml (app scope, the existing rule). Full audit: these were
   the only two offenders (grep of Controls/*.xaml StaticResource keys vs app dictionary).
3. RoundClipInteractionTests — the second 'known failure' the map never explained: it
   was two stale-test layers (window.FindName across the new UserControl namescope +
   VisualTreeHelper.HitTest, which returned the IsHitTestVisible=False WebViewHostPanel
   overlay for EVERY point; UIElement.InputHitTest — the real input pipeline — shows the
   corner IS grabbable in both Traditional and Round). Test fixed, no product bug.
Verified: clean rebuild 0 warnings; app boots (log shows full MainWindow loaded; user
clicked + closed, zero new exceptions; real DB Webcam row = the genuine C920, untouched);
RoundClip + 6 RealApp classes pass natively per-class.
Docs: ai.md known-failure note → 246/247 (audio only); TASK 31 verification paragraph
corrected ('cannot run headless' overstated — per-class Windows-host vstest runs them);
MyMistakes: InputHitTest-vs-VTH recipe + namescope/app-style + shared-log facts.
Spin-guard citations: WPF Visual Tree Overview (InputHitTest vs VisualTreeHelper hit
semantics) + XAML namescope docs, learn.microsoft.com.
This commit is contained in:
2026-09-01 19:33:31 -07:00
parent 711ba54af7
commit 6799c40175
7 changed files with 100 additions and 30 deletions
+4 -18
View File
@@ -45,24 +45,10 @@
</DrawingImage.Drawing> </DrawingImage.Drawing>
</DrawingImage> </DrawingImage>
<!-- Eye icon: open when visible, slashed when hidden (source rows) --> <!-- EyeButton/EyeIconStyle MOVED to Themes/Controls.xaml (2026-09-01): Controls/
<Style x:Key="EyeIconStyle" TargetType="Path"> LeftPanel.xaml consumes them via StaticResource, and window-scope resources
<Setter Property="Data" Value="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/> are invisible to a UserControl at parse time — the "known test failure" that
<Style.Triggers> was actually an app-crash regression from the LeftPanel extraction. -->
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="Data" Value="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="EyeButton" TargetType="Button" BasedOn="{StaticResource IconButton}">
<Setter Property="ToolTip" Value="Hide Layer"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="ToolTip" Value="Show Layer"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary> </ResourceDictionary>
</Window.Resources> </Window.Resources>
+35
View File
@@ -89,3 +89,38 @@ paths to `\\wsl.localhost\Debian\…` and throws "not a valid application for th
OS" when handed a Linux ELF ffmpeg. So never plan to have dotnet exec a Linux OS" when handed a Linux ELF ffmpeg. So never plan to have dotnet exec a Linux
ffmpeg here; verify the contract with shell/python instead, and leave the ffmpeg here; verify the contract with shell/python instead, and leave the
CLR→real-ffmpeg run to the native Windows suite. CLR→real-ffmpeg run to the native Windows suite.
## WPF hit-test truth in tests: `UIElement.InputHitTest`, NOT `VisualTreeHelper.HitTest`
(2026-09-01, the RoundClip "known failure" post-mortem — a failure the map carried as
"not a regression" for weeks without ever recording WHY.)
**The trap:** `VisualTreeHelper.HitTest(window, pt)` returned the window's
`WebViewHostPanel` overlay (`IsHitTestVisible="False"`, `Opacity=0`, ZERO children) for
EVERY point in the window — so a "corner is grabbable" assertion could never pass, and
it looked like a real interaction bug. The actual input pipeline (`UIElement.InputHitTest`,
what Mouse routing uses) correctly returned the element's Grid at elem-center/corner-in/
corner-exact and fell through to CanvasGrid just past the corner. The product was fine;
the TEST was probing an API that doesn't model input semantics.
**Rule:** any test asserting "where does a click land" uses `window.InputHitTest(pt)` +
`IsDescendantOf` — never `VisualTreeHelper.HitTest`.
**Diagnosis recipe (how the lie was caught in ~3 probe cycles, no guessing):** add a TEMP
probe `[Fact]` in the RealApp collection that hit-tests a spread of points
(elem-center / corner-in / corner-exact / corner-out / bg-center) and `Assert.Fail`s with a
composed dump: per-point VTH hit + `InputHitTest` hit + ancestor chain (`GetParent` walk
with `#Name`) + panel properties (`IsHitTestVisible/Opacity/children/actual size`) +
`TranslatePoint` origins. Run the class alone, read the message, delete the probe.
**Two sibling facts learned the same session (record-once):**
1. A UserControl owns its own XAML namescope — after extracting a region out of a window,
`window.FindName("InnerPart")` returns null; resolve the UserControl by its window-level
name, then `pane.FindName("InnerPart")`. And window-scope STYLES are invisible to a
UserControl's `StaticResource` at parse time — move such styles to `Themes/Controls.xaml`
(the app-scope rule exists for this).
2. Per-class `dotnet.exe vstest` from WSL DOES execute the RealApp/`MainWindow` tests fine
(they passed natively 2026-09-01) — only the FULL suite hangs (WASAPI startup). And the
test process shares `%APPDATA%\ytLlive\startup.log` with the real app: lines like
`camera 'test-camera' failed` are test noise, not DB state — to check pollution, query
the DB directly (`python3 sqlite3`, `SELECT DeviceId FROM Webcam`), not the log.
+10 -2
View File
@@ -1311,8 +1311,16 @@ What landed — core optimization + SceneGraph component, all in one commit:
**Verification:** SceneCompositorTests 4, StretchMathTests 4, BackgroundTests 16, SceneCatalogTests 18, **Verification:** SceneCompositorTests 4, StretchMathTests 4, BackgroundTests 16, SceneCatalogTests 18,
LayoutStorePersistenceTests 12, FramePumpTests 9, SceneGraphTests 1 all green. RealAppHost GUI/collection LayoutStorePersistenceTests 12, FramePumpTests 9, SceneGraphTests 1 all green. RealAppHost GUI/collection
tests (SourceNaming, RoundClip, BackgroundHeal, ...) construct a real `MainWindow` and cannot run in a tests (SourceNaming, RoundClip, BackgroundHeal, ...) construct a real `MainWindow` — **CORRECTED
headless WSL session (pre-existing limitation, not caused by this change). 2026-09-01:** they DO run from WSL when invoked per-class through the Windows `dotnet.exe` vstest host
(the old "cannot run headless" claim conflated them with the full-suite WASAPI hang). Two first-launch
crashes this refactor shipped with were caught on the first real native launch and fixed same day:
ctor-order `SceneGraph` NRE (`null!` field assigned after first ctor use — now field-initialized) and
window-scope `EyeButton`/`EyeIconStyle` consumed via `StaticResource` from the extracted `LeftPanel`
(UserControl namescopes can't see window resources — styles moved to `Themes/Controls.xaml`, the
app-scope rule honored). The RoundClip "known failure" was then root-caused to stale test code
(`window.FindName` across namescopes + `VisualTreeHelper.HitTest` where `UIElement.InputHitTest`
models input) — test green 2026-09-01, the sole remaining known failure is the audio one.
### Design ### Design
+23
View File
@@ -66,6 +66,29 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<!-- Eye icon: open when visible, slashed when hidden (source rows). App scope for
the same LeftPanel reason as EyeButton below. -->
<Style x:Key="EyeIconStyle" TargetType="Path">
<Setter Property="Data" Value="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="Data" Value="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
</DataTrigger>
</Style.Triggers>
</Style>
<!-- Lives here (app scope), NOT in a window: Controls/LeftPanel.xaml references it
via StaticResource, and a UserControl cannot see window resources at parse
time (startup crash caught 2026-09-01). -->
<Style x:Key="EyeButton" TargetType="Button" BasedOn="{StaticResource IconButton}">
<Setter Property="ToolTip" Value="Hide Layer"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="ToolTip" Value="Show Layer"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="YtTextBox" TargetType="TextBox"> <Style x:Key="YtTextBox" TargetType="TextBox">
<Setter Property="Background" Value="#0f3460"/> <Setter Property="Background" Value="#0f3460"/>
<Setter Property="Foreground" Value="#e0e0e0"/> <Setter Property="Foreground" Value="#e0e0e0"/>
+5 -2
View File
@@ -41,7 +41,11 @@ public partial class MainViewModel : ViewModelBase
// Scene graph (TASK 31): owns the scene collection + element inventory and the // Scene graph (TASK 31): owns the scene collection + element inventory and the
// baked-crust compositor cache. The ViewModel exposes Scenes/StagedScene/LiveScene // baked-crust compositor cache. The ViewModel exposes Scenes/StagedScene/LiveScene
// on top of it; the scene collection itself lives here. // on top of it; the scene collection itself lives here.
private SceneGraph _sceneGraph = null!; // Field-initialized, NOT ctor-assigned: the ctor touches Scenes (CollectionChanged
// wiring) before its later statements run — first-ctor-line NRE if the graph isn't
// born yet (startup crash regression from TASK 31, 2026-09-01; headless RealApp
// tests never ran it, so it waited for the first native launch).
private SceneGraph _sceneGraph = new();
private readonly TransitionService _transition = new(); private readonly TransitionService _transition = new();
private WebView2Manager? _webView2Manager; private WebView2Manager? _webView2Manager;
@@ -294,7 +298,6 @@ public partial class MainViewModel : ViewModelBase
_chatLayer = new ChatOverlayLayer(new ChatBoxRenderer()); _chatLayer = new ChatOverlayLayer(new ChatBoxRenderer());
_sceneGraph = new SceneGraph();
_rtmpUrlProvider = () => _reusableStreamUrl; // TASK 5: the reusable stream's ingest URL _rtmpUrlProvider = () => _reusableStreamUrl; // TASK 5: the reusable stream's ingest URL
_framePump = new FramePump( _framePump = new FramePump(
sceneProvider: () => StagedScene, sceneProvider: () => StagedScene,
+8 -3
View File
@@ -126,9 +126,14 @@ the Socials fediverse-heal roundtrip, AboutHubTests, NotificationAreaIntegration
GlobalHotkeyTests + HotkeyConfigTests (TASK 20), WebcamMenuGateTests (TASK 26), ChatLayerGateTests GlobalHotkeyTests + HotkeyConfigTests (TASK 20), WebcamMenuGateTests (TASK 26), ChatLayerGateTests
(TASK 27), BroadcastPullOutTests (TASK 29), DefaultRecordFolder fallback (TASK 30), WebView2ManagerTests (TASK 27), BroadcastPullOutTests (TASK 29), DefaultRecordFolder fallback (TASK 30), WebView2ManagerTests
(TASK 17), RecordingFileTests + OnAirSignTests (TASK 18) — (TASK 17), RecordingFileTests + OnAirSignTests (TASK 18) —
**247 total (245 pass — the 2 pre-existing known failures **247 total (246 pass — the ONE remaining known failure is
`AudioPipelineTests.Mix_HonorsProviderGains…` + `RoundClipInteractionTests.Round_Clip_Corner_Is_Grabbable…` `AudioPipelineTests.Mix_HonorsProviderGains…` (the creator-declared known, suspected WASAPI
are NOT regressions; see HANDOFF.md. Run test classes individually — the full-suite vstest run hangs.)** channel declaration/init). The former "known" `RoundClipInteractionTests.Round_Clip_Corner_Is_Grabbable…`
was root-caused and FIXED 2026-09-01: two stale-test layers from the component refactor (namescoped
`FindName` + `VisualTreeHelper.HitTest` used where `UIElement.InputHitTest` models input — see
MyMistakes recipe), no product bug. Full-suite vstest still hangs headless — but per-class runs
THROUGH THE WINDOWS dotnet.exe HOST DO EXECUTE the real-`MainWindow`/RealApp tests fine (verified
same day; the old "GUI suites can't run from here" was an overstatement.)**
Reward-event capture (monetization awareness, see the Monetization section) will add its integration Reward-event capture (monetization awareness, see the Monetization section) will add its integration
tests here when it ships: one real chat-poll payload containing all seven reward event types → tests here when it ships: one real chat-poll payload containing all seven reward event types →
+15 -5
View File
@@ -34,6 +34,7 @@ public sealed class RoundClipInteractionTests
_app.Run(Run); _app.Run(Run);
} }
private void Run() private void Run()
{ {
// Never let the real MainWindow read/write the user's actual layout DB — // Never let the real MainWindow read/write the user's actual layout DB —
@@ -62,8 +63,12 @@ public sealed class RoundClipInteractionTests
vm.SelectedElement = webcam; vm.SelectedElement = webcam;
window.UpdateLayout(); window.UpdateLayout();
var previewGrid = (Grid)window.FindName("PreviewGrid")!; // PreviewGrid/CanvasGrid moved into Controls/PreviewPane.xaml during the
var canvasGrid = (Grid)window.FindName("CanvasGrid")!; // component refactor — a UserControl owns its own namescope, so window.FindName
// returns null for them. Resolve through the pane (found by its window-level name).
var pane = (System.Windows.Controls.UserControl)window.FindName("PreviewPane")!;
var previewGrid = (Grid)pane.FindName("PreviewGrid")!;
var canvasGrid = (Grid)pane.FindName("CanvasGrid")!;
var toWindow = canvasGrid.TransformToVisual(window); var toWindow = canvasGrid.TransformToVisual(window);
var corner = new Point(webcam.X + webcam.Width, webcam.Y + webcam.Height); var corner = new Point(webcam.X + webcam.Width, webcam.Y + webcam.Height);
@@ -78,11 +83,16 @@ public sealed class RoundClipInteractionTests
// A click on the corner must land inside the preview (descendant // A click on the corner must land inside the preview (descendant
// of PreviewGrid); otherwise Window_PreviewMouseLeftButtonDown // of PreviewGrid); otherwise Window_PreviewMouseLeftButtonDown
// deselects the source and the handle can never be grabbed. // deselects the source and the handle can never be grabbed.
var hit = VisualTreeHelper.HitTest(window, cornerInWindow); // UIElement.InputHitTest IS the input pipeline — what Mouse uses.
var grabbable = hit != null && IsDescendantOf(hit.VisualHit, previewGrid); // Do NOT use VisualTreeHelper.HitTest here: it returned the window's
// non-hit-testable WebViewHostPanel overlay for EVERY point (it does
// not model IsHitTestVisible input semantics) — the bug this test
// "found" for months was this wrong API (root-caused 2026-09-01).
var hit = window.InputHitTest(cornerInWindow) as DependencyObject;
var grabbable = hit != null && IsDescendantOf(hit, previewGrid);
Assert.True(grabbable, Assert.True(grabbable,
$"{shape}: corner click fell through to '{hit?.VisualHit.GetType().Name ?? "null"}' " + $"{shape}: corner click fell through to '{hit?.GetType().Name ?? "null"}' " +
"so the source gets deselected before the resize handler runs"); "so the source gets deselected before the resize handler runs");
} }