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";
|
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
|
||||||
|
|
||||||
const { mockGet } = vi.hoisted(() => ({ mockGet: vi.fn() }));
|
const { mockGet } = vi.hoisted(() => ({ mockGet: vi.fn() }));
|
||||||
@@ -6,6 +7,8 @@ vi.mock("#/api/open-hands-axios", () => ({
|
|||||||
openHands: { get: mockGet },
|
openHands: { get: mockGet },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("axios");
|
||||||
|
|
||||||
describe("V1ConversationService", () => {
|
describe("V1ConversationService", () => {
|
||||||
describe("readConversationFile", () => {
|
describe("readConversationFile", () => {
|
||||||
it("uses default plan path when filePath is not provided", async () => {
|
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
|
* 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 conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
|
||||||
* @param sessionApiKey Session API key for authentication (required for V1)
|
* @param sessionApiKey Session API key for authentication (required for V1)
|
||||||
@@ -269,10 +269,11 @@ class V1ConversationService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Default to /workspace/{filename} if no path provided (must be absolute)
|
// Default to /workspace/{filename} if no path provided (must be absolute)
|
||||||
const uploadPath = path || `/workspace/${file.name}`;
|
const uploadPath = path || `/workspace/${file.name}`;
|
||||||
const encodedPath = encodeURIComponent(uploadPath);
|
const params = new URLSearchParams();
|
||||||
|
params.append("path", uploadPath);
|
||||||
const url = this.buildRuntimeUrl(
|
const url = this.buildRuntimeUrl(
|
||||||
conversationUrl,
|
conversationUrl,
|
||||||
`/api/file/upload/${encodedPath}`,
|
`/api/file/upload?${params.toString()}`,
|
||||||
);
|
);
|
||||||
const headers = buildSessionHeaders(sessionApiKey);
|
const headers = buildSessionHeaders(sessionApiKey);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user