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:
+4
-18
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 →
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user