Skip to content

Commit 445f81b

Browse files
committed
Fix sandbox git fetch stderr handling
1 parent c82d7e0 commit 445f81b

4 files changed

Lines changed: 66 additions & 17 deletions

File tree

packages/sandbox/git.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { describe, expect, test } from "bun:test";
22
import type { ExecResult, Sandbox } from "./interface";
33
import { syncToRemotePreservingChanges } from "./git";
44

5+
const fetchFeatureCommand =
6+
"GIT_TERMINAL_PROMPT=0 git fetch --force origin feature:refs/remotes/origin/feature";
7+
58
function result(params: Partial<ExecResult> = {}): ExecResult {
69
return {
710
success: true,
@@ -56,7 +59,7 @@ describe("syncToRemotePreservingChanges", () => {
5659
await syncToRemotePreservingChanges(sandbox, "feature");
5760

5861
expect(sandbox.commands).toEqual([
59-
"git fetch origin feature:refs/remotes/origin/feature",
62+
fetchFeatureCommand,
6063
"git status --porcelain",
6164
"git rev-parse HEAD",
6265
"git stash push --include-untracked -m open-agents-pre-commit-sync",
@@ -77,9 +80,7 @@ describe("syncToRemotePreservingChanges", () => {
7780

7881
await syncToRemotePreservingChanges(sandbox, "feature");
7982

80-
expect(sandbox.commands).toEqual([
81-
"git fetch origin feature:refs/remotes/origin/feature",
82-
]);
83+
expect(sandbox.commands).toEqual([fetchFeatureCommand]);
8384
});
8485

8586
test("rolls back and restores local changes when stash restore conflicts after sync", async () => {
@@ -107,7 +108,7 @@ describe("syncToRemotePreservingChanges", () => {
107108
);
108109

109110
expect(sandbox.commands).toEqual([
110-
"git fetch origin feature:refs/remotes/origin/feature",
111+
fetchFeatureCommand,
111112
"git status --porcelain",
112113
"git rev-parse HEAD",
113114
"git stash push --include-untracked -m open-agents-pre-commit-sync",

packages/sandbox/git.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ async function fetchRemoteBranch(
5151
): Promise<"fetched" | "missing"> {
5252
const fetchResult = await exec(
5353
sandbox,
54-
`git fetch origin ${branch}:refs/remotes/origin/${branch}`,
54+
`GIT_TERMINAL_PROMPT=0 git fetch --force origin ${branch}:refs/remotes/origin/${branch}`,
5555
30000,
5656
);
5757

packages/sandbox/vercel/sandbox.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type MockRunCommandResult = {
1111
exitCode?: number;
1212
cmdId: string;
1313
stdout: () => Promise<string>;
14-
stderr?: () => Promise<string>;
14+
stderr: () => Promise<string>;
1515
wait?: (params?: { signal?: AbortSignal }) => Promise<MockWaitResult>;
1616
};
1717
type MockRunCommandParams = {
@@ -48,6 +48,7 @@ let runCommandMock = async (
4848
exitCode: 0,
4949
cmdId: "cmd-1",
5050
stdout: async () => "",
51+
stderr: async () => "",
5152
});
5253
let lastRunCommandEnv: Record<string, string> | undefined;
5354
let currentSessionStateFactory = (_name: string): MockSessionState => ({});
@@ -167,6 +168,7 @@ beforeEach(() => {
167168
exitCode: 0,
168169
cmdId: "cmd-1",
169170
stdout: async () => "",
171+
stderr: async () => "",
170172
});
171173
lastRunCommandEnv = undefined;
172174
currentSessionStateFactory = () => ({});
@@ -249,6 +251,36 @@ describe("VercelSandbox.environmentDetails", () => {
249251
});
250252
});
251253

254+
describe("VercelSandbox.exec", () => {
255+
test("preserves stderr output from failed commands", async () => {
256+
runCommandMock = async () => ({
257+
exitCode: 128,
258+
cmdId: "cmd-fetch-failed",
259+
stdout: async () => "",
260+
stderr: async () => "fatal: couldn't find remote ref feature\n",
261+
});
262+
263+
const sandbox = await sandboxModule.VercelSandbox.connect("sbx-test", {
264+
ports: [3000],
265+
remainingTimeout: 0,
266+
});
267+
268+
const result = await sandbox.exec(
269+
"git fetch origin feature",
270+
"/vercel/sandbox",
271+
5_000,
272+
);
273+
274+
expect(result).toEqual({
275+
success: false,
276+
exitCode: 128,
277+
stdout: "",
278+
stderr: "fatal: couldn't find remote ref feature\n",
279+
truncated: false,
280+
});
281+
});
282+
});
283+
252284
describe("VercelSandbox persistence", () => {
253285
test("connects by persistent sandbox name without auto-resume by default", async () => {
254286
const sandbox = await sandboxModule.VercelSandbox.connect("session_123", {
@@ -472,6 +504,7 @@ describe("VercelSandbox.execDetached", () => {
472504
runCommandMock = async () => ({
473505
cmdId: "cmd-detached-running",
474506
stdout: async () => "",
507+
stderr: async () => "",
475508
wait: async () => await new Promise<MockWaitResult>(() => {}),
476509
});
477510

@@ -508,6 +541,7 @@ describe("VercelSandbox.execDetached", () => {
508541
runCommandMock = async () => ({
509542
cmdId: "cmd-detached-error",
510543
stdout: async () => "",
544+
stderr: async () => "",
511545
wait: async () => {
512546
throw new Error("wait failed");
513547
},
@@ -527,6 +561,7 @@ describe("VercelSandbox.execDetached", () => {
527561
runCommandMock = async () => ({
528562
cmdId: "cmd-detached-fail",
529563
stdout: async () => "",
564+
stderr: async () => "",
530565
wait: async () => ({
531566
exitCode: 1,
532567
stdout: async () => "",

packages/sandbox/vercel/sandbox.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,20 @@ function getRemainingTimeoutFromSession(
156156
return remaining > 10_000 ? remaining : undefined;
157157
}
158158

159+
function truncateCommandOutput(output: string): {
160+
output: string;
161+
truncated: boolean;
162+
} {
163+
if (output.length <= MAX_OUTPUT_LENGTH) {
164+
return { output, truncated: false };
165+
}
166+
167+
return {
168+
output: output.slice(0, MAX_OUTPUT_LENGTH),
169+
truncated: true,
170+
};
171+
}
172+
159173
/**
160174
* Vercel Sandbox implementation using the @vercel/sandbox SDK.
161175
* Runs code in isolated Firecracker MicroVMs.
@@ -901,20 +915,19 @@ ${hostLine}${portLines}${runtimeEnvLine}`;
901915
signal,
902916
});
903917

904-
let stdout = await result.stdout();
905-
let truncated = false;
906-
907-
if (stdout.length > MAX_OUTPUT_LENGTH) {
908-
stdout = stdout.slice(0, MAX_OUTPUT_LENGTH);
909-
truncated = true;
910-
}
918+
const [rawStdout, rawStderr] = await Promise.all([
919+
result.stdout(),
920+
result.stderr(),
921+
]);
922+
const stdout = truncateCommandOutput(rawStdout);
923+
const stderr = truncateCommandOutput(rawStderr);
911924

912925
return {
913926
success: result.exitCode === 0,
914927
exitCode: result.exitCode,
915-
stdout,
916-
stderr: "", // Vercel SDK combines stdout/stderr
917-
truncated,
928+
stdout: stdout.output,
929+
stderr: stderr.output,
930+
truncated: stdout.truncated || stderr.truncated,
918931
};
919932
} catch (error) {
920933
if (error instanceof Error && error.name === "TimeoutError") {

0 commit comments

Comments
 (0)