From ff24ea6dbce11826a61c9a7b6e5bce999bbea8a7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:07:19 +0000 Subject: [PATCH 1/4] Add Go local runner --- src/mcp/tools/runner-tools.ts | 2 +- src/runner/runner.ts | 5 +- src/runner/subprocess-runner.ts | 19 ++--- tests/e2e/runner.test.ts | 78 +++++++++++++++++-- .../runner-tools-integration.test.ts | 8 +- tests/runner/subprocess-runner.test.ts | 59 +++++++++++++- 6 files changed, 143 insertions(+), 28 deletions(-) diff --git a/src/mcp/tools/runner-tools.ts b/src/mcp/tools/runner-tools.ts index 2764568..123295f 100644 --- a/src/mcp/tools/runner-tools.ts +++ b/src/mcp/tools/runner-tools.ts @@ -52,7 +52,7 @@ export class RunnerToolRegistry extends ToolRegistry { "run_local_tests", { description: - "Runs the user's code locally in an isolated subprocess, captures stdout / stderr / exit code, and updates the session's lastLocalRunPassed flag. Use this in the inner loop instead of submit_solution — it costs no LeetCode submission and turns around in seconds. The agent is responsible for including test invocations (e.g. `print(Solution().twoSum([2,7,11,15], 9))`) in the code passed in. Phase 4a ships python3; go and java land in Phase 4b/4c.", + "Runs the user's code locally in an isolated subprocess, captures stdout / stderr / exit code, and updates the session's lastLocalRunPassed flag. Use this in the inner loop instead of submit_solution — it costs no LeetCode submission and turns around in seconds. The agent is responsible for including test invocations (e.g. `print(Solution().twoSum([2,7,11,15], 9))`) in the code passed in. Currently runnable: python3 and go; java lands in Phase 4c.", inputSchema: { titleSlug: z .string() diff --git a/src/runner/runner.ts b/src/runner/runner.ts index addb32d..80136e3 100644 --- a/src/runner/runner.ts +++ b/src/runner/runner.ts @@ -33,7 +33,7 @@ export const SUPPORTED_LANGUAGES: readonly RunnerLanguage[] = [ /** * The languages this build of the runner has *implemented*. Phase 4a - * ships `python3` only. Phase 4b/4c grow this list. + * shipped `python3`; Phase 4b adds `go`; Phase 4c adds `java`. * * Kept distinct from `SUPPORTED_LANGUAGES` so the wire-level * `RUNNER_NOT_IMPLEMENTED_FOR_LANGUAGE` error has a single source of @@ -41,5 +41,6 @@ export const SUPPORTED_LANGUAGES: readonly RunnerLanguage[] = [ * "coming soon" language. */ export const IMPLEMENTED_LANGUAGES: readonly RunnerLanguage[] = [ - "python3" + "python3", + "go" ] as const; diff --git a/src/runner/subprocess-runner.ts b/src/runner/subprocess-runner.ts index c816206..39db7f6 100644 --- a/src/runner/subprocess-runner.ts +++ b/src/runner/subprocess-runner.ts @@ -1,7 +1,7 @@ /** * Plain-subprocess `LocalRunner` implementation. * - * Per-language registry (currently `python3`) describes how to: + * Per-language registry (currently `python3` and `go`) describes how to: * - probe whether the runtime is available on PATH * - spawn the runtime against a source file written to the run's * temp dir @@ -59,8 +59,7 @@ interface LanguageSpec { probe: { cmd: string; args: string[] }; /** * Build the spawn args given the path of the source file we wrote - * for this run. Compiled languages (Go, Java) will hook in extra - * compile steps via subclassing later. + * for this run. */ buildArgs(sourcePath: string): { cmd: string; args: string[] }; } @@ -74,18 +73,16 @@ const LANGUAGES: Record = { args: [sourcePath] }) }, - // Phase 4b/4c stubs — present in the registry so the type system + // Phase 4c stub — present in the registry so the type system // requires they stay in sync with `RunnerLanguage`. The runner - // refuses to use these until we actually wire harnesses. + // refuses to use it until we actually wire the harness. go: { extension: "go", probe: { cmd: "go", args: ["version"] }, - buildArgs: () => { - throw new LeetCodeError( - ErrorCode.RUNNER_NOT_IMPLEMENTED_FOR_LANGUAGE, - "Go runner ships in Phase 4b" - ); - } + buildArgs: (sourcePath) => ({ + cmd: "go", + args: ["run", sourcePath] + }) }, java: { extension: "java", diff --git a/tests/e2e/runner.test.ts b/tests/e2e/runner.test.ts index 05ac940..6a10e7b 100644 --- a/tests/e2e/runner.test.ts +++ b/tests/e2e/runner.test.ts @@ -1,10 +1,10 @@ /** * Local-runner e2e: spawn the real `build/index.js`, drive * `runner_doctor` and `run_local_tests` over the wire, and assert the - * runner actually executes Python on the host. + * runner actually executes available runtimes on the host. * - * Skipped automatically on hosts without `python3` so the suite stays - * portable; the project's CI image has it. + * Runtime-specific cases skip automatically when that runtime is absent, + * so the suite stays portable. */ import { execFileSync } from "node:child_process"; import { afterEach, describe, expect, it } from "vitest"; @@ -28,6 +28,11 @@ const TWO_SUM_PROBLEM = { lang: "Python3", langSlug: "python3", code: "class Solution:\n def twoSum(self, nums, target):\n pass\n" + }, + { + lang: "Go", + langSlug: "go", + code: "package main\n\nfunc main() {}\n" } ], similarQuestions: "[]", @@ -56,6 +61,17 @@ function pythonAvailable(): boolean { const PYTHON_PRESENT = pythonAvailable(); +function goAvailable(): boolean { + try { + execFileSync("go", ["version"], { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +const GO_PRESENT = goAvailable(); + describe.skipIf(!PYTHON_PRESENT)("e2e: local runner (python3)", () => { let spawned: SpawnedServer | undefined; @@ -190,15 +206,15 @@ describe.skipIf(!PYTHON_PRESENT)("e2e: local runner (python3)", () => { await spawned.client.callTool({ name: "start_problem", - arguments: { titleSlug: "two-sum", language: "go" } + arguments: { titleSlug: "two-sum", language: "java" } }); const run = (await spawned.client.callTool({ name: "run_local_tests", arguments: { titleSlug: "two-sum", - language: "go", - code: "package main\nfunc main() {}" + language: "java", + code: "public class Solution {}" } })) as ToolTextResult; @@ -258,3 +274,53 @@ describe.skipIf(!PYTHON_PRESENT)("e2e: local runner (python3)", () => { expect(allowedPayload.code).not.toBe("LOCAL_TESTS_NOT_PASSED"); }); }); + +describe.skipIf(!GO_PRESENT)("e2e: local runner (go)", () => { + let spawned: SpawnedServer | undefined; + + afterEach(async () => { + if (spawned) { + await spawned.cleanup(); + spawned = undefined; + } + }); + + it("executes a passing Go program and updates the session", async () => { + spawned = await spawnServer({ fixture: FIXTURE }); + + await spawned.client.callTool({ + name: "start_problem", + arguments: { titleSlug: "two-sum", language: "go" } + }); + + const run = (await spawned.client.callTool({ + name: "run_local_tests", + arguments: { + titleSlug: "two-sum", + language: "go", + code: [ + "package main", + 'import "fmt"', + "func main() {", + ' fmt.Println("go ok")', + ' if 1 + 1 != 2 { panic("bad math") }', + "}" + ].join("\n") + } + })) as ToolTextResult; + + const payload = JSON.parse(run.content[0].text); + expect(payload.titleSlug).toBe("two-sum"); + expect(payload.result.passed).toBe(true); + expect(payload.result.exitCode).toBe(0); + expect(payload.result.stdout).toContain("go ok"); + + const state = (await spawned.client.callTool({ + name: "get_session_state", + arguments: { titleSlug: "two-sum" } + })) as ToolTextResult; + const sessionPayload = JSON.parse(state.content[0].text); + expect(sessionPayload.session.lastLocalRunPassed).toBe(true); + expect(sessionPayload.session.attempts).toBe(1); + }); +}); diff --git a/tests/integration/runner-tools-integration.test.ts b/tests/integration/runner-tools-integration.test.ts index 446eb86..280f51c 100644 --- a/tests/integration/runner-tools-integration.test.ts +++ b/tests/integration/runner-tools-integration.test.ts @@ -234,13 +234,13 @@ describe("Runner Tools Integration", () => { ); it( - "surfaces RUNNER_NOT_IMPLEMENTED_FOR_LANGUAGE thrown from the runner", + "surfaces RUNNER_NOT_IMPLEMENTED_FOR_LANGUAGE for still-unimplemented languages", async () => { await sessions.startOrResume({ slug: "two-sum" }); const broken = createFakeRunner({ runError: new LeetCodeError( ErrorCode.RUNNER_NOT_IMPLEMENTED_FOR_LANGUAGE, - "Go runner ships in Phase 4b" + "Java runner ships in Phase 4c" ) }); await testClient.cleanup(); @@ -257,8 +257,8 @@ describe("Runner Tools Integration", () => { name: "run_local_tests", arguments: { titleSlug: "two-sum", - language: "go", - code: "package main" + language: "java", + code: "public class Solution {}" } }); diff --git a/tests/runner/subprocess-runner.test.ts b/tests/runner/subprocess-runner.test.ts index 27969dc..b9df2ac 100644 --- a/tests/runner/subprocess-runner.test.ts +++ b/tests/runner/subprocess-runner.test.ts @@ -6,6 +6,7 @@ * availability; a missing python3 produces a `LANGUAGE_RUNTIME_NOT_FOUND` * which is its own first-class assertion. */ +import { execFileSync } from "node:child_process"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { __resetSandboxCacheForTest } from "../../src/runner/sandbox.js"; import { @@ -18,6 +19,17 @@ import { type RunnerLanguage } from "../../src/types/index.js"; +function runtimeAvailable(cmd: string, args: string[]): boolean { + try { + execFileSync(cmd, args, { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +const GO_PRESENT = runtimeAvailable("go", ["version"]); + describe("SubprocessRunner", () => { let runner: SubprocessRunner; @@ -44,7 +56,7 @@ describe("SubprocessRunner", () => { expect(typeof py?.available).toBe("boolean"); }); - it("reports go and java as supported languages even before they are implemented", async () => { + it("reports all supported local-runner languages", async () => { const caps = await runner.capabilities(); const langs = caps.languages.map((l) => l.language).sort(); expect(langs).toEqual(["go", "java", "python3"]); @@ -100,6 +112,45 @@ describe("SubprocessRunner", () => { expect(result.stderr).toContain("boom"); }); + it.skipIf(!GO_PRESENT)("executes a happy-path Go program", async () => { + const result = await runner.run({ + titleSlug: "two-sum", + language: "go", + code: [ + "package main", + 'import "fmt"', + "func main() {", + ' fmt.Println("hello from go")', + ' if 1 + 1 != 2 { panic("bad math") }', + "}" + ].join("\n") + }); + + expect(result.passed).toBe(true); + expect(result.exitCode).toBe(0); + expect(result.timedOut).toBe(false); + expect(result.stdout).toContain("hello from go"); + expect(result.stderr).toBe(""); + }); + + it.skipIf(GO_PRESENT)( + "reports LANGUAGE_RUNTIME_NOT_FOUND when Go is unavailable", + async () => { + await expect(async () => { + await runner.run({ + titleSlug: "two-sum", + language: "go", + code: "package main\nfunc main() {}" + }); + }).rejects.toSatisfy((error: unknown) => { + if (!isLeetCodeError(error)) { + return false; + } + return error.code === ErrorCode.LANGUAGE_RUNTIME_NOT_FOUND; + }); + } + ); + it("kills runaway processes after the timeout budget", async () => { const start = Date.now(); const result = await runner.run({ @@ -117,12 +168,12 @@ describe("SubprocessRunner", () => { expect(elapsed).toBeLessThan(2_500); }); - it("rejects unsupported languages with RUNNER_NOT_IMPLEMENTED_FOR_LANGUAGE", async () => { + it("rejects still-unimplemented languages with RUNNER_NOT_IMPLEMENTED_FOR_LANGUAGE", async () => { await expect(async () => { await runner.run({ titleSlug: "two-sum", - language: "go" as RunnerLanguage, - code: 'package main\nfunc main() { println("hi") }' + language: "java" as RunnerLanguage, + code: "public class Solution {}" }); }).rejects.toSatisfy((error: unknown) => { if (!isLeetCodeError(error)) { From 635aff7312f691415e1375ca3cace560ade09767 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:13:54 +0000 Subject: [PATCH 2/4] Stabilize Go runner caches --- src/mcp/tools/runner-tools.ts | 2 +- src/runner/sandbox.ts | 21 +++--- src/runner/subprocess-runner.ts | 76 ++++++++++++++++---- src/types/runner.ts | 7 +- tests/runner/sandbox.test.ts | 19 +++++ tests/runner/subprocess-runner.test.ts | 97 +++++++++++++++++++++----- 6 files changed, 179 insertions(+), 43 deletions(-) diff --git a/src/mcp/tools/runner-tools.ts b/src/mcp/tools/runner-tools.ts index 123295f..85aa808 100644 --- a/src/mcp/tools/runner-tools.ts +++ b/src/mcp/tools/runner-tools.ts @@ -78,7 +78,7 @@ export class RunnerToolRegistry extends ToolRegistry { .max(60_000) .optional() .describe( - "Optional wall-clock budget in milliseconds. Defaults to 5000." + "Optional wall-clock budget in milliseconds. Defaults are language-specific." ) } }, diff --git a/src/runner/sandbox.ts b/src/runner/sandbox.ts index 34ed322..49f4305 100644 --- a/src/runner/sandbox.ts +++ b/src/runner/sandbox.ts @@ -113,9 +113,11 @@ export async function detectSandbox(): Promise { export async function wrapWithSandbox( cmd: string, args: string[], - cwdAllowed: string + cwdAllowed: string, + writablePaths: string[] = [] ): Promise<{ cmd: string; args: string[]; kind: SandboxKind }> { const detected = await detectSandbox(); + const allowedWritePaths = [cwdAllowed, ...writablePaths]; if (detected.kind === "bwrap") { return { cmd: "bwrap", @@ -125,9 +127,7 @@ export async function wrapWithSandbox( "/", "--tmpfs", "/tmp", - "--bind", - cwdAllowed, - cwdAllowed, + ...allowedWritePaths.flatMap((path) => ["--bind", path, path]), "--proc", "/proc", "--dev", @@ -149,7 +149,7 @@ export async function wrapWithSandbox( "--noprofile", "--net=none", "--private-tmp", - `--whitelist=${cwdAllowed}`, + ...allowedWritePaths.map((path) => `--whitelist=${path}`), "--", cmd, ...args @@ -159,14 +159,19 @@ export async function wrapWithSandbox( } if (detected.kind === "sandbox-exec") { // Minimal sandbox-exec profile — deny by default, allow process - // primitives + reads everywhere + writes only under cwdAllowed. - const writableSubpath = escapeSandboxProfileString(cwdAllowed); + // primitives + reads everywhere + writes only under allowed paths. + const writableSubpaths = allowedWritePaths + .map( + (path) => + `(allow file-write* (subpath "${escapeSandboxProfileString(path)}"))` + ) + .join("\n"); const profile = `(version 1) (deny default) (allow process-fork) (allow process-exec) (allow file-read*) -(allow file-write* (subpath "${writableSubpath}")) +${writableSubpaths} (allow file-write* (regex #"^/dev/null$")) (allow file-write* (regex #"^/dev/dtracehelper$")) (allow sysctl-read) diff --git a/src/runner/subprocess-runner.ts b/src/runner/subprocess-runner.ts index 39db7f6..58585f9 100644 --- a/src/runner/subprocess-runner.ts +++ b/src/runner/subprocess-runner.ts @@ -10,10 +10,10 @@ * results are cached for the lifetime of the process. * * Safety nets every run gets, even with no OS sandbox: - * - per-process wall-clock timeout (default 5_000 ms; configurable - * per `RunInput`) - * - clean env (just PATH / HOME / LANG forwarded — secrets in the - * user's shell never leak in) + * - per-process wall-clock timeout (language-specific default; + * configurable per `RunInput`) + * - clean env (PATH / HOME / LANG plus language-specific cache dirs — + * secrets in the user's shell never leak in) * - cwd is a freshly-mkdtemp'd directory under the OS tmp; it is * removed after the run regardless of outcome * - stdout/stderr captured with a 1 MB ceiling; runaway output gets @@ -24,7 +24,7 @@ import { spawn, type ChildProcess } from "node:child_process"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { performance } from "node:perf_hooks"; @@ -50,6 +50,7 @@ const execFile = promisify(execFileCb); const MAX_OUTPUT_BYTES = 1_000_000; // 1 MB per stream const DEFAULT_TIMEOUT_MS = 5_000; +const GO_DEFAULT_TIMEOUT_MS = 20_000; const TRUNCATION_MARKER = "\n[...output truncated at 1 MB...]"; interface LanguageSpec { @@ -57,6 +58,8 @@ interface LanguageSpec { extension: string; /** `[binary, args]` to probe — exit code 0 means available. */ probe: { cmd: string; args: string[] }; + defaultTimeoutMs?: number; + prepareRuntime?(): Promise; /** * Build the spawn args given the path of the source file we wrote * for this run. @@ -64,6 +67,44 @@ interface LanguageSpec { buildArgs(sourcePath: string): { cmd: string; args: string[] }; } +interface RuntimeLayout { + env: Record; + writablePaths: string[]; +} + +interface GoRuntimePaths { + root: string; + buildCache: string; + moduleCache: string; +} + +let goRuntimePathsPromise: Promise | undefined; + +async function getGoRuntimePaths(): Promise { + if (!goRuntimePathsPromise) { + goRuntimePathsPromise = (async () => { + const root = await mkdtemp(join(tmpdir(), "leetcode-mcp-go-")); + const buildCache = join(root, "go-build"); + const moduleCache = join(root, "gomod"); + await mkdir(buildCache, { recursive: true }); + await mkdir(moduleCache, { recursive: true }); + return { root, buildCache, moduleCache }; + })(); + } + return goRuntimePathsPromise; +} + +async function prepareGoRuntime(): Promise { + const paths = await getGoRuntimePaths(); + return { + env: { + GOCACHE: paths.buildCache, + GOMODCACHE: paths.moduleCache + }, + writablePaths: [paths.root] + }; +} + const LANGUAGES: Record = { python3: { extension: "py", @@ -73,17 +114,19 @@ const LANGUAGES: Record = { args: [sourcePath] }) }, - // Phase 4c stub — present in the registry so the type system - // requires they stay in sync with `RunnerLanguage`. The runner - // refuses to use it until we actually wire the harness. go: { extension: "go", probe: { cmd: "go", args: ["version"] }, + defaultTimeoutMs: GO_DEFAULT_TIMEOUT_MS, + prepareRuntime: prepareGoRuntime, buildArgs: (sourcePath) => ({ cmd: "go", args: ["run", sourcePath] }) }, + // Phase 4c stub — present in the registry so the type system + // requires it stays in sync with `RunnerLanguage`. The runner + // refuses to use it until we actually wire the harness. java: { extension: "java", probe: { cmd: "java", args: ["-version"] }, @@ -220,7 +263,12 @@ export class SubprocessRunner implements LocalRunner { } const spec = LANGUAGES[input.language]; - const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const timeoutMs = + input.timeoutMs ?? spec.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS; + const runtimeLayout = (await spec.prepareRuntime?.()) ?? { + env: {}, + writablePaths: [] + }; const workDir = await mkdtemp(join(tmpdir(), "leetcode-mcp-run-")); const sourcePath = join(workDir, `solution.${spec.extension}`); @@ -230,7 +278,8 @@ export class SubprocessRunner implements LocalRunner { const wrapped = await wrapWithSandbox( baseArgs.cmd, baseArgs.args, - workDir + workDir, + runtimeLayout.writablePaths ); return await this.spawnAndCapture({ @@ -238,7 +287,8 @@ export class SubprocessRunner implements LocalRunner { args: wrapped.args, cwd: workDir, timeoutMs, - sandbox: wrapped.kind + sandbox: wrapped.kind, + env: runtimeLayout.env }); } finally { await rm(workDir, { recursive: true, force: true }).catch( @@ -258,6 +308,7 @@ export class SubprocessRunner implements LocalRunner { cwd: string; timeoutMs: number; sandbox: SandboxKind; + env: Record; }): Promise { return new Promise((resolve) => { const start = performance.now(); @@ -266,7 +317,8 @@ export class SubprocessRunner implements LocalRunner { env: { PATH: process.env.PATH ?? "", HOME: options.cwd, - LANG: process.env.LANG ?? "C.UTF-8" + LANG: process.env.LANG ?? "C.UTF-8", + ...options.env }, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] diff --git a/src/types/runner.ts b/src/types/runner.ts index 8f3078c..796db43 100644 --- a/src/types/runner.ts +++ b/src/types/runner.ts @@ -47,9 +47,10 @@ export interface RunInput { */ code: string; /** - * Wall-clock budget in milliseconds. Defaults to 5_000 if omitted. - * The runner kills the subprocess when this elapses and returns - * `timedOut: true` with whatever partial output was captured. + * Wall-clock budget in milliseconds. If omitted, the runner chooses + * a language-specific default. The runner kills the subprocess when + * this elapses and returns `timedOut: true` with whatever partial + * output was captured. */ timeoutMs?: number; } diff --git a/tests/runner/sandbox.test.ts b/tests/runner/sandbox.test.ts index 34c9308..d6e42b0 100644 --- a/tests/runner/sandbox.test.ts +++ b/tests/runner/sandbox.test.ts @@ -25,6 +25,25 @@ describe("sandbox wrapping", () => { ); }); + it("allows additional writable paths for runtime caches", async () => { + __setSandboxCacheForTest({ kind: "bwrap" }); + + const wrapped = await wrapWithSandbox( + "go", + ["run", "solution.go"], + "/tmp/leetcode-mcp-run-work", + ["/tmp/leetcode-mcp-go-cache"] + ); + + expect(wrapped.args).toEqual( + expect.arrayContaining([ + "--bind", + "/tmp/leetcode-mcp-run-work", + "/tmp/leetcode-mcp-go-cache" + ]) + ); + }); + it("rejects sandbox-exec subpaths containing newlines", async () => { __setSandboxCacheForTest({ kind: "sandbox-exec" }); diff --git a/tests/runner/subprocess-runner.test.ts b/tests/runner/subprocess-runner.test.ts index b9df2ac..3f7ac0d 100644 --- a/tests/runner/subprocess-runner.test.ts +++ b/tests/runner/subprocess-runner.test.ts @@ -7,8 +7,14 @@ * which is its own first-class assertion. */ import { execFileSync } from "node:child_process"; +import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { delimiter, join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { __resetSandboxCacheForTest } from "../../src/runner/sandbox.js"; +import { + __resetSandboxCacheForTest, + __setSandboxCacheForTest +} from "../../src/runner/sandbox.js"; import { SubprocessRunner, __resetProbeCacheForTest @@ -112,25 +118,78 @@ describe("SubprocessRunner", () => { expect(result.stderr).toContain("boom"); }); - it.skipIf(!GO_PRESENT)("executes a happy-path Go program", async () => { - const result = await runner.run({ - titleSlug: "two-sum", - language: "go", - code: [ - "package main", - 'import "fmt"', - "func main() {", - ' fmt.Println("hello from go")', - ' if 1 + 1 != 2 { panic("bad math") }', - "}" - ].join("\n") - }); + it.skipIf(!GO_PRESENT)( + "executes a happy-path Go program", + async () => { + const result = await runner.run({ + titleSlug: "two-sum", + language: "go", + code: [ + "package main", + 'import "fmt"', + "func main() {", + ' fmt.Println("hello from go")', + ' if 1 + 1 != 2 { panic("bad math") }', + "}" + ].join("\n") + }); - expect(result.passed).toBe(true); - expect(result.exitCode).toBe(0); - expect(result.timedOut).toBe(false); - expect(result.stdout).toContain("hello from go"); - expect(result.stderr).toBe(""); + expect(result.passed).toBe(true); + expect(result.exitCode).toBe(0); + expect(result.timedOut).toBe(false); + expect(result.stdout).toContain("hello from go"); + expect(result.stderr).toBe(""); + }, + 30_000 + ); + + it("reuses a stable Go build cache across runs", async () => { + __setSandboxCacheForTest({ kind: "none" }); + const fakeBin = await mkdtemp(join(tmpdir(), "fake-go-bin-")); + const fakeGo = join(fakeBin, "go"); + const originalPath = process.env.PATH; + const script = [ + "#!/usr/bin/env node", + 'if (process.argv[2] === "version") {', + ' console.log("go version go1.test linux/amd64");', + "} else {", + " console.log(JSON.stringify({", + " home: process.env.HOME,", + " gocache: process.env.GOCACHE,", + " gomodcache: process.env.GOMODCACHE", + " }));", + "}" + ].join("\n"); + + try { + await writeFile(fakeGo, script, "utf-8"); + await chmod(fakeGo, 0o755); + process.env.PATH = `${fakeBin}${delimiter}${originalPath ?? ""}`; + __resetProbeCacheForTest(); + + const first = await runner.run({ + titleSlug: "two-sum", + language: "go", + code: "package main\nfunc main() {}" + }); + const second = await runner.run({ + titleSlug: "two-sum", + language: "go", + code: "package main\nfunc main() {}" + }); + + const firstEnv = JSON.parse(first.stdout); + const secondEnv = JSON.parse(second.stdout); + expect(firstEnv.home).not.toBe(secondEnv.home); + expect(firstEnv.gocache).toBe(secondEnv.gocache); + expect(firstEnv.gomodcache).toBe(secondEnv.gomodcache); + expect(firstEnv.gocache).toContain("leetcode-mcp-go-"); + expect(firstEnv.gomodcache).toContain("leetcode-mcp-go-"); + } finally { + process.env.PATH = originalPath; + __resetProbeCacheForTest(); + await rm(fakeBin, { recursive: true, force: true }); + } }); it.skipIf(GO_PRESENT)( From a734c6fb3aee5bf2aa7ab8fc393ba6ede8372326 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:14:34 +0000 Subject: [PATCH 3/4] Set Go runner test timeout --- tests/runner/subprocess-runner.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/runner/subprocess-runner.test.ts b/tests/runner/subprocess-runner.test.ts index 3f7ac0d..3855152 100644 --- a/tests/runner/subprocess-runner.test.ts +++ b/tests/runner/subprocess-runner.test.ts @@ -124,6 +124,7 @@ describe("SubprocessRunner", () => { const result = await runner.run({ titleSlug: "two-sum", language: "go", + timeoutMs: 20_000, code: [ "package main", 'import "fmt"', From c84078604c9d6c2be2c053aa5cf86293c3e75875 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:24:50 +0000 Subject: [PATCH 4/4] Reset Go cache init after transient failure --- src/runner/subprocess-runner.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/runner/subprocess-runner.ts b/src/runner/subprocess-runner.ts index 58585f9..c6e37c4 100644 --- a/src/runner/subprocess-runner.ts +++ b/src/runner/subprocess-runner.ts @@ -82,7 +82,7 @@ let goRuntimePathsPromise: Promise | undefined; async function getGoRuntimePaths(): Promise { if (!goRuntimePathsPromise) { - goRuntimePathsPromise = (async () => { + const attempt = (async () => { const root = await mkdtemp(join(tmpdir(), "leetcode-mcp-go-")); const buildCache = join(root, "go-build"); const moduleCache = join(root, "gomod"); @@ -90,6 +90,10 @@ async function getGoRuntimePaths(): Promise { await mkdir(moduleCache, { recursive: true }); return { root, buildCache, moduleCache }; })(); + goRuntimePathsPromise = attempt.catch((error) => { + goRuntimePathsPromise = undefined; + throw error; + }); } return goRuntimePathsPromise; }