Buckets:
GHHG10/CodeServer / opencode /packages /app /src /components /prompt-input /build-request-parts.test.ts
| import { describe, expect, test } from "bun:test" | |
| import type { Prompt } from "@/context/prompt" | |
| import { buildRequestParts } from "./build-request-parts" | |
| describe("buildRequestParts", () => { | |
| test("builds typed request and optimistic parts without cast path", () => { | |
| const prompt: Prompt = [ | |
| { type: "text", content: "hello", start: 0, end: 5 }, | |
| { | |
| type: "file", | |
| path: "src/foo.ts", | |
| content: "@src/foo.ts", | |
| start: 5, | |
| end: 16, | |
| selection: { startLine: 4, startChar: 1, endLine: 6, endChar: 1 }, | |
| }, | |
| { type: "agent", name: "planner", content: "@planner", start: 16, end: 24 }, | |
| ] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [{ key: "ctx:1", type: "file", path: "src/bar.ts", comment: "check this" }], | |
| images: [ | |
| { type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, | |
| ], | |
| text: "hello @src/foo.ts @planner", | |
| messageID: "msg_1", | |
| sessionID: "ses_1", | |
| sessionDirectory: "/repo", | |
| }) | |
| expect(result.requestParts[0]?.type).toBe("text") | |
| expect(result.requestParts.some((part) => part.type === "agent")).toBe(true) | |
| expect( | |
| result.requestParts.some((part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts")), | |
| ).toBe(true) | |
| expect(result.requestParts.some((part) => part.type === "text" && part.synthetic)).toBe(true) | |
| expect( | |
| result.requestParts.some( | |
| (part) => | |
| part.type === "text" && | |
| part.synthetic && | |
| part.metadata?.opencodeComment && | |
| (part.metadata.opencodeComment as { comment?: string }).comment === "check this", | |
| ), | |
| ).toBe(true) | |
| expect(result.optimisticParts).toHaveLength(result.requestParts.length) | |
| expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true) | |
| }) | |
| test("keeps multiple uploaded attachments in order", () => { | |
| const result = buildRequestParts({ | |
| prompt: [{ type: "text", content: "check these", start: 0, end: 11 }], | |
| context: [], | |
| images: [ | |
| { type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, | |
| { | |
| type: "image", | |
| id: "img_2", | |
| filename: "b.pdf", | |
| mime: "application/pdf", | |
| dataUrl: "data:application/pdf;base64,BBB", | |
| }, | |
| ], | |
| text: "check these", | |
| messageID: "msg_multi", | |
| sessionID: "ses_multi", | |
| sessionDirectory: "/repo", | |
| }) | |
| const files = result.requestParts.filter((part) => part.type === "file" && part.url.startsWith("data:")) | |
| expect(files).toHaveLength(2) | |
| expect(files.map((part) => (part.type === "file" ? part.filename : ""))).toEqual(["a.png", "b.pdf"]) | |
| }) | |
| test("deduplicates context files when prompt already includes same path", () => { | |
| const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [ | |
| { key: "ctx:dup", type: "file", path: "src/foo.ts" }, | |
| { key: "ctx:comment", type: "file", path: "src/foo.ts", comment: "focus here" }, | |
| ], | |
| images: [], | |
| text: "@src/foo.ts", | |
| messageID: "msg_2", | |
| sessionID: "ses_2", | |
| sessionDirectory: "/repo", | |
| }) | |
| const fooFiles = result.requestParts.filter( | |
| (part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts"), | |
| ) | |
| const synthetic = result.requestParts.filter((part) => part.type === "text" && part.synthetic) | |
| expect(fooFiles).toHaveLength(2) | |
| expect(synthetic).toHaveLength(1) | |
| }) | |
| test("adds file parts for @mentions inside comment text", () => { | |
| const result = buildRequestParts({ | |
| prompt: [{ type: "text", content: "look", start: 0, end: 4 }], | |
| context: [ | |
| { | |
| key: "ctx:comment-mention", | |
| type: "file", | |
| path: "src/review.ts", | |
| comment: "Compare with @src/shared.ts and @src/review.ts.", | |
| }, | |
| ], | |
| images: [], | |
| text: "look", | |
| messageID: "msg_comment_mentions", | |
| sessionID: "ses_comment_mentions", | |
| sessionDirectory: "/repo", | |
| }) | |
| const files = result.requestParts.filter((part) => part.type === "file") | |
| expect(files).toHaveLength(2) | |
| expect(files.some((part) => part.type === "file" && part.url === "file:///repo/src/review.ts")).toBe(true) | |
| expect(files.some((part) => part.type === "file" && part.url === "file:///repo/src/shared.ts")).toBe(true) | |
| }) | |
| test("handles Windows paths correctly (simulated on macOS)", () => { | |
| const prompt: Prompt = [{ type: "file", path: "src\\foo.ts", content: "@src\\foo.ts", start: 0, end: 11 }] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [], | |
| images: [], | |
| text: "@src\\foo.ts", | |
| messageID: "msg_win_1", | |
| sessionID: "ses_win_1", | |
| sessionDirectory: "D:\\projects\\myapp", // Windows path | |
| }) | |
| // Should create valid file URLs | |
| const filePart = result.requestParts.find((part) => part.type === "file") | |
| expect(filePart).toBeDefined() | |
| if (filePart?.type === "file") { | |
| // URL should be parseable | |
| expect(() => new URL(filePart.url)).not.toThrow() | |
| // Should not have encoded backslashes in wrong place | |
| expect(filePart.url).not.toContain("%5C") | |
| // Should have normalized to forward slashes | |
| expect(filePart.url).toContain("/src/foo.ts") | |
| } | |
| }) | |
| test("handles Windows absolute path with special characters", () => { | |
| const prompt: Prompt = [{ type: "file", path: "file#name.txt", content: "@file#name.txt", start: 0, end: 14 }] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [], | |
| images: [], | |
| text: "@file#name.txt", | |
| messageID: "msg_win_2", | |
| sessionID: "ses_win_2", | |
| sessionDirectory: "C:\\Users\\test\\Documents", // Windows path | |
| }) | |
| const filePart = result.requestParts.find((part) => part.type === "file") | |
| expect(filePart).toBeDefined() | |
| if (filePart?.type === "file") { | |
| // URL should be parseable | |
| expect(() => new URL(filePart.url)).not.toThrow() | |
| // Special chars should be encoded | |
| expect(filePart.url).toContain("file%23name.txt") | |
| // Should have Windows drive letter properly encoded | |
| expect(filePart.url).toMatch(/file:\/\/\/[A-Z]:/) | |
| } | |
| }) | |
| test("handles Linux absolute paths correctly", () => { | |
| const prompt: Prompt = [{ type: "file", path: "src/app.ts", content: "@src/app.ts", start: 0, end: 10 }] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [], | |
| images: [], | |
| text: "@src/app.ts", | |
| messageID: "msg_linux_1", | |
| sessionID: "ses_linux_1", | |
| sessionDirectory: "/home/user/project", | |
| }) | |
| const filePart = result.requestParts.find((part) => part.type === "file") | |
| expect(filePart).toBeDefined() | |
| if (filePart?.type === "file") { | |
| // URL should be parseable | |
| expect(() => new URL(filePart.url)).not.toThrow() | |
| // Should be a normal Unix path | |
| expect(filePart.url).toBe("file:///home/user/project/src/app.ts") | |
| } | |
| }) | |
| test("handles macOS paths correctly", () => { | |
| const prompt: Prompt = [{ type: "file", path: "README.md", content: "@README.md", start: 0, end: 9 }] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [], | |
| images: [], | |
| text: "@README.md", | |
| messageID: "msg_mac_1", | |
| sessionID: "ses_mac_1", | |
| sessionDirectory: "/Users/kelvin/Projects/opencode", | |
| }) | |
| const filePart = result.requestParts.find((part) => part.type === "file") | |
| expect(filePart).toBeDefined() | |
| if (filePart?.type === "file") { | |
| // URL should be parseable | |
| expect(() => new URL(filePart.url)).not.toThrow() | |
| // Should be a normal Unix path | |
| expect(filePart.url).toBe("file:///Users/kelvin/Projects/opencode/README.md") | |
| } | |
| }) | |
| test("handles context files with Windows paths", () => { | |
| const prompt: Prompt = [] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [ | |
| { key: "ctx:1", type: "file", path: "src\\utils\\helper.ts" }, | |
| { key: "ctx:2", type: "file", path: "test\\unit.test.ts", comment: "check tests" }, | |
| ], | |
| images: [], | |
| text: "test", | |
| messageID: "msg_win_ctx", | |
| sessionID: "ses_win_ctx", | |
| sessionDirectory: "D:\\workspace\\app", | |
| }) | |
| const fileParts = result.requestParts.filter((part) => part.type === "file") | |
| expect(fileParts).toHaveLength(2) | |
| // All file URLs should be valid | |
| fileParts.forEach((part) => { | |
| if (part.type === "file") { | |
| expect(() => new URL(part.url)).not.toThrow() | |
| expect(part.url).not.toContain("%5C") // No encoded backslashes | |
| } | |
| }) | |
| }) | |
| test("handles absolute Windows paths (user manually specifies full path)", () => { | |
| const prompt: Prompt = [ | |
| { type: "file", path: "D:\\other\\project\\file.ts", content: "@D:\\other\\project\\file.ts", start: 0, end: 25 }, | |
| ] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [], | |
| images: [], | |
| text: "@D:\\other\\project\\file.ts", | |
| messageID: "msg_abs", | |
| sessionID: "ses_abs", | |
| sessionDirectory: "C:\\current\\project", | |
| }) | |
| const filePart = result.requestParts.find((part) => part.type === "file") | |
| expect(filePart).toBeDefined() | |
| if (filePart?.type === "file") { | |
| // Should handle absolute path that differs from sessionDirectory | |
| expect(() => new URL(filePart.url)).not.toThrow() | |
| expect(filePart.url).toContain("/D:/other/project/file.ts") | |
| } | |
| }) | |
| test("handles selection with query parameters on Windows", () => { | |
| const prompt: Prompt = [ | |
| { | |
| type: "file", | |
| path: "src\\App.tsx", | |
| content: "@src\\App.tsx", | |
| start: 0, | |
| end: 11, | |
| selection: { startLine: 10, startChar: 0, endLine: 20, endChar: 5 }, | |
| }, | |
| ] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [], | |
| images: [], | |
| text: "@src\\App.tsx", | |
| messageID: "msg_sel", | |
| sessionID: "ses_sel", | |
| sessionDirectory: "C:\\project", | |
| }) | |
| const filePart = result.requestParts.find((part) => part.type === "file") | |
| expect(filePart).toBeDefined() | |
| if (filePart?.type === "file") { | |
| // Should have query parameters | |
| expect(filePart.url).toContain("?start=10&end=20") | |
| // Should be valid URL | |
| expect(() => new URL(filePart.url)).not.toThrow() | |
| // Query params should parse correctly | |
| const url = new URL(filePart.url) | |
| expect(url.searchParams.get("start")).toBe("10") | |
| expect(url.searchParams.get("end")).toBe("20") | |
| } | |
| }) | |
| test("handles file paths with dots and special segments on Windows", () => { | |
| const prompt: Prompt = [ | |
| { type: "file", path: "..\\..\\shared\\util.ts", content: "@..\\..\\shared\\util.ts", start: 0, end: 21 }, | |
| ] | |
| const result = buildRequestParts({ | |
| prompt, | |
| context: [], | |
| images: [], | |
| text: "@..\\..\\shared\\util.ts", | |
| messageID: "msg_dots", | |
| sessionID: "ses_dots", | |
| sessionDirectory: "C:\\projects\\myapp\\src", | |
| }) | |
| const filePart = result.requestParts.find((part) => part.type === "file") | |
| expect(filePart).toBeDefined() | |
| if (filePart?.type === "file") { | |
| // Should be valid URL | |
| expect(() => new URL(filePart.url)).not.toThrow() | |
| // Should preserve .. segments (backend normalizes) | |
| expect(filePart.url).toContain("/..") | |
| } | |
| }) | |
| }) | |
Xet Storage Details
- Size:
- 11.6 kB
- Xet hash:
- b9348952574ab957ea9b8bd7cf98da9bf0007041c4d2345eb0378bd8164aa8c9
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.