From 26ec7d20f0e7b3174270f7212b6c2be686ed35b7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 25 Dec 2025 18:10:00 +0700 Subject: [PATCH] Add getGitDiff() to DaemonClient + E2E tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add getGitDiff(agentId) method to DaemonClient that sends git_diff_request and waits for git_diff_response - Add 3 E2E tests covering: 1. Modified file in git repo returns correct diff 2. Clean repo returns empty diff 3. Non-git directory returns error 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/server/src/server/daemon.e2e.test.ts | 117 ++++++++++++++++++ .../src/server/test-utils/daemon-client.ts | 26 ++++ plan.md | 4 +- 3 files changed, 146 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/daemon.e2e.test.ts b/packages/server/src/server/daemon.e2e.test.ts index 2db880a89..84472b378 100644 --- a/packages/server/src/server/daemon.e2e.test.ts +++ b/packages/server/src/server/daemon.e2e.test.ts @@ -1098,4 +1098,121 @@ describe("daemon E2E", () => { 180000 ); }); + + describe("getGitDiff", () => { + test( + "returns diff for modified file in git repo", + 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 (creates unstaged changes) + writeFileSync(testFile, "modified content\n"); + + // Create agent in the git repo + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "Git Diff Test", + }); + + expect(agent.id).toBeTruthy(); + expect(agent.status).toBe("idle"); + + // Get git diff + const result = await ctx.client.getGitDiff(agent.id); + + // Verify diff returned without error + expect(result.error).toBeNull(); + expect(result.diff).toBeTruthy(); + expect(result.diff).toContain("test.txt"); + expect(result.diff).toContain("-original content"); + expect(result.diff).toContain("+modified content"); + + // Cleanup + await ctx.client.deleteAgent(agent.id); + rmSync(cwd, { recursive: true, force: true }); + }, + 60000 // 1 minute timeout + ); + + test( + "returns empty diff when no 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 + 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 (no modifications) + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "Git Diff Clean Test", + }); + + expect(agent.id).toBeTruthy(); + + // Get git diff - should be empty + const result = await ctx.client.getGitDiff(agent.id); + + expect(result.error).toBeNull(); + expect(result.diff).toBe(""); + + // 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 Diff Non-Git Test", + }); + + expect(agent.id).toBeTruthy(); + + // Get git diff - should return error + const result = await ctx.client.getGitDiff(agent.id); + + expect(result.diff).toBe(""); + expect(result.error).toBeTruthy(); + expect(result.error).toContain("git"); + + // 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 0ccbb6fe4..5498b9841 100644 --- a/packages/server/src/server/test-utils/daemon-client.ts +++ b/packages/server/src/server/test-utils/daemon-client.ts @@ -363,6 +363,32 @@ export class DaemonClient { this.send({ type: "set_agent_mode", agentId, modeId }); } + // ============================================================================ + // Git Operations + // ============================================================================ + + async getGitDiff( + agentId: string + ): Promise<{ diff: string; error: string | null }> { + const startPosition = this.messageQueue.length; + + this.send({ type: "git_diff_request", agentId }); + + return this.waitFor( + (msg) => { + if ( + msg.type === "git_diff_response" && + msg.payload.agentId === agentId + ) { + return { diff: msg.payload.diff, error: msg.payload.error }; + } + return null; + }, + 10000, + { skipQueueBefore: startPosition } + ); + } + // ============================================================================ // Permissions // ============================================================================ diff --git a/plan.md b/plan.md index 0c0a0cde3..afc4a879a 100644 --- a/plan.md +++ b/plan.md @@ -1174,7 +1174,7 @@ Goal: Expand DaemonClient to cover all daemon WebSocket capabilities so the app - **Done (2025-12-25 18:06)**: WHAT: Created shared `useTempClaudeConfigDir()` helper in `test-utils/claude-config.ts:22-49`, exported from `test-utils/index.ts:14`. Updated `daemon.e2e.test.ts:904-915` to add beforeAll/afterAll hooks using this helper and removed `.skip` from `describe("permission flow: Claude")`. Refactored `claude-agent.test.ts:19` to import shared helper and removed duplicate local implementation. RESULT: Both daemon Claude permission tests now pass. EVIDENCE: `npx vitest run --testNamePattern="permission flow: Claude"` shows: ✓ approves permission and executes command (8424ms), ✓ denies permission and prevents execution (7945ms). Direct `claude-agent.test.ts` permission tests also pass (4 tests). Typecheck passes. -- [ ] **Implement**: Add `getGitDiff()` to DaemonClient + E2E test. +- [x] **Implement**: Add `getGitDiff()` to DaemonClient + E2E test. **Context**: App calls `git_diff_request` to get file diffs. DaemonClient doesn't support this yet. @@ -1187,6 +1187,8 @@ Goal: Expand DaemonClient to cover all daemon WebSocket capabilities so the app - Method returns diff content - E2E test passes + - **Done (2025-12-25 19:33)**: WHAT: Added `getGitDiff(agentId: string)` method to `packages/server/src/server/test-utils/daemon-client.ts:370-390`. Added 3 E2E tests in `packages/server/src/server/daemon.e2e.test.ts:1102-1217`: (1) "returns diff for modified file in git repo" - creates temp git repo, commits file, modifies it, verifies diff contains expected content; (2) "returns empty diff when no changes" - creates clean repo, verifies empty diff; (3) "returns error for non-git directory" - verifies error message for non-git cwd. RESULT: All 3 tests pass. Method returns `{diff: string, error: string | null}` matching the server's `git_diff_response` payload structure. EVIDENCE: `npm run test --workspace=@paseo/server -- daemon.e2e.test.ts -t "getGitDiff"` shows 3 passed (12 skipped). + - [ ] **Implement**: Add `getGitRepoInfo()` to DaemonClient + E2E test. **Context**: App calls `git_repo_info_request` to get repo info (branch, status, etc).