From bf83908a42402ec7b59d2084cf58cfc117b10496 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 25 Dec 2025 18:13:49 +0700 Subject: [PATCH] Add getGitRepoInfo() to DaemonClient + E2E tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added getGitRepoInfo(agentId) method that returns repo info (branches, currentBranch, isDirty, repoRoot) by looking up the agent's cwd and sending git_repo_info_request - Added 3 E2E tests: (1) returns repo info for git repo with branch and dirty state, (2) returns clean state when no uncommitted changes, (3) returns error for non-git directory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/server/src/server/daemon.e2e.test.ts | 119 ++++++++++++++++++ .../src/server/test-utils/daemon-client.ts | 43 +++++++ 2 files changed, 162 insertions(+) diff --git a/packages/server/src/server/daemon.e2e.test.ts b/packages/server/src/server/daemon.e2e.test.ts index 84472b378..d78289bdf 100644 --- a/packages/server/src/server/daemon.e2e.test.ts +++ b/packages/server/src/server/daemon.e2e.test.ts @@ -1215,4 +1215,123 @@ describe("daemon E2E", () => { 60000 // 1 minute timeout ); }); + + describe("getGitRepoInfo", () => { + test( + "returns repo info for git repo with branch and dirty state", + async () => { + const cwd = tmpCwd(); + + // Initialize git repo + const { execSync } = await import("child_process"); + execSync("git init", { cwd, stdio: "pipe" }); + execSync("git config user.email 'test@test.com'", { cwd, stdio: "pipe" }); + execSync("git config user.name 'Test'", { cwd, stdio: "pipe" }); + + // Create and commit a file + const testFile = path.join(cwd, "test.txt"); + writeFileSync(testFile, "original content\n"); + execSync("git add test.txt", { cwd, stdio: "pipe" }); + execSync("git commit -m 'Initial commit'", { cwd, stdio: "pipe" }); + + // Modify the file (makes repo dirty) + writeFileSync(testFile, "modified content\n"); + + // Create agent in the git repo + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "Git Repo Info Test", + }); + + expect(agent.id).toBeTruthy(); + expect(agent.status).toBe("idle"); + + // Get git repo info + const result = await ctx.client.getGitRepoInfo(agent.id); + + // Verify repo info returned without error + expect(result.error).toBeNull(); + // macOS symlinks /var to /private/var, so we check containment + expect(result.repoRoot).toContain("daemon-e2e-"); + expect(result.currentBranch).toBeTruthy(); + expect(result.branches.length).toBeGreaterThan(0); + expect(result.branches.some((b) => b.isCurrent)).toBe(true); + expect(result.isDirty).toBe(true); + + // Cleanup + await ctx.client.deleteAgent(agent.id); + rmSync(cwd, { recursive: true, force: true }); + }, + 60000 // 1 minute timeout + ); + + test( + "returns clean state when no uncommitted changes", + async () => { + const cwd = tmpCwd(); + + // Initialize git repo with clean state + const { execSync } = await import("child_process"); + execSync("git init", { cwd, stdio: "pipe" }); + execSync("git config user.email 'test@test.com'", { cwd, stdio: "pipe" }); + execSync("git config user.name 'Test'", { cwd, stdio: "pipe" }); + + // Create and commit a file (no uncommitted changes) + const testFile = path.join(cwd, "test.txt"); + writeFileSync(testFile, "content\n"); + execSync("git add test.txt", { cwd, stdio: "pipe" }); + execSync("git commit -m 'Initial commit'", { cwd, stdio: "pipe" }); + + // Create agent in the git repo + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "Git Repo Info Clean Test", + }); + + expect(agent.id).toBeTruthy(); + + // Get git repo info + const result = await ctx.client.getGitRepoInfo(agent.id); + + expect(result.error).toBeNull(); + expect(result.isDirty).toBe(false); + expect(result.currentBranch).toBeTruthy(); + + // Cleanup + await ctx.client.deleteAgent(agent.id); + rmSync(cwd, { recursive: true, force: true }); + }, + 60000 // 1 minute timeout + ); + + test( + "returns error for non-git directory", + async () => { + const cwd = tmpCwd(); + // Don't initialize git - just a regular directory + + // Create agent in a non-git directory + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "Git Repo Info Non-Git Test", + }); + + expect(agent.id).toBeTruthy(); + + // Get git repo info - should return error + const result = await ctx.client.getGitRepoInfo(agent.id); + + // Server returns cwd as repoRoot even on error, so we just check for error + expect(result.error).toBeTruthy(); + + // Cleanup + await ctx.client.deleteAgent(agent.id); + rmSync(cwd, { recursive: true, force: true }); + }, + 60000 // 1 minute timeout + ); + }); }); diff --git a/packages/server/src/server/test-utils/daemon-client.ts b/packages/server/src/server/test-utils/daemon-client.ts index 5498b9841..93de2b84f 100644 --- a/packages/server/src/server/test-utils/daemon-client.ts +++ b/packages/server/src/server/test-utils/daemon-client.ts @@ -389,6 +389,49 @@ export class DaemonClient { ); } + async getGitRepoInfo(agentId: string): Promise<{ + repoRoot: string; + currentBranch: string | null; + branches: Array<{ name: string; isCurrent: boolean }>; + isDirty: boolean; + error: string | null; + }> { + // Get the agent's cwd from the current list + const agents = this.listAgents(); + const agent = agents.find((a) => a.id === agentId); + if (!agent) { + return { + repoRoot: "", + currentBranch: null, + branches: [], + isDirty: false, + error: `Agent not found: ${agentId}`, + }; + } + + const cwd = agent.cwd; + const startPosition = this.messageQueue.length; + + this.send({ type: "git_repo_info_request", cwd }); + + return this.waitFor( + (msg) => { + if (msg.type === "git_repo_info_response" && msg.payload.cwd === cwd) { + return { + repoRoot: msg.payload.repoRoot, + currentBranch: msg.payload.currentBranch ?? null, + branches: msg.payload.branches ?? [], + isDirty: msg.payload.isDirty ?? false, + error: msg.payload.error ?? null, + }; + } + return null; + }, + 10000, + { skipQueueBefore: startPosition } + ); + } + // ============================================================================ // Permissions // ============================================================================