diff --git a/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.ps1 b/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.ps1 index 3cc0db7..97f3682 100644 --- a/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.ps1 +++ b/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.ps1 @@ -22,6 +22,21 @@ if ($env:GO_MODERN_GUIDELINES_DEV) { exit $LASTEXITCODE } +function Assert-BinaryVersion([string] $path, [string] $label) { + $actualVersion = "" + $exitCode = 1 + try { + $actualVersion = ((& $path --version 2>$null) -join "`n").Trim() + $exitCode = $LASTEXITCODE + } catch { + } + if ($exitCode -ne 0 -or $actualVersion -ne $cliVersion) { + $reportedVersion = if ($actualVersion) { $actualVersion } else { "unknown version" } + Write-Error "go-modern-guidelines: $label binary reports $reportedVersion, want $cliVersion" + exit 1 + } +} + $installDir = Join-Path $cacheRoot $cliVersion $binaryPath = Join-Path $installDir $binaryName @@ -31,8 +46,8 @@ if (-not (Test-Path -LiteralPath $binaryPath -PathType Leaf)) { exit 1 } - $tmpDir = "$installDir.tmp.$PID" - Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path $cacheRoot -Force | Out-Null + $tmpDir = Join-Path $cacheRoot "$cliVersion.tmp.$([System.IO.Path]::GetRandomFileName())" New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null Write-Host "go-modern-guidelines: installing $modulePath@$cliVersion into $installDir" -ForegroundColor DarkGray @@ -63,22 +78,10 @@ if (-not (Test-Path -LiteralPath $binaryPath -PathType Leaf)) { exit 1 } - $actualVersion = "" - try { - $actualVersion = (& $tmpBinary --version 2>$null) - } catch { - $actualVersion = "" - } - if ($actualVersion -ne $cliVersion) { - if (-not $actualVersion) { - $actualVersion = "unknown version" - } - Write-Error "go-modern-guidelines: installed $actualVersion, want $cliVersion" - exit 1 - } + Assert-BinaryVersion $tmpBinary "installed" New-Item -ItemType Directory -Path $installDir -Force | Out-Null - $stagedBinary = "$binaryPath.tmp.$PID" + $stagedBinary = "$binaryPath.tmp.$([System.IO.Path]::GetRandomFileName())" Move-Item -LiteralPath $tmpBinary -Destination $stagedBinary -Force Move-Item -LiteralPath $stagedBinary -Destination $binaryPath -Force Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue @@ -87,5 +90,6 @@ if (-not (Test-Path -LiteralPath $binaryPath -PathType Leaf)) { } } +Assert-BinaryVersion $binaryPath "cached" & $binaryPath @args exit $LASTEXITCODE diff --git a/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.sh b/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.sh index a9c3a98..921d9c9 100755 --- a/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.sh +++ b/packages/mtmharness/resources/go-modern-guidelines/scripts/run-tool.sh @@ -1,5 +1,6 @@ #!/usr/bin/env sh set -eu +umask 077 script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" @@ -35,9 +36,8 @@ if [ ! -x "${binary_path}" ]; then exit 1 fi - tmp_dir="${install_dir}.tmp.$$" - rm -rf "${tmp_dir}" - mkdir -p "${tmp_dir}" + mkdir -p "${cache_root}" + tmp_dir="$(mktemp -d "${install_dir}.tmp.XXXXXX")" trap 'rm -rf "${tmp_dir}"' EXIT HUP INT TERM echo "go-modern-guidelines: installing ${module_path}@${cli_version} into ${install_dir}" >&2 @@ -60,9 +60,16 @@ if [ ! -x "${binary_path}" ]; then fi mkdir -p "${install_dir}" - mv "${tmp_binary}" "${binary_path}.tmp.$$" - mv "${binary_path}.tmp.$$" "${binary_path}" + staged_binary="$(mktemp "${binary_path}.tmp.XXXXXX")" + mv "${tmp_binary}" "${staged_binary}" + mv "${staged_binary}" "${binary_path}" rm -rf "${tmp_dir}" fi +actual_version="$("${binary_path}" --version 2>/dev/null || true)" +if [ "${actual_version}" != "${cli_version}" ]; then + echo "go-modern-guidelines: cached binary reports ${actual_version:-unknown version}, want ${cli_version}" >&2 + exit 1 +fi + exec "${binary_path}" "$@" diff --git a/packages/mtmharness/src/features/coding/modern-go.ts b/packages/mtmharness/src/features/coding/modern-go.ts index 1c56c26..6ac7680 100644 --- a/packages/mtmharness/src/features/coding/modern-go.ts +++ b/packages/mtmharness/src/features/coding/modern-go.ts @@ -8,7 +8,7 @@ export const MODERN_GO_RESOURCE_BASE = resolve( "../resources/go-modern-guidelines", ); -// Adapted from JetBrains/go-modern-guidelines v1.1.1; command paths are resolved for mtmharness. +// Adapted from JetBrains/go-modern-guidelines CLI v0.1.1; command paths are resolved for mtmharness. const SKILL_CONTENT_TEMPLATE = [ "# Modern Go Guidelines CLI", "", @@ -65,10 +65,14 @@ const SKILL_CONTENT_TEMPLATE = [ "Do not call `explain` without guideline IDs. Use `list` first to discover the short guideline list for the target Go version, then call `explain` for the specific returned IDs.", ].join("\n"); +function powerShellLiteral(value: string): string { + return "'" + value.replaceAll("'", "''") + "'"; +} + function content(command: string): string { const configured = command.trim(); const unixCommand = configured || "sh \"" + join(MODERN_GO_RESOURCE_BASE, "scripts", "run-tool.sh") + "\""; - const windowsCommand = configured || "& '" + join(MODERN_GO_RESOURCE_BASE, "scripts", "run-tool.ps1") + "'"; + const windowsCommand = configured || "& " + powerShellLiteral(join(MODERN_GO_RESOURCE_BASE, "scripts", "run-tool.ps1")); return SKILL_CONTENT_TEMPLATE .replaceAll("__UNIX_COMMAND__", unixCommand) .replaceAll("__WINDOWS_COMMAND__", windowsCommand); diff --git a/packages/mtmharness/tests/coding-runtime.test.mjs b/packages/mtmharness/tests/coding-runtime.test.mjs index b0c6189..09fa63d 100644 --- a/packages/mtmharness/tests/coding-runtime.test.mjs +++ b/packages/mtmharness/tests/coding-runtime.test.mjs @@ -1,6 +1,9 @@ import assert from "node:assert/strict"; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; import { deflateRawSync, gzipSync } from "node:zlib"; -import { join } from "node:path"; import test from "node:test"; import { ensureRuntime, @@ -42,6 +45,54 @@ test("explicit command remains an opt-in override", () => { }); }); +test("lazily installs and validates the Modern Go wrapper cache", () => { + const root = mkdtempSync(join(tmpdir(), "mtmharness-modern-go-")); + try { + const binDir = join(root, "bin"); + const cacheDir = join(root, "cache"); + const installCount = join(root, "install-count"); + const wrapper = resolve(import.meta.dirname, "../resources/go-modern-guidelines/scripts/run-tool.sh"); + const version = readFileSync(resolve(import.meta.dirname, "../resources/go-modern-guidelines/scripts/VERSION"), "utf8").trim(); + const fakeGo = join(binDir, "go"); + mkdirSync(binDir, { recursive: true }); + writeFileSync(fakeGo, `#!/bin/sh +set -eu +printf 'install\n' >> "$FAKE_GO_INSTALLS" +printf '%s\n' '#!/bin/sh' 'if [ "$1" = "--version" ]; then printf "%s\\n" "$FAKE_VERSION"; else printf "fake:%s\\n" "$*"; fi' > "$GOBIN/go-modern-guidelines" +chmod +x "$GOBIN/go-modern-guidelines" +`); + chmodSync(fakeGo, 0o755); + const env = { + ...process.env, + FAKE_GO_INSTALLS: installCount, + FAKE_VERSION: version, + HOME: root, + PATH: binDir + ":" + (process.env.PATH ?? "/usr/bin:/bin"), + XDG_CACHE_HOME: cacheDir, + }; + const run = (...args) => spawnSync("/bin/sh", [wrapper, ...args], { env, encoding: "utf8" }); + + let result = run("list", "--go-version", "1.27"); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout.trim(), "fake:list --go-version 1.27"); + assert.equal(readFileSync(installCount, "utf8"), "install\n"); + + result = run("list", "--go-version", "1.27"); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout.trim(), "fake:list --go-version 1.27"); + assert.equal(readFileSync(installCount, "utf8"), "install\n"); + + const cached = join(cacheDir, "go-modern-guidelines", version, "go-modern-guidelines"); + writeFileSync(cached, "#!/bin/sh\nprintf 'wrong\\n'\n"); + chmodSync(cached, 0o755); + result = run("list", "--go-version", "1.27"); + assert.equal(result.status, 1); + assert.match(result.stderr, /cached binary reports wrong/); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("resolves the native binary for the long-lived MCP connection", async () => { const specs = []; const fake = {