Repository navigation
2.12.0 — a compact readout over the game, a frame counter that reports a number, and Ukrainian - #21
Merged
Merged
Conversation
The Monitor page is only useful while you are looking at it, and while you are playing you are not. This adds a small window with the same readings that stays above other windows, summoned with Ctrl+Shift+M from anywhere. It is not an overlay and will not become one. Nothing is injected into the game, nothing hooks its swap chain, no driver is loaded - it is an ordinary top-level window with its chrome removed. So it draws over a borderless windowed game and it does not draw over one in exclusive fullscreen. That limit is real, it is stated in the README, in the changelog and in the settings page, and it is the price of never tripping an anti-cheat. Three pieces of logic went into Core rather than into the window, because that is the only place a test can reach them: HotkeySpec parses and formats "Ctrl+Shift+M" in any order, spacing or case, refuses a modifier-free key - which would fire while the user is typing anywhere on the machine - and always hands back a working default rather than leaving the app with no hotkey at all. MonitorReadout turns a selection plus a tick of values into the rows to draw. A reading the machine cannot supply is left out rather than drawn empty; a frame reading with the counter running but nothing rendering shows "--", because the row's presence is what says the counter is watching. The settings panel's preview now draws from the same code, so what it promises is what the window shows. WindowPlacement decides where a remembered position should actually open. Drag the window onto a second monitor, unplug the monitor, and the saved position names a place no screen covers: the window opens with no title bar to alt-drag and no taskbar button to right-click. Two of these tests failed on the first implementation and both were bugs in the arithmetic. The window and its view model are transient, not singletons. A singleton would keep a timer and a sensor sample alive for the life of the app with the window closed, which is exactly the background work this project exists to remove from other people's machines. A refused hotkey is reported rather than swallowed. The failure is a code, not an English sentence, so the settings page can say it in the user's language: "Ctrl+Shift+M is already taken by another program" is something the user can act on, and a key that silently does nothing is not.
…rite-j1tyfw # Conflicts: # CHANGELOG.md # src/SysTuneX.App/Resources/Strings.resx # src/SysTuneX.App/Resources/Strings.ru.resx
Reported from a real machine: the FPS counter does not work. It was two clocks being compared against each other. Frame timestamps come from the trace - whatever epoch ETW hands us. The window then aged those timestamps against DateTime.Now, the local wall clock. Where the two do not line up, every frame in the window looks hours old on the first read, the window empties itself, and the counter reports nothing. Forever, with no error anywhere: an empty window is a perfectly ordinary state, so there was nothing to log and nothing to show. Every test passed the same value for both roles, which is exactly why nothing caught it. FrameTimeWindow now keeps two timestamps per frame and says in the type why: the presentation time, only ever used as a difference against another presentation time, and the arrival time on this process's own monotonic clock, which is the only value compared against "now". Environment.TickCount64 rather than a wall clock, so a time zone change or an NTP correction cannot empty the window either. Three tests, all of which fail against the old behaviour: one in the direction that actually broke, one keeping the number from going stale after a game closes, and one pinning that the rate is measured from presentation times - ETW delivers events in bursts, and measuring the intervals between arrivals would report an absurd rate and a meaningless 1% low. Two things the same report exposed, both of which would have made it answerable in one round instead of several. Direct3D 9 games were never counted. A D3D9 title does not call IDXGISwapChain::Present, so with the DXGI provider alone the counter sat at nothing for every game of that era and gave no hint why. The session now listens to Microsoft-Windows-D3D9 as well, and checks the provider as well as the event id - 42 means something different in every provider. "No frame rate" said one thing for four different situations. The probe now counts Present events seen and Present events attributed to the watched process, so the page can tell them apart: the session would not start; the session is up and nothing on this PC presents through DirectX at all, which is what a Vulkan or OpenGL game looks like; frames are being presented but by some other window; or this game was being counted and has stopped, which is what alt-tabbing out of an exclusive fullscreen game does.
Three new tweaks, and one deliberately left out. PCI Express link state power management, off. ASPM parks a PCIe link between transfers and wakes it on the next one; waking takes microseconds, which matters only when it lands between a mouse report and the frame that should have used it, on the link carrying the drive a game streams from or the network card a match is on. It costs power, and the description says so rather than selling it as free. A machine whose firmware does not expose the setting now reports Unknown instead of offering a switch that can do nothing. The accessibility key shortcuts, off. Five taps of Shift opens the Sticky Keys dialogue, which in a shooter is crouch, crouch, crouch and a modal window over the game. Only the shortcut flag is cleared, so the features themselves stay available to anyone who uses them. The data collection scheduled tasks, off. Seven of them: the Compatibility Appraiser, which walks every installed program and can hold a core busy for minutes, and six smaller Customer Experience Improvement Program tasks. Only the ones actually running are switched off, and only those are switched back on - a task the user disabled themselves has no journal entry and stays as they left it, which is the rule the service tweaks already follow. No audio latency tweak, though every optimizer has one. On stock Windows 11 the MMCSS Audio task is already at Priority 6 and Pro Audio already at Scheduling Category High - the values those guides tell you to write. The one value not already set is the Audio task's category, and raising it puts the audio engine in the same scheduling band as the game. That is a trade, not a win. There is a comment in the catalogue saying so, so the next person to read a guide does not add it. Two things the new tweaks needed, which were worth fixing anyway: Reading a power setting meant scanning powercfg's output for a line with "index" in it, in English or in Russian. That had never been run against either, because reaching it needed powercfg on a Windows machine - and a scan that matches nothing does not fail, it returns zero, which for most of these settings means "off". It would report a machine as tuned having read nothing at all. The parsing is now a function with twelve tests over real powercfg output in both languages, and a setting the machine does not expose reads as nothing rather than as zero. Core parking, which had this code inline, is now one caller of it. Scheduled tasks are read through PowerShell rather than schtasks, because schtasks prints the state as a translated word: "Ready" is "Готово" on a Russian install, so the parse would match nothing and report every task as already disabled - claiming work it never did. Get-ScheduledTask returns a .NET enum, which prints the same everywhere.
Four changes, one of which is a defect rather than a preference. The metric style carried a comment saying it used tabular figures - every digit the same width - and a setter that did nothing of the kind. So a counter going 99 to 100 and back shoved the caption beside it about, twice a second, on the two screens where that is worst: the monitor page and the small window that sits over a game. It had done that since the page was written, because nothing about a comment is enforced. Typography.Numeral Alignment is now actually set. Every page opens with the brand accent under its title. The palette has had a gradient in it and nowhere that used it, so every screen started as grey text on a grey background and looked like a settings dialogue. Four pixels of colour, and unlike a drop shadow it costs nothing to draw - which matters in a tool whose argument is that it does not waste the machine. One type scale. Pages reached past the shared styles with hard-coded sizes, 13 here and 26 there, so two things that meant the same thing were set differently on different pages and nothing said which was right. The sizes that were off the scale are on it, and the two that had no style now have one: DisplayValue for the one number a screen is about, AccentCaption for the line on a card that is a verdict. And a test that reads the markup. A StaticResource naming a key that does not exist is not a build error: it throws when the page is parsed, which is when the user opens it, and takes the page down - this project has already shipped exactly that once, as a pack URI to an image that was never compiled in. Every resource key every page asks for is now resolved by a test that needs neither Windows nor WPF, alongside a check that no page quietly loses the accent rule and that every XAML file is well-formed. Both sabotage checks fail the right tests.
The first two items from the SOLID audit, and they are the two that unblock everything else. IProcessRunner. Everything SysTuneX cannot do through an API it does by running powercfg, netsh, bcdedit or PowerShell and reading what they print. The reading is the part that goes wrong - those tools print in the user's language and their output shape is not a contract - and the runner was static, so none of it could be reached from a test. A comment in PowerService said exactly that. Two real defects came out of that blind spot this week alone, both found by reading rather than testing. Twelve tests now cover what powercfg prints and what we make of it, in English and in Russian, including the claim the scheme parser has carried since it was written: that the labels are translated, so only the GUID and the parenthesised name can be relied on. Never once run against a Russian machine's output before now. It is correct. The power setting reader made the same claim and was not. One data directory. Half the services took an optional dataDirectory and fell back to a static path when it was not given, and no test ever passed it - so every test of the profile, snapshot, game mode and game watcher services read and wrote the real %ProgramData%\SysTuneX on whatever machine ran them, leaving state behind between runs and reading state the developer's own installation had put there. The parameter is gone; the path comes from IEnvironmentService. AppPaths keeps only what the file logger needs before the container exists, and says so. Two things the audit was right about that fell out of the same change: Rebooting the machine left MainWindowViewModel. It was a one-line call to the static runner from the class that describes a window - the most consequential thing this application does, in the layer furthest from anything that could test it. It is IEnvironmentService's business now, beside RestartElevated and RestartExplorerAsync, and it reports a refusal rather than failing silently. Opening a folder left the view models too. Four commands called Process.Start directly; they go through IShellLauncher, which additionally refuses to hand Windows anything that is not plainly an http address - UseShellExecute interprets whatever it is given, and every URL in the app being a constant in our own source is the kind of thing that stops being true one edit later. Four architecture tests hold the line: nothing but the logger reads a static data directory, nothing calls the process runner statically, no view model starts a process, and a guard on each scan so a broken regex cannot make the rules pass by finding nothing.
"Restore All" was a chain of if (active.Any(e => e.Kind == X)) blocks inside ProfileService, one per kind of change. A new kind that nobody remembered to add to that chain would be recorded faithfully, listed in the change journal, and then quietly skipped by the button whose entire promise is that it puts everything back - with nothing anywhere saying so. That is the worst shape a bug can take in this application. Restorers are resolved as a set now and each claims the entries it is responsible for, in a declared order: a registry value written by a tweak goes back through that tweak's own revert, which for a handler-backed tweak is more than one write and may not be a registry write at all. The extensibility is not the fix. The fix is the last step: whatever no restorer claimed comes back in the report as an error, so the user is told the machine is not fully restored rather than told it is. And a test asserts that every BackupKind has a restorer - adding one without a restorer now fails at the moment it is added, not on a user's machine at the moment they press the button. Two things fell out of it: Materialize - turning a journal entry's recorded text back into a registry value of the right type - was a private method inside TweakEngine. The restorer that puts back a value nothing else owns had no way to reach it and would have grown a second copy. It lives in RegistryValueComparer now, next to Stringify, which is the function it is the inverse of. The applied-profile record is its own store. ProfileService had eleven dependencies and was also doing JSON persistence; its tests passed null! for six of those eleven just to reach this. AppliedProfileStore is four methods and testable on its own. Also fixed while here: FakeEnvironment defaulted its data directory to the shared %TEMP% root, so two tests that each built one wrote the same gamemode.json and one read what the other left. It gets a fresh directory per instance, and the test that deliberately wants two services to share a machine - a crash and a relaunch - now says so instead of relying on the collision.
Every one of these was a single interface that two unrelated callers took because there was only one to take, and the proof was the test fakes: FakeBackupService left six of eleven members empty, FakePowerService about half. IBackupService is a writer and a reader. Services that change the machine record and retire entries; the history page and the diagnostics report read what was changed and have no business recording anything. A fake for the reader is three members. IPowerService is schemes and settings. The settings page and game mode want a scheme; a tweak wants one index inside whichever scheme happens to be active and does not care which. Core parking sits with the settings, because that is what it is - two indexes in the processor subgroup - and dressing it up as its own pair of methods is why ASPM, which is the same shape, needed the generic pair added separately later. IGameWatcher is a list and a detector. The settings page edits the list and never starts a poll; the automation watches for a game and never adds one. IProcessService is IMemoryTrimmer, one method. SetPriority, SetAffinity and GetTopProcessesByMemory had no caller anywhere in the application and were implemented, translated and maintained for nobody. So had IPowerService.SetHibernationAsync. They are in the history if a page ever wants them; carrying them as a promise nothing kept was the worse option. Each union interface survives for the implementation and the container, which hands one instance out behind all of them - and a test now fails if anything else takes the union back, because that would undo the split one constructor at a time. Three ProfileService dependencies went with the rollback that used them: the change journal, the privacy service and the network service. Its preview test passed null! for six of eleven parameters; it is three of ten now, and the three are the ones apply genuinely needs.
Four seams, each one a place where a class needed a running Windows desktop to exist at all. Post-apply refreshes. TweakEngine ran a chain of HasFlag branches, each calling a static P/Invoke helper, so the engine knew about every kind of refresh there is and none of it could be reached from a test - reaching it meant calling SystemParametersInfo on a live desktop. One IPostApplyEffect per flag now, resolved as a set, and six tests cover what used to be unobservable: that only the asked-for effects run, that reverting runs them the other way round, that one throwing does not lose the next or fail the tweak, and that every flag in the enum has an effect registered for it. The CPU temperature. GPU sensors have been behind a probe interface since the start and the composition around them is tested; the CPU side was a WMI query inline in SensorService, so SensorCompositionTests ran a real query against root\WMI on whatever machine it ran on - slow, dependent on that machine's firmware, and unable to pin the one thing it should: that a machine with no thermal zone reports no temperature rather than a zero the dashboard would draw as real. Application.Current. View models reached for the dispatcher, the main window and Shutdown directly. IUiDispatcher, IAppLifetime and IWindowAppearance replace them. The appearance one earns its place immediately: applying a backdrop is WPF-UI's business and fails in an interesting way - it rebuilds the window chrome, and replacing one that already carries an inheritance context throws from inside WPF - so that refusal is handled in one place instead of at each call site. MainWindow.GoTo reached into the static container for a page although the page provider is already a constructor parameter. It uses the parameter. Caught by an existing test while doing this: moving the backdrop logic out dropped the message that tells the user it will apply after a restart. The string-coverage check noticed the resource had stopped being used, which is the only reason it was not shipped as a button that silently does nothing. ApplyBackdrop returns whether it took effect, and the settings page says so.
Two catalogues that promised "adding an entry is one line" and charged four.
Global search had three sources written into it, and decided which page owns
a tweak category with a switch whose default arm was the gaming page. A
category added to the enum and not to that switch sent the user to a page
that could not contain what they searched for - silently, with nothing to
say so. Sources are ISearchSource now, and the category-to-page mapping is
declared next to the page itself where it is registered. A category with no
page is simply not findable, which is visibly wrong rather than quietly
wrong, and a test fails the moment one is added without a page.
The monitor page had thirteen near-identical Display* properties, their
thirteen names repeated in the list of things to notify when a tick moved,
and a thirteen-case switch formatting the preview - while the catalogue they
all come from says adding a reading is one entry plus two resource strings.
One indexer replaces the properties, one notification replaces the list, and
the preview is built by the same code as the compact window, so what the
preview promises is what that window shows. That switch was a second copy of
a formatter I had already moved into Core and then not used.
The rebinding found its own bug immediately: two of the thirteen were
renamed to names the enum does not have, which parses to nothing and hides
the tile forever with nothing anywhere saying why. There is now a check that
every {Binding [X]} names a real reading, and it fails on a deliberately
broken one.
SnapshotService held the comparison logic behind four constructor dependencies, so testing "did these two snapshots differ" meant building a service with four nulls. The comparison never needed any of them: it is two records in, a list of differences out. SnapshotComparer is that function, and the tests now call it directly. CleanupService cleaned directories and also enumerated and removed Store packages through PowerShell - two jobs with nothing in common but the page they appear on. IAppPackageService is the second one. Parsing what Get-AppxPackage prints is worth its own tested function because of a detail that is invisible while developing: ConvertTo-Json emits a bare object instead of an array when exactly one package matches. A parser that assumed an array works on every developer's machine, where dozens match, and returns nothing on a cleaned-up machine with one - the machine whose owner opened this page in the first place.
The two halves of that page answer different questions. The change log is
what SysTuneX did and can undo; snapshots are what this machine looks like,
which works just as well for changes SysTuneX had nothing to do with. They
shared a class only because they share a screen.
They still share the spinner, so IBusyScope is the whole of what the panel
needs from the page that hosts it - not the page itself, with its eight
dependencies.
Scoping part of a page to a child view model is the construct WPF is quietest
about: under DataContext="{Binding Snapshots}" a name that is still perfectly
real on the page's own view model resolves against nothing, renders nothing,
and says nothing. The existing binding check could not see it, because it asks
only whether a name exists somewhere in the source. The second check added
here resolves each binding outside a template against the view model that
element actually reads, following base classes - four pages take nearly all
their content from a shared one.
Verified by putting the mistake back: restoring the old property name inside
the new scope fails only the new check, while an ordinary typo fails both.
The last screen before a dozen registry writes was ninety lines of WPF built control by control inside the class that shows toasts. What it says and what it looks like were the same ninety lines, and neither could be checked except by applying a profile and reading the dialog - so the omission that would matter most, a change the list quietly leaves out, was the one nothing could see. The decisions are content, not layout, so they are a function in Core now: which entries appear, which are left out because the machine is already in that state, and what a tweak with no registry values says instead of showing an empty gap. Eleven tests cover it, including the one that reads the whole split: a tweak already applied is counted but not listed. One thing changed on the way through. A registry value the machine does not have arrives as null rather than as an empty string, so "not set" stays distinguishable from a value that is genuinely empty - flattening the two was free while this was all one string.Format call, and wrong.
translated Windows Fourteen dependencies and eight jobs in one class: hardware read once, counters every second, sensors every fifth tick, the game mode card from whichever thread the news arrived on, the tuning score, and four actions. They are three view models now - Hardware, Counters, GameMode - and a page that owns the tick, the score and the actions. Two pieces moved out of the application entirely, because neither was about a screen. Quick Optimize is the one button that changes the machine without asking a second time, and the rule that earns it that - safe tweaks only, never moderate, never advanced - was a .Where in the middle of a WPF command handler. Nothing could assert it there. One careless edit and a click labelled "optimise" disables virtualisation-based security. It is IQuickOptimizer now, with the rule under test. Moving the "is this a high-performance scheme" heuristic onto PowerScheme exposed a real defect. When Windows ships with neither built-in scheme SysTuneX duplicates one; powercfg gives the copy a fresh GUID and keeps the source scheme's name, which on a Russian or Ukrainian Windows is not the English string the check looked for. So on every non-English machine that needed the duplicate, the dashboard reported a tuned machine as untuned and withheld a fifth of the score. The service already writes down which copy it made, so the question goes to the service. The binding check follows a whole path now, not just its first step. The three cards interleave on screen, so they are reached as Counters.CpuUsage rather than through a scoped subtree, and a scan that stopped at "Counters" would have verified nothing about the thirty-odd names after the dot. Verified by breaking one: the second step is caught.
Eight folded cards, and every field, handler and summary property of all eight ran as one stripe through a single seven-hundred-line class with fourteen dependencies. Five of them are their own view models now - the game watch list, the schedule, the power scheme, the compact readout and diagnostics - each scoped to the expander it lives in. What is left is the two cards that stay open, the tray, the "about" block, and the loading flag. That flag is the only thing the sections share, so it is the whole of ISettingsHost: the stored settings, a way to write them back, and whether the page is still filling its controls in. Every handler checks the last one, because assigning a property raises the same notification a user's click does - without it, opening the page writes the file eight times and applies each value as though it had just been chosen. Verified by putting back the mistake this shape makes possible: a binding that names a property which is still perfectly real on the page's own view model but not on the section it now sits in. The app-wide check passes it, because the name does exist; the scoped check catches it.
All 644 strings: the whole interface plus the catalogues. Every tweak, service, profile and cleanup target carries its name and its description, because a page that lists "Вимкнути Game Bar" next to an English paragraph explaining it is not translated, it is half-translated. The checks that hold the languages together had to stop naming them first. They were hard-coded to compare exactly two files, so a third language would have shipped with none of them - which is precisely the language that needs them. They discover every Strings.<lang>.resx now and hold each one to the English original: same keys, same placeholders, nothing blank, nothing twice. One check is new. Catalogue names live in C# in English and in the resource files translated, so a translation legitimately carries keys the English file does not - which meant a second translation could silently miss a whole catalogue and nothing would notice. The translations are held to each other as well as to the original. Verified by breaking the new file two ways: a dropped key and a lost placeholder are both caught, and the failure names the language.
English is the one part of the interface that is not in a resource file. Every tweak, service, profile and cleanup target carries its English in C# so Core stays useful on its own, and CatalogText looks for a translation first, falling back to that text. It is the right arrangement, and it has a blind spot in both directions. An entry with no English description is an empty paragraph for every English user, and no check that compares resource files can see it, because that text is not in one. A missing translation is one English line in the middle of a translated page, and nothing reports it either - degrading to English is correct on a user's machine and wrong on a developer's. Both directions are held to the catalogue itself now: every entry must carry finished English text, not blank and not a placeholder, and every entry must be translated in every shipped language. The reverse as well - a translation naming an id the catalogue no longer has is a renamed tweak's old name and description, carried and re-reviewed in every language forever. Nothing was wrong when these went in. The point is that nothing would have said so. Verified by making it wrong: an emptied English description is caught, and a renamed id fails four ways - missing in both translations, orphaned in both.
v2.11.0 shipped on 17 September with one section - the profiles page saying which profile is on - and everything since had been piling into that same heading. The release page slices the changelog by heading and the workflow refuses to retag, so this work had nowhere to go until the number moved. The v2.11.0 section is restored to exactly what was published, byte for byte, and everything else is v2.12.0. Two pieces of the audit were worth writing up rather than leaving as commit messages, because both are visible from outside: the tuning score was wrong on a translated Windows, and Restore All could skip a kind of change in silence. The Russian README had fallen behind the English one on the compact readout - in the feature list, the page table and the paragraph that admits there is no overlay. A feature documented in one and not the other is half-documented. Both now carry the language row as well.
The four translations were plain text links, which said nothing about which page you were on. They are badges now, and the current page's own badge is coloured while the rest stay grey - so the row answers "where am I" as well as "where can I go". A UI-languages badge joins the others, because the interface speaking three languages is a fact about the application rather than a line of prose. The rest is catching up with work already merged: - The architecture tree was missing four directories that exist and one that matters - Resources now says what a language file is called, because that is the thing a contributor has to get right. - Gaming and Privacy list the three tweaks added this release. - The Ukrainian landing page says the interface is Ukrainian, which is the point of that page now. - The Spanish one says the opposite, plainly: the page is translated and the application is not. Someone downloading it from that page would otherwise find out afterwards. - Both landing pages gained the compact readout, which they had never heard of. All four badge URLs were fetched and render the text they should, Cyrillic and ñ included - an encoding mistake here is a broken image on the front page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
A small always-on-top window with the monitor's numbers, called up with Ctrl+Shift+M from
inside a game. The frame counter reports a number again — it never did, on any machine. Three new
tweaks, a third interface language, and two counters that were quietly wrong now read correctly.
Underneath, the SOLID audit is done: half of Core became reachable from a test, and the two
largest view models came apart into the cards they draw.
Why
Four things were wrong rather than missing.
The frame counter never worked. Frame timestamps come from Event Tracing in whatever epoch ETW
hands over, and the window aged them against the local wall clock. Where the two do not line up
every frame looks hours old on the first read, the window empties itself, and the counter reports
nothing — forever, and silently, because an empty window is an ordinary state. Every test passed
the same value for both roles, which is exactly why nothing caught it.
The tuning score was wrong on a translated Windows. A fifth of the score is "is a
high-performance scheme active". Where Windows ships without one, SysTuneX duplicates it —
powercfg /duplicateschemegives the copy a fresh GUID and keeps the source scheme's name, whichon a Russian or Ukrainian machine is not the English string the check looked for. Every non-English
machine that needed the duplicate was reported as untuned and lost twenty points.
Restore All could skip a kind of change in silence. Restoring went through one method that knew
every recorded kind by name; a kind added later and not added there was simply not restored, and
nothing said so — the promise the whole application rests on, failing quietly.
Reading a power setting returned zero when it read nothing, and for most of those settings zero
means "off", so it reported a machine as tuned having read nothing at all.
The rest is the audit: running an external tool was a static call, so nothing that parses
powercfg,netshorbcdeditoutput could be reached from a test — and parsing is the part thatgoes wrong, because those tools print in the user's language. Both defects above came out of that
blind spot, found by reading rather than by testing.
How it was verified
dotnet build SysTuneX.sln -c Release— no warnings (TreatWarningsAsErrorsis on)dotnet test tests/SysTuneX.Core.Tests— 456 green, up from 400dotnet test tests/SysTuneX.App.Tests— Windows only; CI runs it. It builds here, it cannot runEvery behavioural claim was checked by breaking the code and confirming the right test failed,
not just that some test did:
Only_safe_tweaks_are_offeredA_tweak_that_is_already_applied_is_counted_but_not_listedConvertTo-Json's single-object caseA_single_package_comes_back_as_a_bare_object_and_is_still_readCounters.CpuUsage{0}Every_catalogue_entry_carries_its_English_textTwo
WindowPlacementTestsfailed on their first run and both were real bugs in new code: verticalreachability used the horizontal threshold, and a screen smaller than the window broke the default
corner.
What is not verified: the frame counter fix cannot be confirmed without a Windows machine and a
game. The failure it addresses is structural and the reasoning is in the changelog, but the fix
itself is unproven on hardware. The same applies to the compact window's placement across monitors.
Risk and rollback
write, through the same journal as every existing tweak.
scheduled-task tweaks are the first two that are not plain registry writes, and they are the
reason the restore path was rebuilt so it can no longer skip a kind without reporting it.
One tweak was deliberately not added. A "lower audio latency" MMCSS change came up while
writing the others; the stock Windows 11 values were checked and the only unset one is a trade
against the game, not a free win. There is a comment in the catalogue saying so, so it is not
"fixed" later by someone who only sees the gap.
Release
release.versionbumped tov2.12.0, andCHANGELOG.mdhas a## v2.12.0sectionREADME.mdandREADME.ru.mdupdated — the Russian one had fallen behind on thecompact readout in three places, and both now carry the language row
Strings.resx,Strings.ru.resxandStrings.uk.resxv2.11.0shipped on 17 September with one section. Everything since had been accumulating underthat same heading, which the release page slices by and the workflow refuses to retag. The v2.11.0
section is restored to exactly what was published, byte for byte; everything else is v2.12.0.
Generated by Claude Code