using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using w4c_workflows.Services; using Xunit; namespace w4c_workflows.Tests; /// /// §4/S4: one async git entry point with a bounded wait, both pipes drained and /// a short-TTL state cache. These tests cover the porcelain-v2 parser (pure), /// the failure path, and — when git is available — a real working tree. /// public class GitRunnerTests { private static GitRunner Runner(int stateCacheSeconds = 0) => new( new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["WorkflowSource:GitStateCacheSeconds"] = stateCacheSeconds.ToString(), }) .Build(), NullLogger.Instance); [Fact] public void ParseState_clean_repo_reports_head_and_not_dirty() { var stdout = string.Join('\n', "# branch.oid 0123456789abcdef", "# branch.head main", "# branch.upstream origin/main", "# branch.ab +0 -0") + "\n"; var state = GitRunner.ParseState(new GitResult(0, stdout, "", TimedOut: false)); Assert.Equal("0123456789abcdef", state.HeadSha); Assert.False(state.Dirty); } [Theory] [InlineData("1 .M N... 100644 100644 100644 aaa bbb file.txt")] [InlineData("? new-file.txt")] public void ParseState_any_content_line_marks_dirty(string line) { var stdout = "# branch.oid abc123\n# branch.head main\n" + line + "\n"; var state = GitRunner.ParseState(new GitResult(0, stdout, "", TimedOut: false)); Assert.Equal("abc123", state.HeadSha); Assert.True(state.Dirty); } [Fact] public void ParseState_unborn_branch_has_no_head() { var state = GitRunner.ParseState( new GitResult(0, "# branch.oid (initial)\n# branch.head main\n", "", TimedOut: false)); Assert.Null(state.HeadSha); Assert.False(state.Dirty); } [Theory] [InlineData(1)] [InlineData(128)] public void ParseState_failed_call_is_empty(int exitCode) { var state = GitRunner.ParseState(new GitResult(exitCode, "", "fatal: nope", TimedOut: false)); Assert.Null(state.HeadSha); Assert.False(state.Dirty); } [Fact] public async Task RunAsync_missing_working_directory_reports_failure_instead_of_throwing() { var missing = Path.Combine(Path.GetTempPath(), "w4c-gitrunner-" + Guid.NewGuid().ToString("N")); var result = await Runner().RunAsync(missing, new[] { "rev-parse", "HEAD" }); Assert.False(result.Succeeded); Assert.Equal(-1, result.ExitCode); } [Fact] public async Task GetStateAsync_reads_head_and_caches_until_invalidated() { if (!GitAvailable()) return; // no git on PATH — parser coverage above still runs var dir = Directory.CreateTempSubdirectory("w4c-gitrunner-").FullName; try { await GitRaw(dir, "init", "-q"); await File.WriteAllTextAsync(Path.Combine(dir, "workflow.yaml"), "name: test\n"); await GitRaw(dir, "add", "workflow.yaml"); await GitRaw(dir, "commit", "-q", "-m", "initial"); // Caching enabled: the clean state is cached… var runner = Runner(stateCacheSeconds: 60); var first = await runner.GetStateAsync(dir); Assert.NotNull(first.HeadSha); Assert.False(first.Dirty); // …so an immediate re-read after a change still returns the cached state… await File.WriteAllTextAsync(Path.Combine(dir, "workflow.yaml"), "name: changed\n"); var cached = await runner.GetStateAsync(dir); Assert.False(cached.Dirty); Assert.Equal(first.HeadSha, cached.HeadSha); // …until the cache is dropped, when the change is observed. runner.InvalidateState(dir); var fresh = await runner.GetStateAsync(dir); Assert.True(fresh.Dirty); } finally { TryDelete(dir); } } private static bool GitAvailable() { try { using var p = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("git", "--version") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }); if (p == null) return false; p.WaitForExit(5000); return p.ExitCode == 0; } catch { return false; } } private static async Task GitRaw(string workDir, params string[] args) { var psi = new System.Diagnostics.ProcessStartInfo("git") { WorkingDirectory = workDir, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; foreach (var a in args) psi.ArgumentList.Add(a); psi.Environment["GIT_AUTHOR_NAME"] = "w4c test"; psi.Environment["GIT_AUTHOR_EMAIL"] = "test@w4c.local"; psi.Environment["GIT_COMMITTER_NAME"] = "w4c test"; psi.Environment["GIT_COMMITTER_EMAIL"] = "test@w4c.local"; using var p = System.Diagnostics.Process.Start(psi)!; await p.WaitForExitAsync(); Assert.Equal(0, p.ExitCode); } private static void TryDelete(string dir) { try { Directory.Delete(dir, recursive: true); } catch { /* best effort */ } } }