Files
riojoy/tests/RioJoy.Core.Tests/Profiles/AutoSwitchTests.cs
T
CydandClaude Opus 4.8 1348040e1c Phase 5: tray app + profiles + runtime wiring
Wire the Core pieces into a runnable tray app with per-game profiles and the
three-state serial-yield auto-switch:

- Profiles/: RioProfile + AppConfig model; ConfigStore (System.Text.Json,
  round-tripped); RioIniImporter ports the legacy RIO.ini (button table, invert
  flags, plasma greeting); AutoSwitchResolver + AutoSwitchWatcher resolve the
  foreground executable into Yield (native game) / Activate (profile) / Idle, with
  native always winning and change-only notifications. IForegroundProcessProvider
  abstracts the OS.
- RioRuntime assembles a profile's live pipeline: serial ButtonPressed/Released +
  KeyPressed/Released → InputRouter (via RioAddress); AnalogReply → AxisCalibrator
  → the six joystick axes; RIO commands → calibration resets + version/check
  requests + lamp re-init. SerialLampSink sends lamp feedback over the link;
  NullJoystickSink is a placeholder until the Phase 1 HID feeder exists.
- RioJoy.Tray: NotifyIcon menu mirroring the legacy console menu (axis resets,
  version/status, raw-axes & poll-rate toggles, quit) + profile selection
  (auto vs. manual) + "start with Windows"; RioCoordinator owns the serial
  acquire/release tied to the watcher (native-game COM-port yield). OS adapters:
  ForegroundProcessProvider (Win32 foreground PID→exe) and AutoStartManager (HKCU
  Run key).
- tests: 18 new xUnit tests (123 total) for config round-trip, ini import,
  the three-state resolver + watcher, and RioRuntime end-to-end over the fake
  transport (button→joystick, keypad-offset→keyboard, analog→six axes).

The joystick output stays a no-op until the Phase 1 driver; on-cabinet
verification of the acquire/release lifecycle remains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:36:58 -05:00

91 lines
2.9 KiB
C#

using RioJoy.Core.Profiles;
using Xunit;
namespace RioJoy.Core.Tests.Profiles;
public class AutoSwitchTests
{
private static AppConfig Config() => new()
{
NativeGameExecutables = { "firestorm.exe", "RedPlanet" },
Profiles =
{
new RioProfile { Name = "Doom", MatchExecutables = { "doom.exe", "doom2" } },
new RioProfile { Name = "Quake", MatchExecutables = { "quake.exe" } },
},
};
[Theory]
[InlineData("firestorm.exe")]
[InlineData("FIRESTORM.EXE")]
[InlineData(@"C:\games\Firestorm\firestorm.exe")]
[InlineData("redplanet.exe")] // configured without extension, matched with one
public void NativeGame_Yields(string exe)
{
Assert.Equal(SwitchMode.Yield, AutoSwitchResolver.Resolve(Config(), exe).Mode);
}
[Fact]
public void SupportedGame_Activates_ItsProfile()
{
SwitchDecision d = AutoSwitchResolver.Resolve(Config(), @"D:\Games\doom.exe");
Assert.Equal(SwitchMode.Activate, d.Mode);
Assert.Equal("Doom", d.Profile!.Name);
}
[Fact]
public void Unknown_IsIdle_WhenNoNeutral()
{
Assert.Equal(SwitchMode.Idle, AutoSwitchResolver.Resolve(Config(), "explorer.exe").Mode);
Assert.Equal(SwitchMode.Idle, AutoSwitchResolver.Resolve(Config(), null).Mode);
}
[Fact]
public void Unknown_ActivatesNeutral_WhenConfigured()
{
AppConfig config = Config();
config.NeutralProfileName = "Doom";
SwitchDecision d = AutoSwitchResolver.Resolve(config, "explorer.exe");
Assert.Equal(SwitchMode.Activate, d.Mode);
Assert.Equal("Doom", d.Profile!.Name);
}
[Fact]
public void Native_Wins_OverProfileMatch()
{
AppConfig config = Config();
// A profile also claims the native exe; native yield must still win.
config.Profiles[0].MatchExecutables.Add("firestorm.exe");
Assert.Equal(SwitchMode.Yield, AutoSwitchResolver.Resolve(config, "firestorm.exe").Mode);
}
[Fact]
public void Watcher_RaisesOnlyOnChange()
{
var config = Config();
string? exe = "doom.exe";
var provider = new FakeForeground(() => exe);
var watcher = new AutoSwitchWatcher(provider, () => config);
var changes = new List<SwitchDecision>();
watcher.DecisionChanged += changes.Add;
watcher.Poll(); // doom → Activate
watcher.Poll(); // doom again → no change
exe = "firestorm.exe";
watcher.Poll(); // native → Yield (change)
exe = "firestorm.exe";
watcher.Poll(); // same → no change
Assert.Equal(2, changes.Count);
Assert.Equal(SwitchMode.Activate, changes[0].Mode);
Assert.Equal(SwitchMode.Yield, changes[1].Mode);
}
private sealed class FakeForeground(Func<string?> get) : IForegroundProcessProvider
{
public string? GetForegroundExecutable() => get();
}
}