mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
fix: use query params for file upload path (#13376)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach, Mock } from "vitest";
|
||||
import axios from "axios";
|
||||
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
|
||||
|
||||
const { mockGet } = vi.hoisted(() => ({ mockGet: vi.fn() }));
|
||||
@@ -6,6 +7,8 @@ vi.mock("#/api/open-hands-axios", () => ({
|
||||
openHands: { get: mockGet },
|
||||
}));
|
||||
|
||||
vi.mock("axios");
|
||||
|
||||
describe("V1ConversationService", () => {
|
||||
describe("readConversationFile", () => {
|
||||
it("uses default plan path when filePath is not provided", async () => {
|
||||
@@ -24,4 +27,91 @@ describe("V1ConversationService", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("uploadFile", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(axios.post as Mock).mockResolvedValue({ data: {} });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("uses query params for file upload path", async () => {
|
||||
// Arrange
|
||||
const conversationUrl = "http://localhost:54928/api/conversations/conv-123";
|
||||
const sessionApiKey = "test-api-key";
|
||||
const file = new File(["test content"], "test.txt", { type: "text/plain" });
|
||||
const uploadPath = "/workspace/custom/path.txt";
|
||||
|
||||
// Act
|
||||
await V1ConversationService.uploadFile(
|
||||
conversationUrl,
|
||||
sessionApiKey,
|
||||
file,
|
||||
uploadPath,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(axios.post).toHaveBeenCalledTimes(1);
|
||||
const callUrl = (axios.post as Mock).mock.calls[0][0] as string;
|
||||
|
||||
// Verify URL uses query params format
|
||||
expect(callUrl).toContain("/api/file/upload?");
|
||||
expect(callUrl).toContain("path=%2Fworkspace%2Fcustom%2Fpath.txt");
|
||||
|
||||
// Verify it's NOT using path params format
|
||||
expect(callUrl).not.toContain("/api/file/upload/%2F");
|
||||
});
|
||||
|
||||
it("uses default workspace path when no path provided", async () => {
|
||||
// Arrange
|
||||
const conversationUrl = "http://localhost:54928/api/conversations/conv-123";
|
||||
const sessionApiKey = "test-api-key";
|
||||
const file = new File(["test content"], "myfile.txt", { type: "text/plain" });
|
||||
|
||||
// Act
|
||||
await V1ConversationService.uploadFile(
|
||||
conversationUrl,
|
||||
sessionApiKey,
|
||||
file,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(axios.post).toHaveBeenCalledTimes(1);
|
||||
const callUrl = (axios.post as Mock).mock.calls[0][0] as string;
|
||||
|
||||
// Default path should be /workspace/{filename}
|
||||
expect(callUrl).toContain("path=%2Fworkspace%2Fmyfile.txt");
|
||||
});
|
||||
|
||||
it("sends file as FormData with correct headers", async () => {
|
||||
// Arrange
|
||||
const conversationUrl = "http://localhost:54928/api/conversations/conv-123";
|
||||
const sessionApiKey = "test-api-key";
|
||||
const file = new File(["test content"], "test.txt", { type: "text/plain" });
|
||||
|
||||
// Act
|
||||
await V1ConversationService.uploadFile(
|
||||
conversationUrl,
|
||||
sessionApiKey,
|
||||
file,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(axios.post).toHaveBeenCalledTimes(1);
|
||||
const callArgs = (axios.post as Mock).mock.calls[0];
|
||||
|
||||
// Verify FormData is sent
|
||||
const formData = callArgs[1];
|
||||
expect(formData).toBeInstanceOf(FormData);
|
||||
expect(formData.get("file")).toBe(file);
|
||||
|
||||
// Verify headers include session API key and content type
|
||||
const headers = callArgs[2].headers;
|
||||
expect(headers).toHaveProperty("X-Session-API-Key", sessionApiKey);
|
||||
expect(headers).toHaveProperty("Content-Type", "multipart/form-data");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -253,7 +253,7 @@ class V1ConversationService {
|
||||
|
||||
/**
|
||||
* Upload a single file to the V1 conversation workspace
|
||||
* V1 API endpoint: POST /api/file/upload/{path}
|
||||
* V1 API endpoint: POST /api/file/upload?path={path}
|
||||
*
|
||||
* @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
|
||||
* @param sessionApiKey Session API key for authentication (required for V1)
|
||||
@@ -269,10 +269,11 @@ class V1ConversationService {
|
||||
): Promise<void> {
|
||||
// Default to /workspace/{filename} if no path provided (must be absolute)
|
||||
const uploadPath = path || `/workspace/${file.name}`;
|
||||
const encodedPath = encodeURIComponent(uploadPath);
|
||||
const params = new URLSearchParams();
|
||||
params.append("path", uploadPath);
|
||||
const url = this.buildRuntimeUrl(
|
||||
conversationUrl,
|
||||
`/api/file/upload/${encodedPath}`,
|
||||
`/api/file/upload?${params.toString()}`,
|
||||
);
|
||||
const headers = buildSessionHeaders(sessionApiKey);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user