using AcDream.Headless.Configuration;
using AcDream.Launcher.Core.Launching;
using AcDream.Launcher.Core.Orchestration;
using AcDream.Launcher.Core.Profiles;
namespace AcDream.Headless.Tests;
///
/// The launcher spawns the headless host; the headless host decides what a
/// valid command line is. Nothing connected those two facts, and they drifted:
/// the launcher passed --config <path> while
/// reads arguments[0] as the
/// command and accepts only validate or run. Every
/// launcher-started headless session and every character refresh therefore died
/// instantly with "Invalid command. Run --help for usage." — visible only as an
/// exit code in a status file, which is why it survived a whole campaign.
///
/// These tests are the missing connection: they take the argument vector
/// the launcher will really use and hand it to the parser the host will really
/// use. The two cannot drift again without failing here.
///
public sealed class LauncherHeadlessCommandLineContractTests : IDisposable
{
private readonly string _root = Path.Combine(
Path.GetTempPath(),
"acdream-headless-cli-contract",
Guid.NewGuid().ToString("N"));
public LauncherHeadlessCommandLineContractTests()
{
// The spec builders refuse to name an executable that is not there, so
// the contract can only be exercised against files that exist.
Directory.CreateDirectory(AppDirectory);
string suffix = OperatingSystem.IsWindows() ? ".exe" : string.Empty;
CreateStubExecutable(Path.Combine(AppDirectory, "AcDream.App" + suffix));
CreateStubExecutable(Path.Combine(AppDirectory, "acdream-headless" + suffix));
}
///
/// On Linux the spec builders also refuse a host that exists but has no
/// execute bit — a real check, since an extracted update that lost its
/// permissions would otherwise fail deep inside process start. A stub
/// written with WriteAllText is 0644, so it has to be made executable for
/// the command-line contract underneath to be reachable at all.
///
private static void CreateStubExecutable(string path)
{
File.WriteAllText(path, "stub");
if (!OperatingSystem.IsWindows())
{
File.SetUnixFileMode(
path,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute
| UnixFileMode.GroupRead | UnixFileMode.GroupExecute
| UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}
}
public void Dispose()
{
if (Directory.Exists(_root))
{
Directory.Delete(_root, recursive: true);
}
}
[Fact]
public void ProbeArgumentsParseAsAHeadlessRunCommand()
{
LauncherProcessSpec spec = ExecutableSet().CreateProbeSpec(ConfigPath);
HeadlessCommandLine parsed = HeadlessCommandLine.Parse(spec.Arguments);
Assert.Equal("run", parsed.Command);
Assert.Equal(ConfigPath, parsed.ConfigurationPath);
}
[Fact]
public void HeadlessPlayArgumentsParseAsAHeadlessRunCommand()
{
LauncherProcessSpec spec = ExecutableSet()
.CreatePlaySpec(LaunchMode.Headless, ConfigPath);
HeadlessCommandLine parsed = HeadlessCommandLine.Parse(spec.Arguments);
Assert.Equal("run", parsed.Command);
Assert.Equal(ConfigPath, parsed.ConfigurationPath);
}
///
/// The graphical host is a DIFFERENT program with a different grammar — it
/// takes a bare --session-config and has no command word. Copying
/// that shape onto the headless call is exactly the mistake this file
/// exists to prevent, so pin the difference rather than leaving it implied.
///
[Theory]
[InlineData(LaunchMode.Gui)]
[InlineData(LaunchMode.GuiSelect)]
public void GraphicalArgumentsAreNotAHeadlessCommandLine(LaunchMode mode)
{
LauncherProcessSpec spec = ExecutableSet().CreatePlaySpec(mode, ConfigPath);
Assert.Equal(["--session-config", ConfigPath], spec.Arguments);
Assert.Throws(
() => HeadlessCommandLine.Parse(spec.Arguments));
}
private string AppDirectory => Path.Combine(_root, "app");
private string ConfigPath => Path.Combine(_root, "session.json");
private LauncherExecutableSet ExecutableSet() =>
LauncherExecutableSet.FromDirectory(AppDirectory);
}