Skip to content

Commit 6aef9cc

Browse files
committed
🤖 Add comprehensive runtime tests for git operations and edge cases
- Add git to SSH test container Dockerfile (enables git operations testing) - Add 12 new test cases covering: - Git operations: init, commit, branches, status - Shell behavior: multi-line output, pipes, command substitution, large output - Error handling: command not found, syntax errors, permission denied - All tests run for both LocalRuntime and SSHRuntime (76 total: 38 × 2) - Tests verify runtime abstraction works consistently across environments Test results: 76 passed (38 local + 38 SSH)
1 parent 23c74d2 commit 6aef9cc

File tree

3 files changed

+232
-6
lines changed

3 files changed

+232
-6
lines changed

src/services/tools/file_edit_operation.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { describe, test, expect, beforeEach } from "@jest/globals";
2-
import { LocalRuntime } from "@/runtime/LocalRuntime";
1+
import { describe, test, expect } from "@jest/globals";
2+
import { executeFileEditOperation } from "./file_edit_operation";
3+
import { WRITE_DENIED_PREFIX } from "@/types/tools";
34
import { createRuntime } from "@/runtime/runtimeFactory";
4-
>>>>>>> a522bfce (🤖 Integrate runtime config with workspace metadata and AIService)
55

66
const TEST_CWD = "/tmp";
77

@@ -10,7 +10,7 @@ function createConfig() {
1010
}
1111

1212
describe("executeFileEditOperation", () => {
13-
it("should return error when path validation fails", async () => {
13+
test("should return error when path validation fails", async () => {
1414
const result = await executeFileEditOperation({
1515
config: createConfig(),
1616
filePath: "../../etc/passwd",

tests/runtime/runtime.test.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,232 @@ describeIntegration("Runtime integration tests", () => {
415415
expect(content).toBe("nested");
416416
});
417417
});
418+
419+
describe("Git operations", () => {
420+
test.concurrent("can initialize a git repository", async () => {
421+
const runtime = createRuntime();
422+
await using workspace = await TestWorkspace.create(runtime, type);
423+
424+
// Initialize git repo
425+
const result = await execBuffered(runtime, "git init", {
426+
cwd: workspace.path,
427+
timeout: 30,
428+
});
429+
430+
expect(result.exitCode).toBe(0);
431+
432+
// Verify .git directory exists
433+
const stat = await runtime.stat(`${workspace.path}/.git`);
434+
expect(stat.isDirectory).toBe(true);
435+
});
436+
437+
test.concurrent("can create commits", async () => {
438+
const runtime = createRuntime();
439+
await using workspace = await TestWorkspace.create(runtime, type);
440+
441+
// Initialize git and configure user
442+
await execBuffered(
443+
runtime,
444+
`git init && git config user.email "test@example.com" && git config user.name "Test User"`,
445+
{ cwd: workspace.path, timeout: 30 }
446+
);
447+
448+
// Create a file and commit
449+
await writeFileString(runtime, `${workspace.path}/test.txt`, "initial content");
450+
await execBuffered(runtime, `git add test.txt && git commit -m "Initial commit"`, {
451+
cwd: workspace.path,
452+
timeout: 30,
453+
});
454+
455+
// Verify commit exists
456+
const logResult = await execBuffered(runtime, "git log --oneline", {
457+
cwd: workspace.path,
458+
timeout: 30,
459+
});
460+
461+
expect(logResult.stdout).toContain("Initial commit");
462+
});
463+
464+
test.concurrent("can create and checkout branches", async () => {
465+
const runtime = createRuntime();
466+
await using workspace = await TestWorkspace.create(runtime, type);
467+
468+
// Setup git repo
469+
await execBuffered(
470+
runtime,
471+
`git init && git config user.email "test@example.com" && git config user.name "Test"`,
472+
{ cwd: workspace.path, timeout: 30 }
473+
);
474+
475+
// Create initial commit
476+
await writeFileString(runtime, `${workspace.path}/file.txt`, "content");
477+
await execBuffered(runtime, `git add file.txt && git commit -m "init"`, {
478+
cwd: workspace.path,
479+
timeout: 30,
480+
});
481+
482+
// Create and checkout new branch
483+
await execBuffered(runtime, "git checkout -b feature-branch", {
484+
cwd: workspace.path,
485+
timeout: 30,
486+
});
487+
488+
// Verify branch
489+
const branchResult = await execBuffered(runtime, "git branch --show-current", {
490+
cwd: workspace.path,
491+
timeout: 30,
492+
});
493+
494+
expect(branchResult.stdout.trim()).toBe("feature-branch");
495+
});
496+
497+
test.concurrent("can handle git status in dirty workspace", async () => {
498+
const runtime = createRuntime();
499+
await using workspace = await TestWorkspace.create(runtime, type);
500+
501+
// Setup git repo with commit
502+
await execBuffered(
503+
runtime,
504+
`git init && git config user.email "test@example.com" && git config user.name "Test"`,
505+
{ cwd: workspace.path, timeout: 30 }
506+
);
507+
await writeFileString(runtime, `${workspace.path}/file.txt`, "original");
508+
await execBuffered(runtime, `git add file.txt && git commit -m "init"`, {
509+
cwd: workspace.path,
510+
timeout: 30,
511+
});
512+
513+
// Make changes
514+
await writeFileString(runtime, `${workspace.path}/file.txt`, "modified");
515+
516+
// Check status
517+
const statusResult = await execBuffered(runtime, "git status --short", {
518+
cwd: workspace.path,
519+
timeout: 30,
520+
});
521+
522+
expect(statusResult.stdout).toContain("M file.txt");
523+
});
524+
});
525+
526+
describe("Environment and shell behavior", () => {
527+
test.concurrent("preserves multi-line output formatting", async () => {
528+
const runtime = createRuntime();
529+
await using workspace = await TestWorkspace.create(runtime, type);
530+
531+
const result = await execBuffered(runtime, 'echo "line1\nline2\nline3"', {
532+
cwd: workspace.path,
533+
timeout: 30,
534+
});
535+
536+
expect(result.stdout).toContain("line1");
537+
expect(result.stdout).toContain("line2");
538+
expect(result.stdout).toContain("line3");
539+
});
540+
541+
test.concurrent("handles commands with pipes", async () => {
542+
const runtime = createRuntime();
543+
await using workspace = await TestWorkspace.create(runtime, type);
544+
545+
await writeFileString(runtime, `${workspace.path}/test.txt`, "line1\nline2\nline3");
546+
547+
const result = await execBuffered(runtime, "cat test.txt | grep line2", {
548+
cwd: workspace.path,
549+
timeout: 30,
550+
});
551+
552+
expect(result.stdout.trim()).toBe("line2");
553+
});
554+
555+
test.concurrent("handles command substitution", async () => {
556+
const runtime = createRuntime();
557+
await using workspace = await TestWorkspace.create(runtime, type);
558+
559+
const result = await execBuffered(runtime, 'echo "Current dir: $(basename $(pwd))"', {
560+
cwd: workspace.path,
561+
timeout: 30,
562+
});
563+
564+
expect(result.stdout).toContain("Current dir:");
565+
});
566+
567+
test.concurrent("handles large stdout output", async () => {
568+
const runtime = createRuntime();
569+
await using workspace = await TestWorkspace.create(runtime, type);
570+
571+
// Generate large output (1000 lines)
572+
const result = await execBuffered(runtime, "seq 1 1000", {
573+
cwd: workspace.path,
574+
timeout: 30,
575+
});
576+
577+
const lines = result.stdout.trim().split("\n");
578+
expect(lines.length).toBe(1000);
579+
expect(lines[0]).toBe("1");
580+
expect(lines[999]).toBe("1000");
581+
});
582+
583+
test.concurrent("handles commands that produce no output but take time", async () => {
584+
const runtime = createRuntime();
585+
await using workspace = await TestWorkspace.create(runtime, type);
586+
587+
const result = await execBuffered(runtime, "sleep 0.1", {
588+
cwd: workspace.path,
589+
timeout: 30,
590+
});
591+
592+
expect(result.exitCode).toBe(0);
593+
expect(result.stdout).toBe("");
594+
expect(result.duration).toBeGreaterThanOrEqual(100);
595+
});
596+
});
597+
598+
describe("Error handling", () => {
599+
test.concurrent("handles command not found", async () => {
600+
const runtime = createRuntime();
601+
await using workspace = await TestWorkspace.create(runtime, type);
602+
603+
const result = await execBuffered(runtime, "nonexistentcommand", {
604+
cwd: workspace.path,
605+
timeout: 30,
606+
});
607+
608+
expect(result.exitCode).not.toBe(0);
609+
expect(result.stderr.toLowerCase()).toContain("not found");
610+
});
611+
612+
test.concurrent("handles syntax errors in bash", async () => {
613+
const runtime = createRuntime();
614+
await using workspace = await TestWorkspace.create(runtime, type);
615+
616+
const result = await execBuffered(runtime, "if true; then echo 'missing fi'", {
617+
cwd: workspace.path,
618+
timeout: 30,
619+
});
620+
621+
expect(result.exitCode).not.toBe(0);
622+
});
623+
624+
test.concurrent("handles permission denied errors", async () => {
625+
const runtime = createRuntime();
626+
await using workspace = await TestWorkspace.create(runtime, type);
627+
628+
// Create file without execute permission and try to execute it
629+
await writeFileString(runtime, `${workspace.path}/script.sh`, "#!/bin/sh\necho test");
630+
await execBuffered(runtime, "chmod 644 script.sh", {
631+
cwd: workspace.path,
632+
timeout: 30,
633+
});
634+
635+
const result = await execBuffered(runtime, "./script.sh", {
636+
cwd: workspace.path,
637+
timeout: 30,
638+
});
639+
640+
expect(result.exitCode).not.toBe(0);
641+
expect(result.stderr.toLowerCase()).toContain("permission denied");
642+
});
643+
});
418644
}
419645
);
420646
});

tests/runtime/ssh-server/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM alpine:latest
22

3-
# Install OpenSSH server
4-
RUN apk add --no-cache openssh-server
3+
# Install OpenSSH server and git
4+
RUN apk add --no-cache openssh-server git
55

66
# Create test user
77
RUN adduser -D -s /bin/sh testuser && \

0 commit comments

Comments
 (0)