using System.Runtime.CompilerServices; using AcDream.App.Configuration; namespace AcDream.App.Tests.Configuration; /// /// Campaign LA slice LA1: proves the App config reader accepts the EXACT /// document the Headless reader also accepts — /// tests/Fixtures/campaign-la/session-config-shared-fixture.json is /// parsed by both here and /// AcDream.Headless.Configuration.HeadlessConfigurationLoader in /// AcDream.Headless.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 AppReaderAcceptsTheProductionShapedSharedFixture() { (SessionConfiguration configuration, SessionDescriptor session) = SessionConfigurationLoader.Load(SharedFixturePath()); Assert.Equal(1, configuration.Version); Assert.Equal( "shared-fixture-dats", configuration.Process?.Content?.DatDirectory); Assert.Equal( "shared-fixture-dats/acdream.pak", configuration.Process?.Content?.PreparedAssetPath); 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); // App parses the policy field structurally but never consults it — // the pinned contract's "parsed-and-ignored" clause. Assert.Equal("idle", session.Policy?.Id); Assert.Equal( SessionCredentialProviderKind.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() { using TemporaryFile file = TemporaryFile.Create( """ { "version": 1, "sessions": [ { "id": "no-launch-contract-fields", "endpoint": { "host": "127.0.0.1", "port": 9000 }, "account": "account", "credential": { "provider": "environment", "reference": "X" } } ] } """); (_, SessionDescriptor session) = SessionConfigurationLoader.Load(file.Path); Assert.Null(session.Character); Assert.Null(session.Plugins); Assert.Null(session.LoginCommands); Assert.Equal(500, session.LoginCommandDelayMs); Assert.Null(session.StatusFile); } [Fact] public void MoreThanOneSessionFailsLoadForTheGraphicalHost() { using TemporaryFile file = TemporaryFile.Create( """ { "version": 1, "sessions": [ { "id": "one", "endpoint": { "host": "127.0.0.1", "port": 9000 }, "account": "account", "credential": { "provider": "environment", "reference": "A" } }, { "id": "two", "endpoint": { "host": "127.0.0.1", "port": 9001 }, "account": "account2", "credential": { "provider": "environment", "reference": "B" } } ] } """); Assert.Throws( () => SessionConfigurationLoader.Load(file.Path)); } [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", "credential": { "provider": "environment", "reference": "X" }, "plugins": ["Ok", " "] } ] } """); Assert.Throws( () => SessionConfigurationLoader.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", "credential": { "provider": "environment", "reference": "X" }, "loginCommandDelayMs": -1 } ] } """); Assert.Throws( () => SessionConfigurationLoader.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", "credential": { "provider": "environment", "reference": "X" }, "statusFile": " " } ] } """); Assert.Throws( () => SessionConfigurationLoader.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-app-la1-{Guid.NewGuid():N}.json"); File.WriteAllText(path, json); return new TemporaryFile(path); } public void Dispose() => File.Delete(Path); } }