using Godosa.Core.Runs; using Godosa.Core.Runs.Scripts; using Xunit; using static Godosa.Core.Tests.Runs.Scripts.ScriptRunnerTests; namespace Godosa.Core.Tests.Runs.Scripts; /// Final-review findings (lib core): each test failed before its fix. public sealed class ReviewFixTests : IDisposable { private readonly string _dir = Path.Combine(AppContext.BaseDirectory, "rs-tmp", Guid.NewGuid().ToString("N")); public void Dispose() { if (Directory.Exists(_dir)) Directory.Delete(_dir, recursive: true); } private const string Script = "format 1\nmode player\nkey A\ncheckpoint one\nkey B\ncheckpoint two\nkey C"; [Fact] public void NoArgVerb_AndEmptySet_Run() { var g = new FakeGame(); g.ActionTable["close-panel"] = new FakeAction(VerbKind.Player, _ => new ActionStart(Status: StepStatus.Done), min: 0, max: 0); var (r, output) = Run("format 1\nmode player\nset x =\nclose-panel\nlog \"[${x}]\"", g); Assert.Equal(RunOutcome.Passed, r.Outcome); Assert.Contains("[]", output); } // A probe value (dialog text) with quotes inside a quoted argument stays text: the line is split into tokens first. [Fact] public void QuotedArgument_ValueWithQuotes_StaysOneToken() { var (r, output) = Run("format 1\nmode player\nset x = \"\\\"Hi,\\\" he said\"\nlog \"R: ${x}\"", new FakeGame()); Assert.Equal(RunOutcome.Passed, r.Outcome); Assert.Contains("R: \"Hi,\" he said", output); } [Fact] public void SubstitutedValues_AreNotRescanned() { var values = new Dictionary { ["d"] = "$", ["e"] = "{x}", ["raw"] = "${y}" }; Assert.Equal("${x}", VarExpander.Expand("${d}${e}", n => values.GetValueOrDefault(n), null)); Assert.Equal("say ${y}!", VarExpander.Expand("say ${raw}!", n => values.GetValueOrDefault(n), null)); } [Theory] [InlineData("log Done:", "log", "Done:")] [InlineData("type name:", "type", "name:")] public void LineEndingInColon_IsAVerbUnlessABlockKeyword(string line, string verb, string args) { var f = ScriptParser.Parse("main.steps", $"format 1\nmode debug\n{line}"); Assert.Empty(f.Errors); var v = Assert.IsType(f.Body.Single()); Assert.Equal((verb, args), (v.Verb, v.Args)); } [Fact] public void ThrowingInputRouter_FailsTheRun() { var g = new FakeGame { OnSend = _ => throw new InvalidOperationException("router down") }; var (r, output) = Run("format 1\nmode player\nkey R", g); Assert.Equal(RunOutcome.Failed, r.Outcome); Assert.Contains(output, l => l == "FAIL main.steps:3 key R: InvalidOperationException: router down"); } [Fact] public void CorruptCacheEntry_IsAMiss() { Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir }); foreach (var f in Directory.GetFiles(_dir, "*.snap")) File.WriteAllBytes(f, [.. "GRS1"u8, 3, 0, 0, 0, (byte)'{', (byte)'x', (byte)'!']); var (r, output) = Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir }); Assert.Equal(RunOutcome.Passed, r.Outcome); Assert.DoesNotContain(output, l => l.StartsWith("resumed")); } // A game state the snapshot can't hold (an open dialog) → checkpoint not cached, run unharmed. [Fact] public void RefusedSnapshot_CheckpointNotCached() { var g = new FakeGame { Refusal = "dialog open" }; var output = new List(); var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = Script, }), new Dictionary()), g, new ScriptRunOptions { CacheDir = _dir }, output.Add); Assert.Equal(RunOutcome.Passed, runner.Run(1000).Outcome); Assert.Equal(["checkpoint one not cached: dialog open", "checkpoint two not cached: dialog open"], runner.Report!.Warnings); Assert.False(Directory.Exists(_dir) && Directory.GetFiles(_dir, "*.snap", SearchOption.AllDirectories).Length > 0); } [Fact] public void FailingRestore_FailsTheRun() { Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir }); var (r, output) = Run(Script, new FakeGame { RestoreThrows = true }, new ScriptRunOptions { CacheDir = _dir }); Assert.Equal((RunOutcome.Failed, 1), (r.Outcome, r.ExitCode)); Assert.Contains(output, l => l.Contains("IOException: restore broke")); } [Fact] public void UnwritableCacheAndReportDirs_WarnInsteadOfThrowing() { Directory.CreateDirectory(_dir); var file = Path.Combine(_dir, "file"); File.WriteAllText(file, "x"); var g = new FakeGame(); g.ProbeFns["no"] = _ => RunValue.Of(false); var output = new List(); var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = "format 1\nmode player\ncheckpoint one\nexpect no", }), new Dictionary()), g, new ScriptRunOptions { CacheDir = Path.Combine(file, "cache"), ReportDir = Path.Combine(file, "report") }, output.Add); var r = runner.Run(1000); Assert.Equal(RunOutcome.Failed, r.Outcome); Assert.Contains(runner.Report!.Warnings, w => w.StartsWith("checkpoint cache not written:")); Assert.Contains(output, l => l.StartsWith("warning: report not written:")); } [Fact] public void Until_UnknownName_Invalid() { var (r, output) = Run(Script, new FakeGame(), new ScriptRunOptions { Until = "nowhere" }); Assert.Equal((RunOutcome.Invalid, 2), (r.Outcome, r.ExitCode)); Assert.Contains("unknown label or checkpoint nowhere", output); } [Fact] public void Until_ResumesOnlyBeforeIt() { Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir }); // caches one and two var output = new List(); var r = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = Script }), new Dictionary()), new FakeGame(), new ScriptRunOptions { CacheDir = _dir, Until = "two" }, output.Add).Run(1000); Assert.Contains("resumed at one (cache) f=2", output); Assert.Equal((RunOutcome.Stopped, 4L), (r.Outcome, r.Frames)); } [Fact] public void ScriptSeedHeader_SeedsTheGame_CommandLineWins() { var g = new FakeGame(); var output = new List(); var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = "format 1\nmode player\nseed 7\nkey A" }), new Dictionary()), g, new ScriptRunOptions(), output.Add); runner.Run(1000); Assert.Equal(["seed 7"], g.RngImpl!.Calls); Assert.Equal(7UL, runner.Report!.Seed); var g2 = new FakeGame(); var runner2 = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = "format 1\nmode player\nseed 7\nkey A" }), new Dictionary()), g2, new ScriptRunOptions { Seed = 9 }, output.Add); runner2.Run(1000); Assert.Equal(["seed 9"], g2.RngImpl!.Calls); } }