From 211159b1c19e32cdf256ef771403b4b319d88928 Mon Sep 17 00:00:00 2001 From: Heimdall Date: Thu, 12 Nov 2026 12:07:46 +0000 Subject: [PATCH] tests: BridgeCliTests gate tests run on Linux (which, sh, sh shims for the .cmd gate wrappers) The dispatch-gate tests spawned where.exe and powershell and pointed the gate at the checked-in .cmd wrappers. On non-Windows they now resolve executables with which, run commands through sh, write a fake gh as an executable script, and stand in for git.cmd/gh.cmd with sh shims that exec the same Node proxies (git-proxy.mjs, gh-proxy.mjs). Windows behavior is unchanged. Co-Authored-By: Claude Sonnet 5.5 --- tests/Bifrost.Web.Tests/BridgeCliTests.cs | 134 +++++++++++++--------- 1 file changed, 79 insertions(+), 55 deletions(-) diff --git a/tests/Bifrost.Web.Tests/BridgeCliTests.cs b/tests/Bifrost.Web.Tests/BridgeCliTests.cs index a8efe6d..3b13d08 100644 --- a/tests/Bifrost.Web.Tests/BridgeCliTests.cs +++ b/tests/Bifrost.Web.Tests/BridgeCliTests.cs @@ -1061,18 +1061,16 @@ rl.on("line", (line) => { await RunGitAsync(["commit", "-m", "Initial commit"], workDir); await RunGitAsync(["remote", "add", "origin", remoteDir], workDir); - var blocked = await RunCommandAsync( - "powershell", - ["-NoProfile", "-Command", "git push -u origin main"], + var blocked = await RunShellAsync( + "git push -u origin main", workDir, BuildDispatchedGitHookEnv()); Assert.NotEqual(0, blocked.ExitCode); Assert.Contains("Bifrost blocked git push", blocked.Stderr, StringComparison.OrdinalIgnoreCase); - var blockedNoVerify = await RunCommandAsync( - "powershell", - ["-NoProfile", "-Command", "git push --no-verify -u origin main"], + var blockedNoVerify = await RunShellAsync( + "git push --no-verify -u origin main", workDir, BuildDispatchedGitGateEnv()); @@ -1154,19 +1152,13 @@ console.log("https://github.com/GameCult/Bifrost/pull/999"); try { - var fakeGhPath = Path.Combine(tempDir, "fake-gh.cmd"); - await File.WriteAllTextAsync(fakeGhPath, """ -@echo off -echo should-not-run -exit /b 0 -""", Encoding.UTF8); + var fakeGhPath = await WriteFakeGhAsync(tempDir, "should-not-run"); var environment = BuildDispatchedGitGateEnv(); environment["BIFROST_REAL_GH"] = fakeGhPath; - var blocked = await RunCommandAsync( - "powershell", - ["-NoProfile", "-Command", "gh pr comment 42 --body blocked"], + var blocked = await RunShellAsync( + "gh pr comment 42 --body blocked", RepoRoot, environment); @@ -1188,19 +1180,13 @@ exit /b 0 try { - var fakeGhPath = Path.Combine(tempDir, "fake-gh.cmd"); - await File.WriteAllTextAsync(fakeGhPath, """ -@echo off -echo should-not-run -exit /b 0 -""", Encoding.UTF8); + var fakeGhPath = await WriteFakeGhAsync(tempDir, "should-not-run"); var environment = BuildDispatchedGitGateEnv(); environment["BIFROST_REAL_GH"] = fakeGhPath; - var blocked = await RunCommandAsync( - "powershell", - ["-NoProfile", "-Command", "gh -R GameCult/Bifrost pr comment 42 --body blocked"], + var blocked = await RunShellAsync( + "gh -R GameCult/Bifrost pr comment 42 --body blocked", RepoRoot, environment); @@ -1222,19 +1208,13 @@ exit /b 0 try { - var fakeGhPath = Path.Combine(tempDir, "fake-gh.cmd"); - await File.WriteAllTextAsync(fakeGhPath, """ -@echo off -echo should-not-run -exit /b 0 -""", Encoding.UTF8); + var fakeGhPath = await WriteFakeGhAsync(tempDir, "should-not-run"); var environment = BuildDispatchedGitGateEnv(); environment["BIFROST_REAL_GH"] = fakeGhPath; - var blocked = await RunCommandAsync( - "powershell", - ["-NoProfile", "-Command", "gh workflow run ci.yml"], + var blocked = await RunShellAsync( + "gh workflow run ci.yml", RepoRoot, environment); @@ -1256,19 +1236,13 @@ exit /b 0 try { - var fakeGhPath = Path.Combine(tempDir, "fake-gh.cmd"); - await File.WriteAllTextAsync(fakeGhPath, """ -@echo off -echo read-only-ok -exit /b 0 -""", Encoding.UTF8); + var fakeGhPath = await WriteFakeGhAsync(tempDir, "read-only-ok"); var environment = BuildDispatchedGitGateEnv(); environment["BIFROST_REAL_GH"] = fakeGhPath; - var allowed = await RunCommandAsync( - "powershell", - ["-NoProfile", "-Command", "gh -R GameCult/Bifrost pr view 42"], + var allowed = await RunShellAsync( + "gh -R GameCult/Bifrost pr view 42", RepoRoot, environment); @@ -1449,27 +1423,77 @@ import('./tools/bifrost-crossing-documents.mjs') private static Dictionary BuildDispatchedGitHookEnv() { var hooksPath = Path.Combine(RepoRoot, "tools", "git-hooks"); + var nodePath = ResolveExecutable("node"); var gatePath = Path.Combine(RepoRoot, "tools", "git-gate"); - var path = Environment.GetEnvironmentVariable("Path") - ?? Environment.GetEnvironmentVariable("PATH") - ?? string.Empty; - var nextPath = string.IsNullOrWhiteSpace(path) - ? gatePath - : $"{gatePath};{path}"; - return new Dictionary + var environment = new Dictionary { ["BIFROST_ENFORCE_GITHUB_GATE"] = "true", - ["BIFROST_GIT_EXECUTABLE"] = Path.Combine(gatePath, "git.cmd"), - ["BIFROST_GH_EXECUTABLE"] = Path.Combine(gatePath, "gh.cmd"), - ["BIFROST_NODE_EXECUTABLE"] = ResolveExecutable("node"), + ["BIFROST_NODE_EXECUTABLE"] = nodePath, ["GIT_CONFIG_COUNT"] = "1", ["GIT_CONFIG_KEY_0"] = "core.hooksPath", ["GIT_CONFIG_VALUE_0"] = hooksPath, - ["PATH"] = nextPath, - ["Path"] = nextPath, }; + + var path = Environment.GetEnvironmentVariable("Path") + ?? Environment.GetEnvironmentVariable("PATH") + ?? string.Empty; + + if (OperatingSystem.IsWindows()) + { + // The checked-in gate wrappers are .cmd files that PATH lookup finds as `git` and `gh`. + var nextPath = string.IsNullOrWhiteSpace(path) ? gatePath : $"{gatePath};{path}"; + environment["BIFROST_GIT_EXECUTABLE"] = Path.Combine(gatePath, "git.cmd"); + environment["BIFROST_GH_EXECUTABLE"] = Path.Combine(gatePath, "gh.cmd"); + environment["PATH"] = nextPath; + environment["Path"] = nextPath; + return environment; + } + + // Non-Windows has no .cmd wrappers: stand in for them with sh shims that run the same + // Node proxies, placed first on PATH. The proxies get the real git by explicit path so + // the shim cannot resolve itself. + var shimDir = Path.Combine(Path.GetTempPath(), $"bifrost-gate-shims-{Guid.NewGuid():N}"); + Directory.CreateDirectory(shimDir); + var gitShim = WriteShim(shimDir, "git", nodePath, Path.Combine(gatePath, "git-proxy.mjs")); + var ghShim = WriteShim(shimDir, "gh", nodePath, Path.Combine(gatePath, "gh-proxy.mjs")); + environment["BIFROST_REAL_GIT"] = ResolveExecutable("git"); + environment["BIFROST_GIT_EXECUTABLE"] = gitShim; + environment["BIFROST_GH_EXECUTABLE"] = ghShim; + environment["PATH"] = string.IsNullOrEmpty(path) ? shimDir : $"{shimDir}{Path.PathSeparator}{path}"; + return environment; } + private static string WriteShim(string directory, string name, string nodePath, string script) + { + var shim = Path.Combine(directory, name); + File.WriteAllText(shim, $"#!/bin/sh\nexec '{nodePath}' '{script}' \"$@\"\n", new UTF8Encoding(false)); + File.SetUnixFileMode(shim, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + return shim; + } + + private static async Task WriteFakeGhAsync(string directory, string output) + { + if (OperatingSystem.IsWindows()) + { + var cmd = Path.Combine(directory, "fake-gh.cmd"); + await File.WriteAllTextAsync(cmd, $"@echo off\r\necho {output}\r\nexit /b 0\r\n", Encoding.UTF8); + return cmd; + } + + var script = Path.Combine(directory, "fake-gh.sh"); + await File.WriteAllTextAsync(script, $"#!/bin/sh\necho {output}\nexit 0\n", new UTF8Encoding(false)); + File.SetUnixFileMode(script, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + return script; + } + + private static Task RunShellAsync( + string command, + string workingDirectory, + IReadOnlyDictionary environment) + => OperatingSystem.IsWindows() + ? RunCommandAsync("powershell", ["-NoProfile", "-Command", command], workingDirectory, environment) + : RunCommandAsync("sh", ["-c", command], workingDirectory, environment); + private static Dictionary BuildDispatchedGitGateEnv() => BuildDispatchedGitHookEnv(); private static string ResolveExecutable(string command) @@ -1478,7 +1502,7 @@ import('./tools/bifrost-crossing-documents.mjs') { StartInfo = new ProcessStartInfo { - FileName = "where.exe", + FileName = OperatingSystem.IsWindows() ? "where.exe" : "which", WorkingDirectory = RepoRoot, RedirectStandardOutput = true, RedirectStandardError = true,