using System.Text.Json; using AcDream.Runtime.Session; namespace AcDream.Runtime.Tests.Session; /// /// Campaign LA slice LA1: pins the exact JSONL status-stream contract both /// the App and Headless hosts write into, and the launcher (a process we /// don't own) reads โ€” see docs/plans/2026-08-14-launcher-campaign.md /// LA1 and docs/superpowers/specs/2026-08-14-launcher-campaign-design.md /// ยง6. /// public sealed class SessionStatusWriterTests { [Fact] public void EachEventWritesTheExactPinnedShapeInOrder() { using TemporaryFile file = TemporaryFile.Create(); var writer = new SessionStatusWriter(file.Path); writer.Started("s1"); writer.Connected("s1"); writer.CharacterList( "s1", new LiveSessionRosterReport( "account", 11, [ new LiveSessionRosterEntry(0x50000001u, "Ready", 0u), new LiveSessionRosterEntry(0x50000002u, "Grey", 10u), ])); writer.EnteredWorld("s1", 0x50000001u, "Ready"); writer.Disconnected("s1", "stopped"); writer.Exited("s1", 0, "disposed"); string[] lines = File.ReadAllLines(file.Path); Assert.Equal(6, lines.Length); JsonElement started = Parse(lines[0]); Assert.Equal(1, started.GetProperty("v").GetInt32()); Assert.Equal("started", started.GetProperty("e").GetString()); Assert.True(started.TryGetProperty("t", out _)); Assert.Equal("s1", started.GetProperty("sessionId").GetString()); JsonElement connected = Parse(lines[1]); Assert.Equal("connected", connected.GetProperty("e").GetString()); Assert.Equal("s1", connected.GetProperty("sessionId").GetString()); JsonElement characterList = Parse(lines[2]); Assert.Equal("characterList", characterList.GetProperty("e").GetString()); Assert.Equal("account", characterList.GetProperty("accountName").GetString()); Assert.Equal(11, characterList.GetProperty("slotCount").GetInt32()); JsonElement characters = characterList.GetProperty("characters"); Assert.Equal(2, characters.GetArrayLength()); JsonElement first = characters[0]; Assert.Equal(0x50000001u, first.GetProperty("id").GetUInt32()); Assert.Equal("Ready", first.GetProperty("name").GetString()); Assert.Equal(0u, first.GetProperty("secondsGreyedOut").GetUInt32()); JsonElement enteredWorld = Parse(lines[3]); Assert.Equal("enteredWorld", enteredWorld.GetProperty("e").GetString()); Assert.Equal(0x50000001u, enteredWorld.GetProperty("characterId").GetUInt32()); Assert.Equal("Ready", enteredWorld.GetProperty("characterName").GetString()); JsonElement disconnected = Parse(lines[4]); Assert.Equal("disconnected", disconnected.GetProperty("e").GetString()); Assert.Equal("stopped", disconnected.GetProperty("reason").GetString()); JsonElement exited = Parse(lines[5]); Assert.Equal("exited", exited.GetProperty("e").GetString()); Assert.Equal(0, exited.GetProperty("code").GetInt32()); Assert.Equal("disposed", exited.GetProperty("reason").GetString()); } [Fact] public void NoOpWriterNeverCreatesAFile() { using TemporaryFile file = TemporaryFile.Reserve(); var writer = new SessionStatusWriter(null); writer.Started("s1"); writer.Connected("s1"); writer.Disconnected("s1", "stopped"); writer.Exited("s1", 0, "disposed"); Assert.False(writer.IsEnabled); Assert.False(File.Exists(file.Path)); } [Fact] public void BlankPathIsTreatedAsAbsent() { var writer = new SessionStatusWriter(" "); Assert.False(writer.IsEnabled); // Must not throw even though there is no real path behind it. writer.Started("s1"); } [Fact] public void PasswordNeverAppearsInTheStatusStream() { using TemporaryFile file = TemporaryFile.Create(); var writer = new SessionStatusWriter(file.Path); writer.Started("bot"); writer.Connected("bot"); writer.CharacterList( "bot", new LiveSessionRosterReport( "account-name", 11, [new LiveSessionRosterEntry(0x50000001u, "Ready", 0u)])); writer.EnteredWorld("bot", 0x50000001u, "Ready"); writer.Disconnected("bot", "stopped"); writer.Exited("bot", 0, "disposed"); string contents = File.ReadAllText(file.Path); Assert.DoesNotContain("hunter2", contents, StringComparison.Ordinal); Assert.DoesNotContain("password", contents, StringComparison.OrdinalIgnoreCase); } [Fact] public void FileIsOpenedShareReadSoAConcurrentTailerCanReadWhileAppending() { using TemporaryFile file = TemporaryFile.Create(); var writer = new SessionStatusWriter(file.Path); writer.Started("s1"); // A concurrent reader (the launcher's tailer) must be able to open // the file for read while the writer holds it โ€” FileShare.Read on // the writer side is what this test is pinning. using FileStream tailer = new( file.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var tailerReader = new StreamReader(tailer); string? firstLine = tailerReader.ReadLine(); Assert.NotNull(firstLine); Assert.Contains("\"started\"", firstLine); // The writer keeps working while the tailer's handle is still open. writer.Connected("s1"); string? secondLine = tailerReader.ReadLine(); Assert.NotNull(secondLine); Assert.Contains("\"connected\"", secondLine); } private static JsonElement Parse(string line) => JsonDocument.Parse(line).RootElement; private sealed class TemporaryFile : IDisposable { private TemporaryFile(string path) => Path = path; internal string Path { get; } internal static TemporaryFile Create() { string path = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"acdream-status-{Guid.NewGuid():N}.jsonl"); return new TemporaryFile(path); } /// A path that is never actually created โ€” used by the /// no-op test to assert the writer truly never touches disk. internal static TemporaryFile Reserve() => Create(); public void Dispose() { if (File.Exists(Path)) File.Delete(Path); } } }