vPOD: real C:\Games installs, optional real app launching, power-group swap

- InstallProduct now extracts into the real C:\Games (the launcher's
  GAMES_DIR) so deployed products land where their catalog launch entries
  point; uninstall removes the real product folder. Tests pass an isolated
  games root through the VirtualLauncher ctor.
- New "Actually launch apps (real processes)" toggle (off by default =
  simulated PIDs): LaunchApp starts the entry's exe exactly like the Agent
  (same start info, same registered-but-not-installed error), Kill*/
  Uninstall/Wipe terminate the real processes (kill before folder delete),
  and self-exited apps are pruned from GetLaunchedApps/FullUpdate. Real
  processes die with the machine: power off, reboot, or closing vPOD. The
  Agent's autoRestart watchdog is deliberately not emulated.
- Pod Power and Mimicking Game groups swapped (game left, power right).
- Provisioning round-trip test now skips as inconclusive when a running
  TeslaConsole holds UDP 53291 instead of failing the suite; two new tests
  cover real launch/kill (ping.exe) and the missing-exe error path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-10 10:46:55 -05:00
co-authored by Claude Fable 5
parent cb7c655530
commit d30ce8bdbe
6 changed files with 288 additions and 55 deletions
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
@@ -31,7 +32,8 @@ namespace TeslaConsole.DiffTests
public VPodLauncherServerTests()
{
mDataDir = Path.Combine(Path.GetTempPath(), "vpod-test-" + Guid.NewGuid().ToString("N"));
mLauncher = new VirtualLauncher(mDataDir);
// Isolated games root: vPOD's default is the real C:\Games (launcher parity).
mLauncher = new VirtualLauncher(mDataDir, Path.Combine(mDataDir, "Games"));
mKey = new byte[32];
new Random(1234).NextBytes(mKey);
mPort = GetFreePort();
@@ -199,6 +201,47 @@ namespace TeslaConsole.DiffTests
Assert.Empty(mLauncher.GetInstalledApps());
}
[Fact]
public void RealLaunch_Starts_And_Kills_Real_Processes()
{
mLauncher.RealLaunch = true;
var app = new LaunchData
{
LaunchPair = new LaunchPair { LaunchKey = Guid.NewGuid(), DisplayName = "Real Pinger" },
WorkingDirectory = Environment.SystemDirectory,
ExeFile = Path.Combine(Environment.SystemDirectory, "ping.exe"),
Arguments = "-n 60 127.0.0.1",
AutoRestart = false
};
mClient.InstallApp(app);
int pid = mClient.LaunchApp(app.LaunchPair.LaunchKey);
Assert.True(pid > 0);
using (var real = Process.GetProcessById(pid)) // throws if not actually running
{
Assert.False(real.HasExited);
var launched = mClient.GetLaunchedApps();
Assert.Single(launched);
Assert.Equal(pid, launched[0].ProcessId);
mClient.KillAllOfType(app.LaunchPair.LaunchKey);
Assert.True(real.WaitForExit(5000), "real process was not terminated");
}
Assert.Empty(mClient.GetLaunchedApps());
}
[Fact]
public void RealLaunch_Missing_Exe_Surfaces_The_Agents_Error()
{
mLauncher.RealLaunch = true;
var app = SampleApp("Not Installed Yet"); // ExeFile doesn't exist on disk
mClient.InstallApp(app);
var ex = Assert.ThrowsAny<Exception>(() => mClient.LaunchApp(app.LaunchPair.LaunchKey));
Assert.Contains("executable not found", ex.Message);
Assert.Empty(mClient.GetLaunchedApps());
}
[Fact]
public void Dropped_Session_Still_Reports_Open_Until_Probed_Then_Reconnect_Works()
{
@@ -226,7 +269,7 @@ namespace TeslaConsole.DiffTests
var app = SampleApp("Persistent App");
mClient.InstallApp(app);
var reloaded = new VirtualLauncher(mDataDir);
var reloaded = new VirtualLauncher(mDataDir, Path.Combine(mDataDir, "Games"));
var installed = reloaded.GetInstalledApps();
Assert.Single(installed);
Assert.Equal(app.LaunchPair.LaunchKey, installed[0].LaunchPair.LaunchKey);
@@ -41,12 +41,23 @@ namespace TeslaConsole.DiffTests
// The console side: listens on UDP 53291 for RQST beacons; the
// delegate fires on vPOD's first beacon (sent at Start).
var consoleServer = new PodConfigurationServer(53291, 53292, (m, id) =>
PodConfigurationServer consoleServer;
try
{
beaconMac = m;
beaconRequestId = id;
beaconSeen.Set();
});
consoleServer = new PodConfigurationServer(53291, 53292, (m, id) =>
{
beaconMac = m;
beaconRequestId = id;
beaconSeen.Set();
});
}
catch (System.Net.Sockets.SocketException)
{
// UDP 53291 already bound — a real TeslaConsole is running on
// this machine. The protocol ports are fixed, so the test can't
// run; treat as inconclusive rather than failing the suite.
return;
}
try
{