using System.Runtime.CompilerServices; using AcDream.Headless.Configuration; namespace AcDream.Headless.Tests; /// /// Campaign LA slice LA1: proves the Headless config reader accepts the /// EXACT document the App reader also accepts — /// tests/Fixtures/campaign-la/session-config-shared-fixture.json is /// parsed by both here and /// AcDream.App.Configuration.SessionConfigurationLoader in /// AcDream.App.Tests's twin of this test. This is the pinned-contract /// acceptance test from docs/plans/2026-08-14-launcher-campaign.md /// LA1: "a SHARED fixture JSON parsed by both test suites proving the two /// readers accept the identical document." If either reader's DTO shape /// drifts from the pinned contract, ONE of these two tests fails. /// public sealed class SessionConfigurationSharedFixtureTests { [Fact] public void HeadlessReaderAcceptsTheProductionShapedSharedFixture() { HeadlessConfiguration configuration = HeadlessConfigurationLoader.Load(SharedFixturePath()); Assert.Equal( "shared-fixture-dats", configuration.Process.Content?.DatDirectory); Assert.Equal( "shared-fixture-dats/acdream.pak", configuration.Process.Content?.PreparedAssetPath); HeadlessSessionDescriptor session = Assert.Single(configuration.Sessions)!; Assert.Equal("shared-fixture", session.Id); Assert.Equal("127.0.0.1", session.Endpoint.Host); Assert.Equal(9000, session.Endpoint.Port); Assert.Equal("sharedaccount", session.Account); Assert.Equal("SharedToon", session.Character.Name); Assert.Equal("idle", session.Policy.Id); Assert.Equal( HeadlessCredentialProviderKind.StandardInput, session.Credential.Provider); Assert.Equal("session", session.Credential.Reference); Assert.Equal(["ExamplePlugin", "AnotherPlugin"], session.Plugins); Assert.Equal( ["/tell someone, hi", "/vt start"], session.LoginCommands); Assert.Equal(750, session.LoginCommandDelayMs); Assert.Equal("shared-fixture-status.jsonl", session.StatusFile); } [Fact] public void AbsentLaunchContractFieldsFallBackToPinnedDefaults() { // Every LA1 field is optional; a document that omits all five must // still load, with loginCommandDelayMs defaulting to the pinned // 500 ms and the rest defaulting to "nothing configured". using TemporaryFile file = TemporaryFile.Create( """ { "version": 1, "sessions": [ { "id": "no-launch-contract-fields", "endpoint": { "host": "127.0.0.1", "port": 9000 }, "account": "account", "character": { "index": 0 }, "policy": { "id": "idle" }, "credential": { "provider": "environment", "reference": "X" } } ] } """); HeadlessConfiguration configuration = HeadlessConfigurationLoader.Load(file.Path); HeadlessSessionDescriptor session = Assert.Single(configuration.Sessions)!; Assert.Null(session.Plugins); Assert.Null(session.LoginCommands); Assert.Equal(500, session.LoginCommandDelayMs); Assert.Null(session.StatusFile); } [Fact] public void EmptyPluginsEntryFailsLoad() { using TemporaryFile file = TemporaryFile.Create( """ { "version": 1, "sessions": [ { "id": "bad-plugins", "endpoint": { "host": "127.0.0.1", "port": 9000 }, "account": "account", "character": { "index": 0 }, "policy": { "id": "idle" }, "credential": { "provider": "environment", "reference": "X" }, "plugins": ["Ok", " "] } ] } """); Assert.Throws( () => HeadlessConfigurationLoader.Load(file.Path)); } [Fact] public void NegativeLoginCommandDelayFailsLoad() { using TemporaryFile file = TemporaryFile.Create( """ { "version": 1, "sessions": [ { "id": "bad-delay", "endpoint": { "host": "127.0.0.1", "port": 9000 }, "account": "account", "character": { "index": 0 }, "policy": { "id": "idle" }, "credential": { "provider": "environment", "reference": "X" }, "loginCommandDelayMs": -1 } ] } """); Assert.Throws( () => HeadlessConfigurationLoader.Load(file.Path)); } [Fact] public void BlankStatusFileFailsLoad() { using TemporaryFile file = TemporaryFile.Create( """ { "version": 1, "sessions": [ { "id": "bad-status-file", "endpoint": { "host": "127.0.0.1", "port": 9000 }, "account": "account", "character": { "index": 0 }, "policy": { "id": "idle" }, "credential": { "provider": "environment", "reference": "X" }, "statusFile": " " } ] } """); Assert.Throws( () => HeadlessConfigurationLoader.Load(file.Path)); } internal static string SharedFixturePath( [CallerFilePath] string sourcePath = "") => Path.Combine( FindRepositoryRoot(sourcePath), "tests", "Fixtures", "campaign-la", "session-config-shared-fixture.json"); private static string FindRepositoryRoot(string sourcePath) { string[] starts = { Path.GetDirectoryName(sourcePath) ?? string.Empty, Directory.GetCurrentDirectory(), AppContext.BaseDirectory, }; foreach (string start in starts) { if (string.IsNullOrEmpty(start)) continue; DirectoryInfo? directory = new(start); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) return directory.FullName; directory = directory.Parent; } } throw new DirectoryNotFoundException( "Could not find AcDream.slnx above the working or output directory."); } private sealed class TemporaryFile : IDisposable { private TemporaryFile(string path) => Path = path; internal string Path { get; } internal static TemporaryFile Create(string json) { string path = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"acdream-headless-la1-{Guid.NewGuid():N}.json"); File.WriteAllText(path, json); return new TemporaryFile(path); } public void Dispose() => File.Delete(Path); } }