Files
riojoy/tests/RioJoy.Core.Tests/Profiles/ConfigStoreTests.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

78 lines
2.5 KiB
C#

using RioJoy.Core.Calibration;
using RioJoy.Core.Profiles;
using Xunit;
namespace RioJoy.Core.Tests.Profiles;
public class ConfigStoreTests
{
[Fact]
public void RoundTrips_FullConfig()
{
var config = new AppConfig
{
DefaultRioComPort = "COM3",
DefaultPlasmaComPort = "COM4",
NativeGameExecutables = { "firestorm.exe", "redplanet.exe" },
NeutralProfileName = "Desktop",
AutoStart = true,
Profiles =
{
new RioProfile
{
Name = "Doom",
RioComPort = "COM5",
PlasmaGreeting = "DOOM",
Buttons = { [0] = 0x8049, [0x50] = 0x1009 },
Calibration = new AxisCalibrationConfig { InvertY = true, EnableZR = false },
MatchExecutables = { "doom.exe" },
},
},
};
string json = ConfigStore.Serialize(config);
AppConfig back = ConfigStore.Deserialize(json);
Assert.Equal("COM3", back.DefaultRioComPort);
Assert.Equal(new[] { "firestorm.exe", "redplanet.exe" }, back.NativeGameExecutables);
Assert.True(back.AutoStart);
RioProfile p = Assert.Single(back.Profiles);
Assert.Equal("Doom", p.Name);
Assert.Equal("COM5", p.RioComPort);
Assert.Equal(0x8049, p.Buttons[0]);
Assert.Equal(0x1009, p.Buttons[0x50]);
Assert.True(p.Calibration.InvertY);
Assert.False(p.Calibration.EnableZR);
Assert.Equal(new[] { "doom.exe" }, p.MatchExecutables);
}
[Fact]
public void Load_MissingFile_ReturnsDefaults()
{
string path = Path.Combine(Path.GetTempPath(), $"riojoy-missing-{Guid.NewGuid():N}.json");
AppConfig config = ConfigStore.Load(path);
Assert.Empty(config.Profiles);
Assert.Equal("COM1", config.DefaultRioComPort);
}
[Fact]
public void Save_ThenLoad_File()
{
string path = Path.Combine(Path.GetTempPath(), $"riojoy-{Guid.NewGuid():N}", "config.json");
try
{
var config = new AppConfig { DefaultRioComPort = "COM9" };
ConfigStore.Save(config, path);
Assert.True(File.Exists(path));
Assert.Equal("COM9", ConfigStore.Load(path).DefaultRioComPort);
}
finally
{
string? dir = Path.GetDirectoryName(path);
if (dir is not null && Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
}