Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ non-releasing PR leaves that version alone.
- [ ] CHANGELOG entry added, opening with a one-line plain-English lead under the version heading
before the first `###` category (CI checks the lead separately, because the release notes are
copied from it verbatim)
- [ ] CHANGELOG heading dated **today in UTC** — and re-dated if the merge slips to another UTC day.
This is the only gate that runs *after* the squash merge, when the branch is already gone: the
tag push that triggers the release requires the date to be today, so a stale date fails the
release rather than the PR. It has published yesterday's date twice.
- [ ] `Version` / `FileVersion` / `AssemblyVersion` in `SysManager/SysManager/SysManager.csproj`
bumped one step from the newest release tag and equal to the new CHANGELOG heading
(`fix:` = patch, `feat:` = minor)
Expand Down
3 changes: 1 addition & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,7 @@ Key services:
output device). Feature-detected (`TryCreate` returns null when it can't bind), guarded (the
SET call is invoked only after a successful `QueryInterface` for the exact IID and any failure
returns false), and its endpoint-id/process-token string helpers are pure + unit-tested. The
routing SET path can only be runtime-verified on a real desktop (laptop workstation), not the
build box. The READ path (`GetPersistedDefaultEndpoint`) is deliberately unimplemented and returns
routing SET path can only be runtime-verified by running the app on real audio hardware. The READ path (`GetPersistedDefaultEndpoint`) is deliberately unimplemented and returns
null, so `IAudioMixerService.GetSessionOutputDevice` has a three-state contract — an endpoint id,
`string.Empty` for "read succeeded, no override", or null for "could not read" — and the row VM
renders null as unknown rather than as the default device.
Expand Down
66 changes: 46 additions & 20 deletions SysManager/SysManager.Tests/ArchitectureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,12 @@ public void EveryViewModelCommand_IsReachableFromTheUi()
/// never went through the correction at all — four typed hex values could produce white on white while
/// every shipped preset was held to a contrast floor. It also silently discarded the shade slider's
/// position.
/// <para>Asserted on the source rather than by calling the method, because <c>SetCustom</c> ends in
/// <c>Save()</c>, which writes the user's real theme file.</para>
/// <para>Asserted on the source, and the reason is coverage rather than safety. It used to say that
/// <c>SetCustom</c> could not be called because it ends in <c>Save()</c> and would write the user's real
/// theme file — true before #1741 made <c>SettingsPath</c> redirectable, and false since:
/// <c>ThemeServiceTests</c> now calls <c>SetCustom</c> for real against a temp directory. What a source
/// check still buys is the entry point nobody wrote a test for. A behaviour test covers the three methods
/// named below; this covers a fourth added later, before anyone thinks to test it.</para>
/// </remarks>
[Fact]
public void EveryThemeEntryPoint_GoesThroughTheLegibilityCorrection()
Expand All @@ -425,11 +429,15 @@ public void EveryThemeEntryPoint_GoesThroughTheLegibilityCorrection()
var at = service.IndexOf($"public void {entry}(", StringComparison.Ordinal);
Assert.True(at > 0, $"ThemeService.{entry} was renamed — update this guard, don't drop it.");

// To the next method declaration, so the slice is this method and nothing after it.
var end = service.IndexOf("\n public ", at + 1, StringComparison.Ordinal);
if (end < 0) end = service.IndexOf("\n private ", at + 1, StringComparison.Ordinal);
Assert.True(end > at, $"could not bound {entry}'s body.");
var body = service[at..end];
// Brace-matched, not "up to the next member declaration". That heuristic looked for the next
// `public` and only fell back to `private` when there was none — so for a method followed by
// private members it skipped past them to a later public one. SetCustom's slice measured 6727
// characters against a real body of 675, swallowing IsDarkBackground, ApplyShade, Shade and
// Legible. Nothing in that region calls ApplyShade() today, so the assertion below was still
// being satisfied by SetCustom's own call — but one added call anywhere in those six kilobytes
// and this guard could no longer tell whether SetCustom still does it.
var body = BalancedBlock(service, $"public void {entry}(");
Assert.True(body.Length > 0, $"could not bound {entry}'s body.");

Assert.Contains("ApplyShade();", body, StringComparison.Ordinal);
Assert.DoesNotContain("CurrentTheme = new ThemePreset", body, StringComparison.Ordinal);
Expand Down Expand Up @@ -610,11 +618,15 @@ public void EveryGoldElevationBanner_PromisesMoreAccess()
/// The two elevation banners that share a slot must share one geometry.
/// </summary>
/// <remarks>
/// 27 views render both banners in the same <c>Grid.Row</c>, swapped on <c>IsElevated</c>, and the two
/// were hand-written with different geometry: <c>CornerRadius="8" Padding="14,12"</c> when not elevated
/// in 27 of 27 views, <c>CornerRadius="12" Padding="12,8"</c> when elevated in 29 of 29. Measured, the
/// banner in that fixed slot was 61.29px in one state and 35.29px in the other, so everything below it
/// jumped 26px at the moment the user granted elevation — and the corners visibly changed shape with it.
/// 31 views render both banners in the same <c>Grid.Row</c>, swapped on <c>IsElevated</c> — 62 banners,
/// one pair per view — and the two states were hand-written with different geometry:
/// <c>CornerRadius="8" Padding="14,12"</c> when not elevated against <c>CornerRadius="12" Padding="12,8"</c>
/// when elevated. Measured, the banner in that fixed slot was 61.29px in one state and 35.29px in the
/// other, so everything below it jumped 26px at the moment the user granted elevation — and the corners
/// visibly changed shape with it. Both states are on <c>CornerRadius="12" Padding="12,8"</c> now.
/// <para>The counts above are what this guard parses today, re-derived. It previously said "27 of 27" and
/// "29 of 29", which were the totals at the time it was written and disagreed with each other and with
/// the tree; a reader could not reconcile them against a failure message quoting the same numbers.</para>
/// <para>18px of that jump is the "Run as administrator" button, which the elevated state has nothing to
/// replace with, and no geometry removes it. This asserts the part that was an accident: one radius, one
/// padding, both states.</para>
Expand Down Expand Up @@ -664,9 +676,10 @@ public void BothAdminBanners_ShareOneGeometry()
.ToArray();

Assert.True(geometries.Length == 1,
"the elevation banners do not agree on their geometry. Both states occupy the SAME slot in 27 "
+ "views, so a difference here is the layout below them jumping the moment the user elevates — "
+ "which is the one moment the app should look steady. Pick one radius and one padding:\n "
$"the elevation banners do not agree on their geometry. Both states occupy the SAME slot in the "
+ $"{seen.Select(b => b.View).Distinct(StringComparer.Ordinal).Count()} views parsed here, so a "
+ "difference is the layout below them jumping the moment the user elevates — which is the one "
+ "moment the app should look steady. Pick one radius and one padding:\n "
+ string.Join("\n ", geometries));
}

Expand Down Expand Up @@ -1595,6 +1608,16 @@ public void ThePullRequestChecklist_AsksForWhatCiEnforces()
"dotnet format",
File.ReadAllText(Path.Combine(root, "CONTRIBUTING.md")),
StringComparison.Ordinal);

// The one gate that is NOT in ci.yml, which is why the checklist omitted it and why this guard could
// not see the omission: auto-release requires the newest CHANGELOG heading to be dated today in UTC,
// and it runs AFTER the squash merge, when the branch is gone. A stale date fails the release rather
// than the pull request, and it has published yesterday's date twice.
var autoRelease = File.ReadAllText(
Path.Combine(root, ".github", "workflows", "auto-release.yml"));
Assert.Contains("TODAY=$(date -u +%Y-%m-%d)", autoRelease, StringComparison.Ordinal);
Assert.Contains(items, item => item.Line > releaseSection
&& item.Text.Contains("UTC", StringComparison.Ordinal));
}

/// <summary>
Expand Down Expand Up @@ -4132,11 +4155,14 @@ public void EveryTabsSidebarLabel_MatchesItsPageHeader()
/// entries that render a PREVIEW pill beside the text.
/// </summary>
/// <remarks>
/// The sidebar is a fixed 220px column (MainWindow.xaml). After the row's 28px left padding, 14px
/// right padding, the 13px glyph and its 10px margin, the label has roughly 155px — about 24
/// characters at FontSize 13. The longest label that ships, "Profile Export / Import", is 23. So this
/// budget says "no longer than what is already there", NOT "proven to fit": whether that one already
/// ellipsizes cannot be settled without running the app, which happens on the other workstation.
/// The sidebar is a fixed 220px column (MainWindow.xaml). A leaf row's <c>Padding="28,9,14,9"</c> leaves
/// the label 178px — roughly 27 characters at FontSize 13. The longest label that ships, "Profile Export /
/// Import", is 23. So this budget says "no longer than what is already there", NOT "proven to fit":
/// whether that one already ellipsizes cannot be settled without running the app.
/// <para>The derivation used to subtract "the 13px glyph and its 10px margin" as well, leaving 155px.
/// Leaf rows carry no glyph — <c>NavItem</c> has no such member, and the only two glyph bindings left in
/// MainWindow.xaml are the group header's own and the single-item row reading its group's — so the budget
/// was charging every leaf 23px it does not spend.</para>
/// <para>Two budgets, because one number cannot express the constraint. A PREVIEW pill takes fixed
/// width out of the same column, so a pilled row has less room for text — and the pill is exactly
/// where this went wrong before: a horizontal StackPanel measured with infinite width pushed it past
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager.Tests/AudioPolicyConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace SysManager.Tests;
/// <summary>
/// Tests for the pure, easy-to-get-wrong string helpers in <see cref="AudioPolicyConfigFactory"/> —
/// the endpoint-id wrapping and the process token. The COM activation path itself is undocumented
/// and can only be exercised on a real Windows desktop (verified on the laptop workstation), so it
/// and can only be exercised by running the app on a real Windows desktop, so it
/// is intentionally not unit-tested here; these pin the formatting the routing SET call depends on.
/// </summary>
public class AudioPolicyConfigTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,8 @@ private static void AssertSetter(XElement owner, string property, string value)
/// clearly built for the keyboard — but opening the panel left focus behind on the chip, so none of
/// it could be reached and there was no way out but the mouse. <c>Key.Escape</c> appeared nowhere in
/// the app.
/// <para>Asserted rather than demonstrated: the behaviour needs the app running, which happens on the
/// other workstation. What is pinned here is the wiring that makes it possible, on both sides — the
/// <para>Asserted rather than demonstrated: the behaviour needs the app running, which a source-level
/// test cannot do. What is pinned here is the wiring that makes it possible, on both sides — the
/// XAML hooks and the handlers they name. Half of it is useless alone: an <c>Opened</c> attribute
/// with a handler that does not move focus reads as fixed and is not.</para>
/// </remarks>
Expand Down