Add exploreFileSystem() to DaemonClient + E2E tests

- 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 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-25 18:17:46 +07:00
parent bf83908a42
commit b32ad5ef13
2 changed files with 178 additions and 1 deletions

View File

@@ -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
);
});
});

View File

@@ -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
// ============================================================================