tray: --import-profile merges a profile document into the user config

RioJoy.Tray --import-profile <profile.json> merges a single-profile
document (a repo's profiles/*.json) into %APPDATA%\RIOJoy\config.json
via ConfigStore.ImportProfile: same-name profiles are replaced in place
(FindProfile's case-insensitive convention), everything else appended,
all other config content preserved. Refuses (exit 3) while a tray
instance is running - a live tray rewrites the config from memory and
would silently discard the import. Documents that a Name must be stated
in the file itself: RioProfile defaults Name to 'Unnamed', so the import
checks the raw JSON, not the deserialized object. This is the profile
install story for bench machines and pods (previously: hand-paste into
the Profiles array).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-30 09:33:25 -05:00
co-authored by Claude Fable 5
parent 6967e66837
commit 3512c89dca
4 changed files with 176 additions and 3 deletions
@@ -138,6 +138,87 @@ public class ConfigStoreTests
Assert.Equal(File.ReadAllBytes(shipped), File.ReadAllBytes(reference));
}
[Fact]
public void ImportProfile_ShippedDescentFile_IntoFreshConfig()
{
string configPath = Path.Combine(Path.GetTempPath(), $"riojoy-import-{Guid.NewGuid():N}", "config.json");
try
{
string shipped = Path.Combine(TestRepo.Root(), "profiles", "descent-d1x.json");
ProfileImportResult result = ConfigStore.ImportProfile(configPath, shipped);
Assert.Equal("Descent", result.Name);
Assert.False(result.Replaced);
RioProfile p = Assert.Single(ConfigStore.Load(configPath).Profiles);
Assert.Equal("Descent", p.Name);
// The routing must survive the import round-trip intact — this is the
// exact payload a cabinet/bench install writes.
Assert.NotNull(p.AxisRouting);
Assert.Equal(new AxisRoute { Target = PadTarget.RightThumbY, Mode = AxisOutputMode.UnipolarPositive }, p.AxisRouting!.Z);
}
finally
{
string? dir = Path.GetDirectoryName(configPath);
if (dir is not null && Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
[Fact]
public void ImportProfile_ReplacesSameName_PreservesEverythingElse()
{
string configPath = Path.Combine(Path.GetTempPath(), $"riojoy-import-{Guid.NewGuid():N}", "config.json");
try
{
ConfigStore.Save(new AppConfig
{
DefaultRioComPort = "COM7",
NeutralProfileName = "Desktop",
Profiles =
{
new RioProfile { Name = "Desktop" },
new RioProfile { Name = "descent", PlasmaGreeting = "STALE" }, // case-insensitive match
},
}, configPath);
string shipped = Path.Combine(TestRepo.Root(), "profiles", "descent-d1x.json");
ProfileImportResult result = ConfigStore.ImportProfile(configPath, shipped);
Assert.True(result.Replaced);
AppConfig back = ConfigStore.Load(configPath);
Assert.Equal("COM7", back.DefaultRioComPort); // untouched settings survive
Assert.Equal("Desktop", back.NeutralProfileName);
Assert.Equal(2, back.Profiles.Count); // replaced in place, not appended
RioProfile descent = Assert.IsType<RioProfile>(back.FindProfile("Descent"));
Assert.Equal("DESCENT", descent.PlasmaGreeting); // stale profile fully overwritten
}
finally
{
string? dir = Path.GetDirectoryName(configPath);
if (dir is not null && Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
[Fact]
public void ImportProfile_NamelessProfile_Throws()
{
string dir = Path.Combine(Path.GetTempPath(), $"riojoy-import-{Guid.NewGuid():N}");
Directory.CreateDirectory(dir);
try
{
string profilePath = Path.Combine(dir, "nameless.json");
File.WriteAllText(profilePath, "{\"PlasmaGreeting\":\"X\"}");
Assert.Throws<Newtonsoft.Json.JsonSerializationException>(
() => ConfigStore.ImportProfile(Path.Combine(dir, "config.json"), profilePath));
}
finally
{
Directory.Delete(dir, recursive: true);
}
}
[Fact]
public void Load_MissingFile_ReturnsDefaults()
{