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:
co-authored by
Claude Opus 4.5
parent
bf83908a42
commit
b32ad5ef13
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest";
|
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 { tmpdir } from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {
|
import {
|
||||||
@@ -1334,4 +1334,124 @@ describe("daemon E2E", () => {
|
|||||||
60000 // 1 minute timeout
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
// Permissions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user