From b32ad5ef134d4c1879b228f1855a4688a44372bb Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 25 Dec 2025 18:17:46 +0700 Subject: [PATCH] Add exploreFileSystem() to DaemonClient + E2E tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add exploreFileSystem(agentId, path, mode) method to DaemonClient - Sends file_explorer_request, waits for file_explorer_response - Returns {path, mode, directory, file, error} payload - Add 3 E2E tests: - "lists directory contents" - verifies entries with name, kind, size - "reads file contents" - verifies file object with path, kind, content - "returns error for non-existent path" - verifies error handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/server/src/server/daemon.e2e.test.ts | 122 +++++++++++++++++- .../src/server/test-utils/daemon-client.ts | 57 ++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/daemon.e2e.test.ts b/packages/server/src/server/daemon.e2e.test.ts index d78289bdf..490daea34 100644 --- a/packages/server/src/server/daemon.e2e.test.ts +++ b/packages/server/src/server/daemon.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest"; -import { mkdtempSync, writeFileSync, existsSync, rmSync } from "fs"; +import { mkdtempSync, writeFileSync, existsSync, rmSync, mkdirSync } from "fs"; import { tmpdir } from "os"; import path from "path"; import { @@ -1334,4 +1334,124 @@ describe("daemon E2E", () => { 60000 // 1 minute timeout ); }); + + describe("exploreFileSystem", () => { + test( + "lists directory contents", + async () => { + const cwd = tmpCwd(); + + // Create test files and directories + writeFileSync(path.join(cwd, "test.txt"), "hello world\n"); + writeFileSync(path.join(cwd, "data.json"), '{"key": "value"}\n'); + mkdirSync(path.join(cwd, "subdir")); + writeFileSync(path.join(cwd, "subdir", "nested.txt"), "nested content\n"); + + // Create agent in the directory + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "File Explorer Test", + }); + + expect(agent.id).toBeTruthy(); + expect(agent.status).toBe("idle"); + + // List directory contents + const result = await ctx.client.exploreFileSystem(agent.id, cwd, "list"); + + // Verify listing returned without error + expect(result.error).toBeNull(); + expect(result.mode).toBe("list"); + expect(result.directory).toBeTruthy(); + expect(result.directory!.entries).toBeTruthy(); + + // Find expected entries + const entries = result.directory!.entries; + const testTxt = entries.find((e) => e.name === "test.txt"); + const dataJson = entries.find((e) => e.name === "data.json"); + const subdir = entries.find((e) => e.name === "subdir"); + + expect(testTxt).toBeTruthy(); + expect(testTxt!.kind).toBe("file"); + expect(testTxt!.size).toBeGreaterThan(0); + + expect(dataJson).toBeTruthy(); + expect(dataJson!.kind).toBe("file"); + + expect(subdir).toBeTruthy(); + expect(subdir!.kind).toBe("directory"); + + // Cleanup + await ctx.client.deleteAgent(agent.id); + rmSync(cwd, { recursive: true, force: true }); + }, + 60000 // 1 minute timeout + ); + + test( + "reads file contents", + async () => { + const cwd = tmpCwd(); + const testContent = "This is test file content.\nLine 2."; + const testFile = path.join(cwd, "readme.txt"); + writeFileSync(testFile, testContent); + + // Create agent in the directory + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "File Read Test", + }); + + expect(agent.id).toBeTruthy(); + + // Read file contents + const result = await ctx.client.exploreFileSystem(agent.id, testFile, "file"); + + // Verify file read + expect(result.error).toBeNull(); + expect(result.mode).toBe("file"); + expect(result.file).toBeTruthy(); + // Server may return basename or full path + expect(result.file!.path).toContain("readme.txt"); + expect(result.file!.kind).toBe("text"); + expect(result.file!.content).toBe(testContent); + expect(result.file!.size).toBe(testContent.length); + + // Cleanup + await ctx.client.deleteAgent(agent.id); + rmSync(cwd, { recursive: true, force: true }); + }, + 60000 // 1 minute timeout + ); + + test( + "returns error for non-existent path", + async () => { + const cwd = tmpCwd(); + + // Create agent + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "File Explorer Error Test", + }); + + expect(agent.id).toBeTruthy(); + + // Try to list non-existent path + const nonExistent = path.join(cwd, "does-not-exist"); + const result = await ctx.client.exploreFileSystem(agent.id, nonExistent, "list"); + + // Should return 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 93de2b84f..b624babe4 100644 --- a/packages/server/src/server/test-utils/daemon-client.ts +++ b/packages/server/src/server/test-utils/daemon-client.ts @@ -432,6 +432,63 @@ export class DaemonClient { ); } + // ============================================================================ + // File Explorer + // ============================================================================ + + async exploreFileSystem( + agentId: string, + path: string, + mode: "list" | "file" = "list" + ): Promise<{ + path: string; + mode: "list" | "file"; + directory: { + path: string; + entries: Array<{ + name: string; + path: string; + kind: "file" | "directory"; + size: number; + modifiedAt: string; + }>; + } | null; + file: { + path: string; + kind: "text" | "image" | "binary"; + encoding: "utf-8" | "base64" | "none"; + content?: string; + mimeType?: string; + size: number; + modifiedAt: string; + } | null; + error: string | null; + }> { + const startPosition = this.messageQueue.length; + + this.send({ type: "file_explorer_request", agentId, path, mode }); + + return this.waitFor( + (msg) => { + if ( + msg.type === "file_explorer_response" && + msg.payload.agentId === agentId + ) { + return { + path: msg.payload.path, + mode: msg.payload.mode, + directory: msg.payload.directory, + file: msg.payload.file, + error: msg.payload.error, + }; + } + return null; + }, + 10000, + { skipQueueBefore: startPosition } + ); + } + // ============================================================================ // Permissions // ============================================================================